This commit is contained in:
2025-04-09 16:54:34 +08:00
parent 7cf1c7741d
commit 1d9ab14b01
11 changed files with 650 additions and 0 deletions

43
app/__init__.py Normal file
View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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

4
pyproject.toml Normal file
View File

@@ -0,0 +1,4 @@
[tool.basedpyright]
typeCheckingMode = "recommended"
reportAny = false
reportExplicitAny = false

27
test.py Normal file
View File

@@ -0,0 +1,27 @@
from app import App
from app.components.http import HTTPComponent
# from app.components.lifespan import LifespanComponent
from app.subroutines.http import HTMLResponse
app = App()
# lifespan = app.use_component(LifespanComponent())
http = app.use_component(HTTPComponent())
@http.route("/teapot", get=True, post=True, put=True, delete=True)
async def teapot() -> HTMLResponse:
resp = """
<!DOCTYPE html>
<html>
<head>
<title>I'm a Teapot!!!</title>
</head>
<body>
<h1>I'm a Teapot!!!</h1>
<p>I've already told you I'm a teapot.</p>
</body>
</html>
"""
return HTMLResponse(status=418, content=resp)