upload
This commit is contained in:
43
app/__init__.py
Normal file
43
app/__init__.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
from typing import Any, Literal, overload
|
||||
|
||||
from app.components.base import Component
|
||||
from app.components.http import HTTPComponent as HTTPComponent
|
||||
from app.components.lifespan import LifespanComponent as LifespanComponent
|
||||
from app.types_ import AnyScope, PassthroughDecorator, Receive, Send
|
||||
|
||||
|
||||
class App:
|
||||
components: list[Component[Any, Any]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.components = []
|
||||
|
||||
async def __call__(self, scope: AnyScope, receive: Receive[Any], send: Send) -> Any:
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
for compo in self.components:
|
||||
if await compo.condition(scope):
|
||||
_ = tg.create_task(compo.handle(scope, receive, send))
|
||||
|
||||
@overload
|
||||
def use_component[T: Component[Any, Any]](self, component: T) -> T: ...
|
||||
|
||||
@overload
|
||||
def use_component(
|
||||
self, component: Literal[None] = ..., *args: Any, **kwds: Any
|
||||
) -> PassthroughDecorator[type[Component[Any, Any]]]: ...
|
||||
|
||||
def use_component(
|
||||
self, component: Component[Any, Any] | None = None, *args: Any, **kwds: Any
|
||||
) -> PassthroughDecorator[type[Component[Any, Any]]] | Component[Any, Any]:
|
||||
if component is None:
|
||||
|
||||
def _use_component(
|
||||
component: type[Component[Any, Any]], /
|
||||
) -> type[Component[Any, Any]]:
|
||||
self.components.append(component(*args, **kwds))
|
||||
return component
|
||||
|
||||
return _use_component
|
||||
self.components.append(component)
|
||||
return component
|
||||
35
app/components/base.py
Normal file
35
app/components/base.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections.abc import MutableMapping
|
||||
from typing import Any, TypeGuard
|
||||
|
||||
from app.types_ import AnyScope, AsyncCallable, Receive, Send
|
||||
|
||||
|
||||
class Component[S: AnyScope, R: Any](metaclass=ABCMeta):
|
||||
def __init__(self, *args: Any, **kwds: Any) -> None:
|
||||
pass
|
||||
|
||||
async def condition(self, scope: AnyScope) -> TypeGuard[S]: # pyright: ignore[reportUnusedParameter]
|
||||
"""Determine whether the component should run."""
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
async def handle(self, scope: S, receive: Receive[R], send: Send) -> None:
|
||||
"""Component processor."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class RouteComponent[S: AnyScope, Recv_T: Any, Route_T: MutableMapping[str, Any], Route_R: Any](Component[S, Recv_T], metaclass=ABCMeta):
|
||||
routes: Route_T
|
||||
|
||||
def __init__(self, *args: Any, **kwds: Any) -> None:
|
||||
super().__init__(*args, **kwds)
|
||||
|
||||
@abstractmethod
|
||||
async def route_dispatch(self, scope: S, receive: Receive[Recv_T], send: Send) -> Any:
|
||||
"""Route dispatcher"""
|
||||
raise NotImplementedError
|
||||
|
||||
def route_install(self, type_: str, route: str, target: AsyncCallable[..., Route_R]) -> None:
|
||||
"""Install route target for specific type and route."""
|
||||
self.routes.setdefault(type_, {})[route] = target
|
||||
99
app/components/http.py
Normal file
99
app/components/http.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from typing import TypeGuard, override
|
||||
|
||||
from app.subroutines.http import Response, SimpleRequest, SimpleResponse
|
||||
from app.types_ import (
|
||||
AnyScope,
|
||||
AsyncCallable,
|
||||
HTTPScope,
|
||||
PassthroughDecorator,
|
||||
Receive,
|
||||
ReceiveHTTP,
|
||||
RouteMapping,
|
||||
Send,
|
||||
)
|
||||
|
||||
from .base import RouteComponent as _RouteComponent
|
||||
|
||||
|
||||
class HTTPComponent(
|
||||
_RouteComponent[HTTPScope, ReceiveHTTP, dict[str, RouteMapping[Response]], Response]
|
||||
):
|
||||
routes: dict[str, RouteMapping[Response]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.routes = {}
|
||||
super().__init__()
|
||||
|
||||
@override
|
||||
async def condition(self, scope: AnyScope) -> TypeGuard[HTTPScope]:
|
||||
return scope["type"] == "http"
|
||||
|
||||
@override
|
||||
async def handle(
|
||||
self, scope: HTTPScope, receive: Receive[ReceiveHTTP], send: Send
|
||||
) -> None:
|
||||
req = SimpleRequest()
|
||||
while not req.receive(await receive()):
|
||||
pass
|
||||
|
||||
print(scope, req.body)
|
||||
resp = await self.route_dispatch(scope, receive, send)
|
||||
if resp is None:
|
||||
resp = Response(status=404, body=b"404 Not Found\n")
|
||||
|
||||
async with SimpleResponse(send).prepare(
|
||||
resp.status, headers=resp.headers
|
||||
) as rsp:
|
||||
await rsp.finish(resp.body if resp.body is not None else b"")
|
||||
|
||||
return None
|
||||
|
||||
@override
|
||||
async def route_dispatch(
|
||||
self, scope: HTTPScope, receive: Receive[ReceiveHTTP], send: Send
|
||||
) -> Response | None:
|
||||
for k, callee in self.routes[scope["method"].upper()].items():
|
||||
if scope["path"] == k: # temporary impl.
|
||||
return await callee()
|
||||
|
||||
def route[T: AsyncCallable[..., Response]](
|
||||
self,
|
||||
route: str,
|
||||
*,
|
||||
get: bool = False,
|
||||
post: bool = False,
|
||||
put: bool = False,
|
||||
delete: bool = False,
|
||||
) -> PassthroughDecorator[T]:
|
||||
def __wrap_route(fn: T) -> T:
|
||||
if get:
|
||||
self.route_install("GET", route, fn)
|
||||
if post:
|
||||
self.route_install("POST", route, fn)
|
||||
if put:
|
||||
self.route_install("PUT", route, fn)
|
||||
if delete:
|
||||
self.route_install("DELETE", route, fn)
|
||||
return fn
|
||||
|
||||
return __wrap_route
|
||||
|
||||
def get[T: AsyncCallable[..., Response]](
|
||||
self, route: str
|
||||
) -> PassthroughDecorator[T]:
|
||||
return self.route(route, get=True)
|
||||
|
||||
def post[T: AsyncCallable[..., Response]](
|
||||
self, route: str
|
||||
) -> PassthroughDecorator[T]:
|
||||
return self.route(route, post=True)
|
||||
|
||||
def put[T: AsyncCallable[..., Response]](
|
||||
self, route: str
|
||||
) -> PassthroughDecorator[T]:
|
||||
return self.route(route, put=True)
|
||||
|
||||
def delete[T: AsyncCallable[..., Response]](
|
||||
self, route: str
|
||||
) -> PassthroughDecorator[T]:
|
||||
return self.route(route, delete=True)
|
||||
25
app/components/lifespan.py
Normal file
25
app/components/lifespan.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from typing import TypeGuard, override
|
||||
|
||||
from app.types_ import AnyScope, LifespanScope, Receive, ReceiveLifespan, Send
|
||||
|
||||
from .base import Component as _Component
|
||||
|
||||
|
||||
class LifespanComponent(_Component[LifespanScope, ReceiveLifespan]):
|
||||
@override
|
||||
async def condition(self, scope: AnyScope) -> TypeGuard[LifespanScope]:
|
||||
return scope["type"] == "lifespan"
|
||||
|
||||
@override
|
||||
async def handle(self, scope: LifespanScope, receive: Receive[ReceiveLifespan], send: Send) -> None:
|
||||
while True:
|
||||
message = await receive()
|
||||
if message['type'] == 'lifespan.startup':
|
||||
... # Do some startup here!
|
||||
print("Startup...")
|
||||
await send({'type': 'lifespan.startup.complete'})
|
||||
elif message['type'] == 'lifespan.shutdown':
|
||||
... # Do some shutdown here!
|
||||
print("Shutdown...")
|
||||
await send({'type': 'lifespan.shutdown.complete'})
|
||||
return
|
||||
10
app/exceptions.py
Normal file
10
app/exceptions.py
Normal file
@@ -0,0 +1,10 @@
|
||||
class AppError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionClosed(AppError):
|
||||
pass
|
||||
|
||||
|
||||
class ParseError(AppError):
|
||||
pass
|
||||
152
app/subroutines/_c_route.c
Normal file
152
app/subroutines/_c_route.c
Normal file
@@ -0,0 +1,152 @@
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SEP 0x01
|
||||
#define FORMAT_START 0x03
|
||||
#define FORMAT_END 0x00
|
||||
#define DOUBLE_STAR 0x09
|
||||
|
||||
void free_parsed_route(char *parsed) {
|
||||
free(parsed);
|
||||
}
|
||||
|
||||
// Helper function to write an Exact component to the output buffer
|
||||
static void write_exact(const char *value, size_t value_len, char **output, size_t *output_len) {
|
||||
memcpy(*output, value, value_len);
|
||||
*output += value_len;
|
||||
**output = SEP;
|
||||
(*output)++;
|
||||
*output_len += value_len + 1;
|
||||
}
|
||||
|
||||
// Helper function to write a Format component to the output buffer
|
||||
static void write_format(const char *name, size_t name_len, size_t minl, size_t maxl, int ignore_sep, char **output, size_t *output_len) {
|
||||
// Write FORMAT_START marker
|
||||
**output = FORMAT_START;
|
||||
(*output)++;
|
||||
*output_len += 1;
|
||||
|
||||
// Write the name
|
||||
memcpy(*output, name, name_len);
|
||||
*output += name_len;
|
||||
**output = FORMAT_END;
|
||||
(*output)++;
|
||||
*output_len += name_len + 1;
|
||||
|
||||
// Write minl and maxl as size_t values
|
||||
memcpy(*output, &minl, sizeof(size_t));
|
||||
*output += sizeof(size_t);
|
||||
memcpy(*output, &maxl, sizeof(size_t));
|
||||
*output += sizeof(size_t);
|
||||
*output_len += 2 * sizeof(size_t);
|
||||
|
||||
// Write ignore_sep as a single byte
|
||||
**output = (char)ignore_sep;
|
||||
(*output)++;
|
||||
**output = SEP;
|
||||
(*output)++;
|
||||
*output_len += 2;
|
||||
}
|
||||
|
||||
// Main parsing function
|
||||
char *parse_route(const char *route, size_t route_len, size_t *output_len) {
|
||||
char *output = malloc(route_len * 8); // Allocate enough space for the output
|
||||
if (!output) return NULL;
|
||||
char *output_start = output;
|
||||
*output_len = 0;
|
||||
|
||||
const char *p = route;
|
||||
const char *end = route + route_len;
|
||||
|
||||
while (p < end) {
|
||||
if (*p == '/') {
|
||||
// Skip leading slashes
|
||||
write_exact("/", 1, &output, output_len);
|
||||
p++;
|
||||
continue;
|
||||
} else if (*p == '{') {
|
||||
// Handle Format component
|
||||
const char *start = p + 1; // Skip '{'
|
||||
const char *end_brace = memchr(start, '}', end - start);
|
||||
if (!end_brace) break; // Invalid format, stop parsing
|
||||
|
||||
// Extract name and constraints
|
||||
const char *colon = memchr(start, ':', end_brace - start);
|
||||
size_t name_len = colon ? (size_t)(colon - start) : (size_t)(end_brace - start);
|
||||
const char *name = start;
|
||||
size_t minl = 0, maxl = (size_t)-1, ignore_sep = 0;
|
||||
|
||||
if (colon) {
|
||||
const char *constraint = colon + 1;
|
||||
if (memchr(constraint, '-', end_brace - constraint)) {
|
||||
// Parse "minl-maxl"
|
||||
sscanf(constraint, "%zu-%zu", &minl, &maxl);
|
||||
} else {
|
||||
// Parse "minl" (fixed length)
|
||||
sscanf(constraint, "%zu", &minl);
|
||||
maxl = minl;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the Format component
|
||||
write_format(name, name_len, minl, maxl, ignore_sep, &output, output_len);
|
||||
|
||||
p = end_brace + 1; // Move past '}'
|
||||
} else if (*p == '*' && *(p + 1) == '*') {
|
||||
// Handle "**" as a special marker (\x09)
|
||||
*output = DOUBLE_STAR;
|
||||
output++;
|
||||
*output = SEP;
|
||||
output++;
|
||||
*output_len += 2;
|
||||
p += 2; // Move past "**"
|
||||
} else {
|
||||
// Handle Exact component
|
||||
const char *next_slash = memchr(p, '/', end - p);
|
||||
size_t segment_len = next_slash ? (size_t)(next_slash - p) : (size_t)(end - p);
|
||||
write_exact(p, segment_len, &output, output_len);
|
||||
p += segment_len;
|
||||
}
|
||||
}
|
||||
|
||||
// Null-terminate the output
|
||||
*output = '\0';
|
||||
*output_len += 1;
|
||||
|
||||
return output_start;
|
||||
}
|
||||
|
||||
// // Example usage
|
||||
// int main() {
|
||||
// const char *route = "/foo/{n:5}/bar/**/baz";
|
||||
// size_t route_len = strlen(route);
|
||||
// size_t output_len;
|
||||
|
||||
// char *result = parse_route(route, route_len, &output_len);
|
||||
// if (!result) {
|
||||
// fprintf(stderr, "Memory allocation failed\n");
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// printf("Parsed result (%zu bytes):\n", output_len);
|
||||
// for (size_t i = 0; i < output_len; i++) {
|
||||
// if (result[i] == SEP) {
|
||||
// printf("\\x01");
|
||||
// } else if (result[i] == FORMAT_START) {
|
||||
// printf("\\x03");
|
||||
// } else if (result[i] == FORMAT_END) {
|
||||
// printf("\\x00");
|
||||
// } else if (result[i] == DOUBLE_STAR) {
|
||||
// printf("\\x09");
|
||||
// } else {
|
||||
// printf("%c", result[i]);
|
||||
// }
|
||||
// }
|
||||
// printf("\n");
|
||||
|
||||
// free(result);
|
||||
// return 0;
|
||||
// }
|
||||
138
app/subroutines/http.py
Normal file
138
app/subroutines/http.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from collections.abc import MutableMapping
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
from http.cookies import BaseCookie
|
||||
from types import TracebackType
|
||||
from typing import Any, Self
|
||||
|
||||
from app.exceptions import ConnectionClosed
|
||||
from app.types_ import CommonMapping, ReceiveHTTP, Send
|
||||
|
||||
|
||||
class SimpleRequest:
|
||||
body: bytes
|
||||
body_parts: list[bytes]
|
||||
body_complete: bool
|
||||
done: bool
|
||||
keep_unset_body: bool = False
|
||||
|
||||
def __init__(self, keep_unset_body: bool | None = None) -> None:
|
||||
self.body = b""
|
||||
self.body_parts = []
|
||||
self.body_complete = False
|
||||
self.done = False
|
||||
if keep_unset_body is not None:
|
||||
self.keep_unset_body = keep_unset_body
|
||||
|
||||
def receive(self, received: ReceiveHTTP) -> bool:
|
||||
if received["type"] == "http.disconnect":
|
||||
self.done = True
|
||||
return True
|
||||
|
||||
if "body" in received:
|
||||
self.body_parts.append(received["body"])
|
||||
elif self.keep_unset_body:
|
||||
self.body_parts.append(b"")
|
||||
|
||||
if not ("more_body" in received and received["more_body"]):
|
||||
self.body = b"".join(self.body_parts)
|
||||
self.body_complete = True
|
||||
|
||||
return self.body_complete or self.done
|
||||
|
||||
|
||||
class SimpleResponse:
|
||||
send: Send
|
||||
headers: list[tuple[bytes, bytes]]
|
||||
status: int
|
||||
trailers: bool
|
||||
done: bool
|
||||
|
||||
def __init__(self, send: Send) -> None:
|
||||
self.send = send
|
||||
self.headers = []
|
||||
self.status = 200
|
||||
self.trailers = False
|
||||
self.done = False
|
||||
|
||||
def add_header(self, name: str, value: object) -> None:
|
||||
self.headers.append((name.lower().encode(), str(value).encode()))
|
||||
|
||||
def prepare(self, status: int = 200, trailers: bool = False, headers: MutableMapping[str, str] | None = None) -> Self:
|
||||
self.status = status
|
||||
self.trailers = trailers
|
||||
if headers:
|
||||
for k, v in headers.items():
|
||||
self.add_header(k, v)
|
||||
return self
|
||||
|
||||
async def start(self) -> None:
|
||||
if self.done:
|
||||
raise ConnectionClosed
|
||||
await self.send({
|
||||
"type": "http.response.start",
|
||||
"status": self.status,
|
||||
"headers": self.headers,
|
||||
"trailers": self.trailers
|
||||
})
|
||||
|
||||
async def body(self, data: bytes = b"", *, done: bool = True) -> None:
|
||||
if self.done:
|
||||
raise ConnectionClosed
|
||||
await self.send({
|
||||
"type": "http.response.body",
|
||||
"body": data,
|
||||
"more_body": not done
|
||||
})
|
||||
self.done = done and not self.trailers
|
||||
|
||||
async def part(self, data: bytes = b"") -> None:
|
||||
"""Same as `Response.body(data, done=False)`"""
|
||||
return await self.body(data, done=False)
|
||||
|
||||
async def finish(self, data: bytes = b"") -> None:
|
||||
"""Same as `Response.body(data, done=True)`"""
|
||||
return await self.body(data, done=True)
|
||||
|
||||
async def trail(self) -> None:
|
||||
if self.done:
|
||||
raise ConnectionClosed
|
||||
await self.send({"type": "http.response.trailers"})
|
||||
self.done = True
|
||||
|
||||
async def __aenter__(self) -> Self:
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, /) -> bool | None:
|
||||
if not (self.done or exc_type):
|
||||
await self.finish()
|
||||
if exc_type and self.trailers:
|
||||
await self.trail()
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Response:
|
||||
status: int = 200
|
||||
body: bytes | None = None
|
||||
headers: CommonMapping = field(default_factory=dict[str, Any])
|
||||
content_type: InitVar[str | None] = None
|
||||
cookies: InitVar[BaseCookie[bytes] | None] = None
|
||||
|
||||
def __post_init__(self, content_type: str | None, cookies: BaseCookie[bytes] | None) -> None:
|
||||
if self.body is not None:
|
||||
self.headers["content-length"] = len(self.body)
|
||||
if content_type is not None:
|
||||
self.headers["content-type"] = content_type
|
||||
if cookies is not None:
|
||||
self.headers["set-cookie"] = cookies.output(header="").strip()
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTMLResponse(Response):
|
||||
content: InitVar[str] = ""
|
||||
encoding: InitVar[str] = "utf-8"
|
||||
|
||||
def __post_init__(self, content_type: str | None, cookies: BaseCookie[bytes] | None, content: str, encoding: str) -> None:
|
||||
self.body: bytes | None = content.encode(encoding)
|
||||
return super().__post_init__("text/html", cookies)
|
||||
48
app/subroutines/route.py
Normal file
48
app/subroutines/route.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# / -> Exact("/")
|
||||
# /foo -> Exact("/foo")
|
||||
# /foo/ -> Exact("/foo/")
|
||||
# /foo/bar -> Exact("/foo/bar")
|
||||
# /foo/bar/{n} -> Exact("/foo/bar/"), Format("n")
|
||||
# /foo/{n}/bar -> Exact("/foo/"), Format("n"), Exact("/bar")
|
||||
# /foo{n:5}/bar -> Exact("/foo"), Format("n", minl=5, maxl=5), Exact("/bar")
|
||||
# /foo/{m:1-}/{n:3-5}bar -> Exact("/foo/"), Format("m", minl=1, maxl=-1), Exact("/"), Format("n", minl=3, maxl=5), Exact("bar")
|
||||
# /* -> Exact("/"), Format("_")
|
||||
# /foo/*bar -> Exact("/foo/"), Format("_"), Exact("bar")
|
||||
# /foo/*/bar -> Exact("/foo/"), Format("_"), Exact("/bar")
|
||||
# /foo/**/bar -> Exact("/foo/"), Format("_", ignore_sep=True), Exact("/bar")
|
||||
|
||||
# ["foo", Format("m", minl=1, maxl=-1), [Format("n", minl=3, maxl=5), "bar"], "*"]
|
||||
|
||||
|
||||
import ctypes
|
||||
|
||||
dll = ctypes.CDLL("./c_route.so")
|
||||
dll.parse_route.restype = type("c_char_alloc", (ctypes.c_char_p,), {})
|
||||
dll.free_parsed_route.argtypes = (ctypes.c_void_p,)
|
||||
dll.free_parsed_route.restype = None
|
||||
|
||||
|
||||
def compile_route(pattern: str) -> list[bytes]:
|
||||
if pattern[0] != "/":
|
||||
from app.exceptions import ParseError
|
||||
raise ParseError("The first character in route pattern must be '/'.")
|
||||
|
||||
# res: list[str] = []
|
||||
# for path in pattern[1:].split("/"):
|
||||
# if (star_idx := path.find("*")) != -1:
|
||||
# path = "".join((path[:star_idx], "{_}", path[star_idx + 1:]))
|
||||
# if "{" not in path and "}" not in path and "*" not in path:
|
||||
# res.append(path)
|
||||
|
||||
outlen = ctypes.c_size_t()
|
||||
data = pattern[1:].encode()
|
||||
ret = dll.parse_route(data, len(data), ctypes.byref(outlen))
|
||||
res = ctypes.string_at(ret, outlen.value - 2).split(b"\x01")
|
||||
dll.free_parsed_route(ret)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(compile_route("/foo/{n:5}foo/bar/**/baz"))
|
||||
print(compile_route("/"))
|
||||
69
app/types_.py
Normal file
69
app/types_.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from collections.abc import (
|
||||
Awaitable,
|
||||
Coroutine,
|
||||
Mapping,
|
||||
MutableMapping,
|
||||
MutableSequence,
|
||||
)
|
||||
from typing import Any, Callable, Literal, NotRequired, TypedDict
|
||||
|
||||
type CommonMapping = MutableMapping[str, Any]
|
||||
|
||||
type PassthroughDecorator[F: Callable[..., Any]] = Callable[[F], F]
|
||||
type WrappingDecorator[**P, R] = Callable[[Callable[P, R]], Callable[P, R]]
|
||||
|
||||
type HostPortTuple = tuple[str, int]
|
||||
type UnixSocketTuple = tuple[str, Literal[None]]
|
||||
type Receive[R: Mapping[str, Any]] = Callable[[], Coroutine[Any, Any, R]]
|
||||
type Send = Callable[[CommonMapping], Coroutine[Any, Any, None]]
|
||||
|
||||
type AsyncCallable[**P, R] = Callable[P, Awaitable[R]]
|
||||
type AnyAsyncCallable = AsyncCallable[..., Any]
|
||||
type RouteMapping[R] = MutableMapping[str, AsyncCallable[..., R]]
|
||||
|
||||
|
||||
class ASGIInfo(TypedDict):
|
||||
version: str
|
||||
spec_version: str
|
||||
|
||||
|
||||
class LifespanScope(TypedDict):
|
||||
type: Literal["lifespan"]
|
||||
asgi: ASGIInfo
|
||||
state: CommonMapping
|
||||
|
||||
|
||||
class HTTPScope(TypedDict):
|
||||
type: Literal["http"]
|
||||
asgi: ASGIInfo
|
||||
http_version: str
|
||||
server: HostPortTuple | UnixSocketTuple
|
||||
client: HostPortTuple
|
||||
scheme: str
|
||||
method: Literal["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
||||
root_path: str
|
||||
path: str
|
||||
raw_path: bytes
|
||||
query_string: bytes
|
||||
headers: MutableSequence[tuple[str, str]]
|
||||
state: CommonMapping
|
||||
|
||||
|
||||
type AnyScope = LifespanScope | HTTPScope
|
||||
|
||||
|
||||
class ReceiveLifespan(TypedDict):
|
||||
type: Literal["lifespan.startup", "lifespan.shutdown"]
|
||||
|
||||
|
||||
class ReceiveHTTPRequest(TypedDict):
|
||||
type: Literal["http.request"]
|
||||
body: NotRequired[bytes]
|
||||
more_body: NotRequired[bool]
|
||||
|
||||
|
||||
class ReceiveHTTPDisconnect(TypedDict):
|
||||
type: Literal["http.disconnect"]
|
||||
|
||||
|
||||
type ReceiveHTTP = ReceiveHTTPRequest | ReceiveHTTPDisconnect
|
||||
Reference in New Issue
Block a user