remove httpx dependency

This commit is contained in:
Jakob Pinterits
2024-11-07 22:39:16 +01:00
parent f1abd075e9
commit 6e30b4f294
8 changed files with 228 additions and 31 deletions

View File

@@ -2,6 +2,8 @@
- New styles for input boxes: "rounded" and "pill"
- Improved mobile support: Dragging is now much smoother
- Improved tables
- `rio run` now also works when using `as_fastapi`
## 0.10

View File

@@ -10,7 +10,6 @@ dependencies = [
"fastapi~=0.110",
"fuzzywuzzy~=0.18",
"gitignore-parser==0.1.11",
"httpx~=0.27.2",
"introspection~=1.9.2",
"isort~=5.13",
"keyring~=24.3",

171
rio/arequests.py Normal file
View File

@@ -0,0 +1,171 @@
import asyncio
import json as json_module
import typing as t
import urllib.error
import urllib.parse
import urllib.request
import urllib.response
class HttpError(Exception):
"""
Raised when an HTTP request fails.
The status code is `None` if the error wasn't caused by HTTP, but some other
reason like a network error.
"""
def __init__(
self,
message: str,
status_code: int | None,
) -> None:
super().__init__(message, status_code)
@property
def message(self) -> str:
return self.args[0]
@property
def status_code(self) -> int | None:
return self.args[1]
class HttpResponse:
"""
Represents an HTTP response.
"""
def __init__(
self,
*,
status_code: int,
headers: dict[str, str],
content: bytes,
) -> None:
self.status_code = status_code
self.headers = headers
self._content = content
def read(self) -> bytes:
"""
Returns the response body as bytes.
"""
return self._content
def json(self) -> t.Any:
"""
Returns the response body as a JSON object. Raises a
`json.JSONDecodeError` if the response body is not valid JSON.
"""
try:
return json_module.loads(self._content)
except UnicodeDecodeError:
raise json_module.JSONDecodeError(
"The response body is not valid UTF-8",
"",
0,
)
def _request_sync(
method: t.Literal["GET", "POST"],
url: str,
*,
content: str | bytes | None = None,
json: dict[str, t.Any] | None = None,
headers: dict[str, str] | None = None,
) -> HttpResponse:
"""
Makes an HTTP request with the specified parameters. Returns the response
headers and body.
"""
# Prepare the request
req = urllib.request.Request(url, method=method)
if headers:
for key, value in headers.items():
req.add_header(key, value)
if json:
if content is not None:
raise ValueError("Cannot specify both `content` and `json`")
content = json_module.dumps(json)
if content:
if isinstance(content, str):
content = content.encode("utf-8")
req.data = content
# Make the request
try:
with urllib.request.urlopen(req) as response:
# Check the status code
if response.status >= 300:
raise HttpError(
response.reason,
response.status,
)
# Epic success!
return HttpResponse(
status_code=response.status,
headers={
key.lower(): value for key, value in response.getheaders()
},
content=response.read(),
)
except urllib.error.HTTPError as e:
raise HttpError(
e.reason,
e.code,
) from None
except urllib.error.URLError as e:
raise HttpError(
str(e.reason),
None,
) from None
async def request(
method: t.Literal["GET", "POST"],
url: str,
*,
content: bytes | None = None,
json: dict[str, t.Any] | None = None,
headers: dict[str, str] | None = None,
) -> HttpResponse:
"""
Makes an HTTP request with the specified parameters. Returns the response
headers and body.
"""
return await asyncio.to_thread(
_request_sync,
method,
url,
content=content,
json=json,
headers=headers,
)
async def main() -> None:
response = await request(
"GET",
"https://postman-echo.com/get?foo=bar",
)
print(response.status_code)
print(response.headers)
print(response.json())
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -7,12 +7,12 @@ import os
import typing as t
from pathlib import Path
import httpx
import typing_extensions as te
from PIL.Image import Image
from yarl import URL
import rio
import rio.arequests as arequests
from .self_serializing import SelfSerializing
from .utils import ImageLike
@@ -257,17 +257,17 @@ class UrlAsset(Asset):
async def try_fetch_as_blob(self) -> tuple[bytes, str | None]:
try:
async with httpx.AsyncClient() as client:
response = await client.get(str(self._url))
response = await arequests.request("GET", str(self._url))
content_type = response.headers.get("Content-Type")
if content_type is None:
content_type = "application/octet-stream"
else:
content_type, _, _ = content_type.partition(";")
content_type = response.headers.get("content-type")
return response.read(), content_type
except httpx.HTTPError:
if isinstance(content_type, str):
content_type, _, _ = content_type.partition(";")
else:
content_type = "application/octet-stream"
return response.read(), content_type
except arequests.HttpError:
raise ValueError(f"Could not fetch asset from {self._url}")
@property

View File

@@ -1,7 +1,7 @@
import typing as t
from datetime import timedelta
import httpx
import rio.arequests as arequests
BASE_URL = "https://rio.dev/api"
@@ -35,9 +35,8 @@ class RioApi:
def __init__(
self,
access_token: str | None = None,
):
) -> None:
self._access_token = access_token
self._http_client = httpx.AsyncClient()
@property
def is_logged_in(self) -> bool:
@@ -51,7 +50,7 @@ class RioApi:
async def close(self) -> None:
"""
Log out, if logged in and close the HTTP client.
Log out, if currently logged in.
"""
if self.is_logged_in:
try:
@@ -59,8 +58,6 @@ class RioApi:
except ApiException:
pass
await self._http_client.aclose()
async def request(
self,
endpoint: str,
@@ -85,7 +82,7 @@ class RioApi:
# Make the request
#
# TODO: Which exceptions can this throw?
response = await self._http_client.request(
response = await arequests.request(
method,
f"{BASE_URL}/{endpoint}",
headers=headers,

View File

@@ -10,11 +10,11 @@ import typing as t
from datetime import datetime, timedelta, timezone
from pathlib import Path
import httpx
import revel
from revel import print
import rio.app_server.fastapi_server
import rio.arequests as arequests
import rio.cli
import rio.snippets
@@ -187,21 +187,17 @@ class Arbiter:
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
"https://pypi.org/pypi/rio-ui/json",
timeout=10,
)
response.raise_for_status()
response = await arequests.request(
"GET",
"https://pypi.org/pypi/rio-ui/json",
)
data = response.json()
return data["info"]["version"]
data = response.json()
return data["info"]["version"]
# Oh muh gawd soo many errors
except (
httpx.HTTPError,
httpx.StreamError,
httpx.ProtocolError,
arequests.HttpError,
json.JSONDecodeError,
) as exc:
raise ValueError(f"Failed to fetch the latest Rio version: {exc}")

View File

@@ -121,7 +121,16 @@ class DevelComponent(FundamentalComponent):
tasks = [
("sass", str(scss_path), str(css_path)),
("tsc", str(ts_path), "--outFile", str(js_path)),
(
"tsc",
"--lib",
"dom,es6",
str(ts_path),
"--target",
"ES6",
"--outFile",
str(js_path),
),
]
with concurrent.futures.ThreadPoolExecutor() as executor:

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
import hashlib
import mimetypes
import os
@@ -8,6 +9,7 @@ import secrets
import socket
import typing as t
from dataclasses import dataclass
from http.client import HTTPSConnection
from io import BytesIO, StringIO
from pathlib import Path
@@ -565,3 +567,24 @@ def soft_sort(
for element, _, _ in keyed_elements:
elements.append(element)
async def async_http_request(url: str) -> Tuple[Dict[str, str], bytes]:
"""Performs an async HTTP GET request to the given URL and returns headers and content blob."""
loop = asyncio.get_running_loop()
host, path = url.split("/", 1)
def fetch() -> Tuple[Dict[str, str], bytes]:
conn = HTTPSConnection(host)
conn.request("GET", f"/{path}")
response = conn.getresponse()
headers = dict(response.getheaders())
blob = response.read()
conn.close()
return headers, blob
return await loop.run_in_executor(None, fetch)
# Example usage:
# headers, blob = asyncio.run(async_http_request("example.com/some-path"))