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
|
||||||
4
pyproject.toml
Normal file
4
pyproject.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[tool.basedpyright]
|
||||||
|
typeCheckingMode = "recommended"
|
||||||
|
reportAny = false
|
||||||
|
reportExplicitAny = false
|
||||||
27
test.py
Normal file
27
test.py
Normal 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)
|
||||||
Reference in New Issue
Block a user