h
This commit is contained in:
parent
2a04991e5c
commit
d48cc7024a
|
@ -3,8 +3,7 @@ __version__ = "0.1.0"
|
|||
|
||||
from .application import Application, StaticHandler
|
||||
from .client import Client
|
||||
from .enums import HttpStatus, PridePalette, SassOutputStyle
|
||||
from .error import HttpError
|
||||
from .enums import PridePalette, SassOutputStyle
|
||||
from .request import Request
|
||||
from .response import Response, FileResponse, TemplateResponse
|
||||
from .runner import Runner, UvicornRunner, GranianRunner
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import traceback
|
||||
|
||||
from blib import Signal
|
||||
from blib import HttpError, Signal
|
||||
from collections.abc import Callable
|
||||
from mimetypes import guess_type
|
||||
from os.path import normpath
|
||||
|
@ -10,7 +10,6 @@ from pathlib import Path
|
|||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from .client import Client
|
||||
from .error import HttpError
|
||||
from .misc import StateProxy, Stream, ReaderFunction, WriterFunction
|
||||
from .request import Request, ScopeType
|
||||
from .response import FileResponse, Response
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
import aputils
|
||||
import httpx
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
from aputils import (
|
||||
AlgorithmType,
|
||||
JsonBase,
|
||||
Message,
|
||||
Nodeinfo,
|
||||
Webfinger,
|
||||
WellKnownNodeinfo,
|
||||
Signer,
|
||||
register_signer
|
||||
)
|
||||
|
||||
from . import __version__
|
||||
|
||||
|
||||
T = TypeVar("T", bound = aputils.JsonBase)
|
||||
T = TypeVar("T", bound = JsonBase)
|
||||
|
||||
|
||||
class Client:
|
||||
|
@ -38,16 +48,16 @@ class Client:
|
|||
async def request(self,
|
||||
method: str,
|
||||
url: str,
|
||||
body: aputils.JsonBase | None = None,
|
||||
body: JsonBase | None = None,
|
||||
headers: dict[str, str] | None = None,
|
||||
signer: aputils.Signer | None = None,
|
||||
algorithm: aputils.AlgorithmType | str = aputils.AlgorithmType.RSASHA256,
|
||||
signer: Signer | None = None,
|
||||
algorithm: AlgorithmType | str = AlgorithmType.RSASHA256,
|
||||
stream: bool = False,
|
||||
follow_redirects: bool = False) -> httpx.Response:
|
||||
|
||||
url = url.split("#", 1)[0]
|
||||
|
||||
if body is not None and not isinstance(body, aputils.JsonBase):
|
||||
if body is not None and not isinstance(body, JsonBase):
|
||||
raise TypeError("body must be a JsonBase object")
|
||||
|
||||
data = body.to_json() if body else None
|
||||
|
@ -69,22 +79,22 @@ class Client:
|
|||
async def fetch_json(self,
|
||||
url: str,
|
||||
cls: type[T] | None = None,
|
||||
signer: aputils.Signer | None = None) -> T:
|
||||
signer: Signer | None = None) -> T:
|
||||
|
||||
message_class: type[T] = aputils.JsonBase if cls is None else cls # type: ignore
|
||||
message_class: type[T] = JsonBase if cls is None else cls # type: ignore
|
||||
|
||||
headers = {
|
||||
"Accept": "application/" + ("activity+json" if cls is aputils.Message else "json")
|
||||
"Accept": "application/" + ("activity+json" if cls is Message else "json")
|
||||
}
|
||||
|
||||
response = await self.get(url, headers, signer)
|
||||
return message_class.parse(response.json())
|
||||
|
||||
|
||||
async def fetch_nodeinfo(self, domain: str) -> aputils.Nodeinfo:
|
||||
async def fetch_nodeinfo(self, domain: str) -> Nodeinfo:
|
||||
wk_nodeinfo = await self.fetch_json(
|
||||
f"https://{domain}/.well-known/nodeinfo",
|
||||
aputils.WellKnownNodeinfo
|
||||
WellKnownNodeinfo
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -93,19 +103,19 @@ class Client:
|
|||
except KeyError:
|
||||
url = wk_nodeinfo.v20
|
||||
|
||||
return await self.fetch_json(url, aputils.Nodeinfo)
|
||||
return await self.fetch_json(url, Nodeinfo)
|
||||
|
||||
|
||||
async def fetch_webfinger(self, username: str, domain: str) -> aputils.Webfinger:
|
||||
async def fetch_webfinger(self, username: str, domain: str) -> Webfinger:
|
||||
url = f"https://{domain}/.well-known/webfinger?resource=acct:{username}@{domain}"
|
||||
return await self.fetch_json(url, aputils.Webfinger)
|
||||
return await self.fetch_json(url, Webfinger)
|
||||
|
||||
|
||||
async def get(self,
|
||||
url: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
signer: aputils.Signer | None = None,
|
||||
algorithm: aputils.AlgorithmType | str = aputils.AlgorithmType.RSASHA256,
|
||||
signer: Signer | None = None,
|
||||
algorithm: AlgorithmType | str = AlgorithmType.RSASHA256,
|
||||
stream: bool = False,
|
||||
follow_redirects: bool = False) -> httpx.Response:
|
||||
|
||||
|
@ -117,8 +127,8 @@ class Client:
|
|||
async def head(self,
|
||||
url: str,
|
||||
headers: dict[str, str] | None = None,
|
||||
signer: aputils.Signer | None = None,
|
||||
algorithm: aputils.AlgorithmType | str = aputils.AlgorithmType.RSASHA256,
|
||||
signer: Signer | None = None,
|
||||
algorithm: AlgorithmType | str = AlgorithmType.RSASHA256,
|
||||
follow_redirects: bool = False) -> httpx.Response:
|
||||
|
||||
return await self.request(
|
||||
|
@ -128,10 +138,10 @@ class Client:
|
|||
|
||||
async def post(self,
|
||||
url: str,
|
||||
body: aputils.JsonBase,
|
||||
body: JsonBase,
|
||||
headers: dict[str, str] | None = None,
|
||||
signer: aputils.Signer | None = None,
|
||||
algorithm: aputils.AlgorithmType | str = aputils.AlgorithmType.RSASHA256,
|
||||
signer: Signer | None = None,
|
||||
algorithm: AlgorithmType | str = AlgorithmType.RSASHA256,
|
||||
stream: bool = False,
|
||||
follow_redirects: bool = False) -> httpx.Response:
|
||||
|
||||
|
@ -140,11 +150,11 @@ class Client:
|
|||
)
|
||||
|
||||
|
||||
@aputils.register_signer(httpx.Request)
|
||||
@register_signer(httpx.Request)
|
||||
def handle_httpx_sign(
|
||||
signer: aputils.Signer,
|
||||
signer: Signer,
|
||||
request: httpx.Request,
|
||||
algorithm: aputils.AlgorithmType) -> httpx.Request:
|
||||
algorithm: AlgorithmType) -> httpx.Request:
|
||||
|
||||
headers = signer.sign_headers(
|
||||
request.method,
|
||||
|
|
|
@ -1,89 +1,4 @@
|
|||
import re
|
||||
|
||||
from blib import Enum, IntEnum, StrEnum
|
||||
|
||||
|
||||
CAPITAL = re.compile("[A-Z][^A-Z]")
|
||||
|
||||
|
||||
class HttpStatus(IntEnum):
|
||||
# 1xx
|
||||
Continue = 100
|
||||
SwitchingProtocols = 101
|
||||
Processing = 102
|
||||
|
||||
# 2xx
|
||||
Ok = 200
|
||||
Created = 201
|
||||
Accepted = 202
|
||||
NonauthritativeInformation = 203
|
||||
NoContent = 204
|
||||
ResetContent = 205
|
||||
PartialContent = 206
|
||||
MultiStatus = 207
|
||||
AlreadyReported = 208
|
||||
ImUsed = 226
|
||||
|
||||
# 3xx
|
||||
MultipleChoices = 300
|
||||
MovedPermanently = 301
|
||||
Found = 302
|
||||
SeeOther = 303
|
||||
NotModified = 304
|
||||
UseProxy = 305
|
||||
TemporaryRedirect = 307
|
||||
PermanentRedirect = 308
|
||||
|
||||
# 4xx
|
||||
BadRequest = 400
|
||||
Unauthorized = 401
|
||||
PaymentRequired = 402
|
||||
Forbidden = 403
|
||||
NotFound = 404
|
||||
MethodNotAllowed = 405
|
||||
NotAcceptable = 406
|
||||
ProxyAuthenticationRequired = 407
|
||||
RequestTimeout = 408
|
||||
Conflict = 409
|
||||
Gone = 410
|
||||
LengthRequired = 411
|
||||
PreconditionFailed = 412
|
||||
PayloadTooLarge = 413
|
||||
RequestUriTooLarge = 414
|
||||
UnsupportedMediaType = 415
|
||||
RequestRangeNotSatisfiable = 416
|
||||
ExpectationFailed = 417
|
||||
ImATeapot = 418
|
||||
EnhanceYourCalm = 420
|
||||
MisdirectedRequest = 421
|
||||
UnprocessableEntity = 422
|
||||
Locked = 423
|
||||
FailedDependency = 424
|
||||
UpgradeRequired = 426
|
||||
PreconditionRequired = 428
|
||||
TooManyRequests = 429
|
||||
RequestHeaderFieldsTooLarge = 431
|
||||
ConnectionClosedWithoutResponse = 444
|
||||
UnavailableForLegalReasons = 451
|
||||
ClientClosedRequest = 499
|
||||
|
||||
InternalServerError = 500
|
||||
NotImplemented = 501
|
||||
BadGateway = 502
|
||||
ServiceUnavailable = 503
|
||||
GatewayTimeout = 504
|
||||
HttpVersionNotSupported = 505
|
||||
VariantAlsoNegotiates = 506
|
||||
InsufficientStorage = 507
|
||||
LoopDetected = 508
|
||||
NotExtended = 512
|
||||
NetworkAuthenticationRequired = 511
|
||||
NetworkConnectTimeoutError = 599
|
||||
|
||||
|
||||
@property
|
||||
def reason(self) -> str:
|
||||
return " ".join(CAPITAL.findall(self.name))
|
||||
from blib import Enum, StrEnum
|
||||
|
||||
|
||||
class PridePalette(tuple[str, ...], Enum):
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
from typing import Any, Self
|
||||
|
||||
from .enums import HttpStatus
|
||||
|
||||
|
||||
class HttpError(Exception):
|
||||
def __init__(self,
|
||||
status: HttpStatus | int,
|
||||
message: str,
|
||||
headers: dict[str, str] | None = None) -> None:
|
||||
|
||||
Exception.__init__(self, f'HTTP ERROR {status}: {message[:100]}')
|
||||
|
||||
self.status: HttpStatus = HttpStatus.parse(status)
|
||||
self.message: str = message
|
||||
self.headers: dict[str, str] = headers or {}
|
||||
|
||||
|
||||
@classmethod
|
||||
def MOVED_PERMANENTLY(cls: type[Self], location: str, **kwargs: Any) -> Self:
|
||||
err = cls(301, f'Resource moved to <a href="{location}">{location}</a>', **kwargs)
|
||||
err.headers["Location"] = location
|
||||
return err
|
||||
|
||||
|
||||
@classmethod
|
||||
def FOUND(cls: type[Self], location: str, **kwargs: Any) -> Self:
|
||||
err = cls(302, f'Resource moved to <a href="{location}">{location}</a>', **kwargs)
|
||||
err.headers["Location"] = location
|
||||
return err
|
||||
|
||||
|
||||
@classmethod
|
||||
def TEMPORARY_REDIRECT(cls: type[Self], location: str, **kwargs: Any) -> Self:
|
||||
err = cls(307, f'Resource moved to <a href="{location}">{location}</a>', **kwargs)
|
||||
err.headers["Location"] = location
|
||||
return err
|
||||
|
||||
|
||||
@classmethod
|
||||
def PERMANENT_REDIRECT(cls: type[Self], location: str, **kwargs: Any) -> Self:
|
||||
err = cls(308, f'Resource moved to <a href="{location}">{location}</a>', **kwargs)
|
||||
err.headers["Location"] = location
|
||||
return err
|
||||
|
||||
|
||||
@classmethod
|
||||
def BAD_REQUEST(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(400, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def UNAUTHORIZED(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(401, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def FORBIDDEN(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(403, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def NOT_FOUND(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(404, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def METHOD_NOT_ALLOWED(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(405, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def GONE(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(410, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def SERVER_ERROR(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(500, message, **kwargs)
|
||||
|
||||
|
||||
@classmethod
|
||||
def NOT_IMPLEMENTED(cls: type[Self], message: str, **kwargs: Any) -> Self:
|
||||
return cls(501, message, **kwargs)
|
||||
|
||||
|
||||
@property
|
||||
def reason(self) -> str:
|
||||
return self.status.reason
|
|
@ -10,6 +10,7 @@ from collections.abc import (
|
|||
Iterator,
|
||||
ItemsView,
|
||||
KeysView,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
Sequence,
|
||||
ValuesView
|
||||
|
@ -268,7 +269,7 @@ class Cookie:
|
|||
domain: str | None = None,
|
||||
secure: bool = False,
|
||||
http_only: bool = False,
|
||||
same_site: Literal["lax", "strict", "none"] | None = "lax") -> None:
|
||||
same_site: Literal["lax", "strict", "none"] | None = None) -> None:
|
||||
"""
|
||||
Create a new cookie
|
||||
|
||||
|
@ -310,7 +311,7 @@ class Cookie:
|
|||
self.http_only: bool = False
|
||||
"Prevent on-page javascript from accessing the cookie"
|
||||
|
||||
self.same_site: Literal["lax", "strict", "none"] = same_site or "none"
|
||||
self.same_site: Literal["lax", "strict", "none"] | None = same_site
|
||||
"Make sure the cookie stays on the same website"
|
||||
|
||||
|
||||
|
@ -323,10 +324,10 @@ class Cookie:
|
|||
:param data: Dictionary of cookie properties
|
||||
"""
|
||||
|
||||
same_site = data.get("same_site", data.get("samesite", "none")).lower()
|
||||
same_site = data.get("same_site", data.get("samesite", None))
|
||||
|
||||
if same_site not in {"lax", "strict", "none"}:
|
||||
raise ValueError(f"same_site must be 'lax', 'strict', or 'none', not {same_site}")
|
||||
if same_site not in {"lax", "strict", "none", None}:
|
||||
raise ValueError(f"same_site must be 'lax', 'strict', 'none' or None, not {same_site}")
|
||||
|
||||
max_age = data.get("max_age", data.get("maxage"))
|
||||
|
||||
|
@ -339,7 +340,7 @@ class Cookie:
|
|||
data.get("domain"),
|
||||
data.get("secure", False),
|
||||
data.get("http_only", data.get("httponly", False)),
|
||||
same_site
|
||||
same_site.lower() if isinstance(same_site, str) else None # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
|
@ -397,10 +398,12 @@ class Cookie:
|
|||
|
||||
values = [
|
||||
f"{self.key}={self.value or ''}",
|
||||
f"path={self.path}",
|
||||
f"samesite={self.same_site}"
|
||||
f"path={self.path}"
|
||||
]
|
||||
|
||||
if self.same_site is not None:
|
||||
values.append(f"samesite={self.same_site}")
|
||||
|
||||
if self.max_age is not None:
|
||||
values.append(f"maxage={self.max_age.seconds}")
|
||||
|
||||
|
@ -576,7 +579,10 @@ class Stream:
|
|||
return data.get("body", b""), data.get("more_body", False)
|
||||
|
||||
|
||||
async def write_headers(self, status: int, headers: Sequence[tuple[bytes, bytes]]) -> None:
|
||||
async def write_headers(self,
|
||||
status: int,
|
||||
headers: Mapping[str, str],
|
||||
cookies: Sequence[Cookie] | None = None) -> None:
|
||||
"""
|
||||
Write the response headers. If this was already called, nothing happens.
|
||||
|
||||
|
@ -587,16 +593,29 @@ class Stream:
|
|||
if self._sent_headers:
|
||||
return
|
||||
|
||||
raw_headers: list[tuple[bytes, bytes]] = []
|
||||
|
||||
for key, value in headers.items():
|
||||
if not isinstance(value, str):
|
||||
# this will be a `logging.warning` call in the future
|
||||
print(f"not string: {key} {type(value)} {value}") # type: ignore[unreachable]
|
||||
continue
|
||||
|
||||
raw_headers.append((key.encode("utf-8"), value.encode("utf-8")))
|
||||
|
||||
for cookie in (cookies or []):
|
||||
raw_headers.append((b"Set-Cookie", cookie.to_string().encode("utf-8")))
|
||||
|
||||
await self.writer({
|
||||
"type": "http.response.start",
|
||||
"status": status,
|
||||
"headers": headers
|
||||
"headers": raw_headers
|
||||
})
|
||||
|
||||
self._sent_headers = True
|
||||
|
||||
|
||||
async def write_body(self, data: bytes, eof: bool = False) -> None:
|
||||
async def write_body(self, data: bytes, eof: bool = True) -> None:
|
||||
"""
|
||||
Send body data to the client. If an eof was already sent, this does nothing.
|
||||
|
||||
|
@ -613,8 +632,7 @@ class Stream:
|
|||
await self.writer({
|
||||
"type": "http.response.body",
|
||||
"body": data,
|
||||
"more_body": eof
|
||||
"more_body": not eof
|
||||
})
|
||||
|
||||
if eof:
|
||||
self._sent_body = True
|
||||
self._sent_body = eof
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import os
|
||||
|
||||
from aputils import HttpDate, JsonBase
|
||||
from blib import convert_to_bytes
|
||||
from blib import HttpError, HttpStatus, convert_to_bytes
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime, timedelta
|
||||
from mimetypes import guess_type
|
||||
|
@ -13,8 +13,6 @@ from pathlib import Path
|
|||
from typing import Any, Literal, Self
|
||||
from urllib.parse import quote
|
||||
|
||||
from .enums import HttpStatus
|
||||
from .error import HttpError
|
||||
from .misc import Cookie, Stream
|
||||
from .request import Request
|
||||
|
||||
|
@ -37,7 +35,7 @@ class Response:
|
|||
:param headers: Header items to include in the message
|
||||
"""
|
||||
|
||||
self.cookies: dict[str, Cookie] = {}
|
||||
self.cookies: list[Cookie] = []
|
||||
"New cookies to be sent to the client"
|
||||
|
||||
self.status: HttpStatus = HttpStatus.parse(status)
|
||||
|
@ -158,6 +156,14 @@ class Response:
|
|||
self.headers.update({"Content-Type": value})
|
||||
|
||||
|
||||
def get_cookie(self, key: str) -> Cookie:
|
||||
for cookie in self.cookies:
|
||||
if cookie.key == key:
|
||||
return cookie
|
||||
|
||||
raise KeyError(key)
|
||||
|
||||
|
||||
def set_cookie(self,
|
||||
key: str,
|
||||
value: str | None,
|
||||
|
@ -170,15 +176,19 @@ class Response:
|
|||
same_site: Literal["lax", "strict", "none"] | None = "lax") -> Cookie:
|
||||
|
||||
cookie = Cookie(key, value, max_age, expires, path, domain, secure, http_only, same_site)
|
||||
self.cookies[cookie.key] = cookie
|
||||
self.cookies.append(cookie)
|
||||
return cookie
|
||||
|
||||
|
||||
def delete_cookie(self, cookie: Cookie) -> Cookie:
|
||||
deleted_cookie = cookie.copy()
|
||||
deleted_cookie.set_deleted()
|
||||
self.cookies[deleted_cookie.key] = deleted_cookie
|
||||
return deleted_cookie
|
||||
def delete_cookie(self, key: str) -> Cookie:
|
||||
try:
|
||||
cookie = self.get_cookie(key)
|
||||
|
||||
except KeyError:
|
||||
cookie = self.set_cookie(key, "")
|
||||
|
||||
cookie.set_deleted()
|
||||
return cookie
|
||||
|
||||
|
||||
async def send(self, stream: Stream, request: Request) -> None:
|
||||
|
@ -189,21 +199,8 @@ class Response:
|
|||
:param request: Request associated with the response
|
||||
"""
|
||||
|
||||
headers = []
|
||||
|
||||
for key, value in self.headers.items():
|
||||
if not isinstance(value, str):
|
||||
# this will be a `logging.warning` call in the future
|
||||
print(f"not string: {key} {type(value)} {value}") # type: ignore[unreachable]
|
||||
continue
|
||||
|
||||
headers.append((key.encode("utf-8"), value.encode("utf-8")))
|
||||
|
||||
for cookie in self.cookies.values():
|
||||
headers.append((b"Set-Cookie", cookie.to_string().encode("utf-8")))
|
||||
|
||||
await stream.write_headers(self.status, headers)
|
||||
await stream.write_body(self.body)
|
||||
await stream.write_headers(self.status, self.headers, self.cookies)
|
||||
await stream.write_body(self.body, True)
|
||||
|
||||
|
||||
class FileResponse(Response):
|
||||
|
@ -212,7 +209,7 @@ class FileResponse(Response):
|
|||
def __init__(self,
|
||||
path: Path | str,
|
||||
mimetype: str | None = None,
|
||||
chunk_size: int = 8192,
|
||||
chunk_size: int = 0,
|
||||
status: HttpStatus | int = HttpStatus.Ok,
|
||||
headers: dict[str, str] | None = None) -> None:
|
||||
"""
|
||||
|
@ -237,7 +234,7 @@ class FileResponse(Response):
|
|||
if isinstance(path, str):
|
||||
path = Path(path)
|
||||
|
||||
self.chunk_size: int = chunk_size if chunk_size > 0 else 8192
|
||||
self.chunk_size: int = chunk_size if chunk_size > 0 else 131_072
|
||||
self.path: Path = path.expanduser().resolve()
|
||||
|
||||
if not self.path.exists():
|
||||
|
@ -246,18 +243,14 @@ class FileResponse(Response):
|
|||
if not self.path.is_file():
|
||||
raise ValueError(f"Path is a directory: {self.path}")
|
||||
|
||||
self.length = self.path.stat().st_size
|
||||
|
||||
|
||||
async def send(self, stream: Stream, request: Request) -> None:
|
||||
await stream.write_headers(200, tuple([]))
|
||||
await stream.write_headers(200, self.headers, self.cookies)
|
||||
|
||||
with self.path.open("rb") as fd:
|
||||
while True:
|
||||
if not (data := fd.read(self.chunk_size)):
|
||||
break
|
||||
|
||||
await stream.write_body(data, True)
|
||||
|
||||
await stream.write_body(b"", False)
|
||||
await stream.write_body(fd.read(), True)
|
||||
|
||||
|
||||
class TemplateResponse(Response):
|
||||
|
@ -312,7 +305,9 @@ class TemplateResponse(Response):
|
|||
"request": request,
|
||||
**self.context
|
||||
}
|
||||
|
||||
text = request.app.template.render(self.name, context, self.pprint)
|
||||
self.body = text.encode("utf-8")
|
||||
self.length = len(self.body)
|
||||
|
||||
await Response.send(self, stream, request)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import http_router
|
||||
|
||||
from blib import HttpError
|
||||
from collections.abc import Awaitable, Callable, Iterable
|
||||
from http_router.routes import RouteMatch
|
||||
from typing import Any, Pattern
|
||||
|
||||
from .error import HttpError
|
||||
from .request import Request
|
||||
from .response import Response
|
||||
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
Enums
|
||||
=====
|
||||
|
||||
.. autoclass:: basgi.HttpStatus
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:undoc-members:
|
||||
:exclude-members: __new__
|
||||
|
||||
.. autoclass:: basgi.PridePalette
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
Exceptions
|
||||
==========
|
||||
|
||||
.. autoclass:: basgi.HttpError
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -41,12 +41,6 @@ Templates
|
|||
Enums
|
||||
-----
|
||||
|
||||
:class:`basgi.HttpStatus`
|
||||
:class:`basgi.PridePalette`
|
||||
|
||||
:class:`basgi.SassOutputStyle`
|
||||
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
:class:`basgi.HttpError`
|
||||
|
|
Loading…
Reference in a new issue