upload
This commit is contained in:
@@ -6,6 +6,7 @@ from app.types_ import AnyScope, AsyncCallable, Receive, Send
|
|||||||
|
|
||||||
|
|
||||||
class Component[S: AnyScope, R: Any](metaclass=ABCMeta):
|
class Component[S: AnyScope, R: Any](metaclass=ABCMeta):
|
||||||
|
@abstractmethod
|
||||||
def __init__(self, *args: Any, **kwds: Any) -> None:
|
def __init__(self, *args: Any, **kwds: Any) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class Component[S: AnyScope, R: Any](metaclass=ABCMeta):
|
|||||||
class RouteComponent[S: AnyScope, Recv_T: Any, Route_T: MutableMapping[str, Any], Route_R: Any](Component[S, Recv_T], metaclass=ABCMeta):
|
class RouteComponent[S: AnyScope, Recv_T: Any, Route_T: MutableMapping[str, Any], Route_R: Any](Component[S, Recv_T], metaclass=ABCMeta):
|
||||||
routes: Route_T
|
routes: Route_T
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
def __init__(self, *args: Any, **kwds: Any) -> None:
|
def __init__(self, *args: Any, **kwds: Any) -> None:
|
||||||
super().__init__(*args, **kwds)
|
super().__init__(*args, **kwds)
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ class RouteComponent[S: AnyScope, Recv_T: Any, Route_T: MutableMapping[str, Any]
|
|||||||
"""Route dispatcher"""
|
"""Route dispatcher"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def route_install(self, type_: str, route: str, target: AsyncCallable[..., Route_R]) -> None:
|
@abstractmethod
|
||||||
|
def route_install(self, route: str, target: AsyncCallable[..., Route_R], *, type_: str | None = None) -> None:
|
||||||
"""Install route target for specific type and route."""
|
"""Install route target for specific type and route."""
|
||||||
self.routes.setdefault(type_, {})[route] = target
|
raise NotImplementedError
|
||||||
@@ -56,6 +56,13 @@ class HTTPComponent(
|
|||||||
if scope["path"] == k: # temporary impl.
|
if scope["path"] == k: # temporary impl.
|
||||||
return await callee()
|
return await callee()
|
||||||
|
|
||||||
|
@override
|
||||||
|
def route_install(self, route: str, target: AsyncCallable[..., Response], *, type_: str | None = None) -> None:
|
||||||
|
"""Install route target for specific type and route."""
|
||||||
|
if type_ is None:
|
||||||
|
raise ValueError("Route type `type_` is unset.")
|
||||||
|
self.routes.setdefault(type_, {})[route] = target
|
||||||
|
|
||||||
def route[T: AsyncCallable[..., Response]](
|
def route[T: AsyncCallable[..., Response]](
|
||||||
self,
|
self,
|
||||||
route: str,
|
route: str,
|
||||||
@@ -67,13 +74,13 @@ class HTTPComponent(
|
|||||||
) -> PassthroughDecorator[T]:
|
) -> PassthroughDecorator[T]:
|
||||||
def __wrap_route(fn: T) -> T:
|
def __wrap_route(fn: T) -> T:
|
||||||
if get:
|
if get:
|
||||||
self.route_install("GET", route, fn)
|
self.route_install(route, fn, type_="GET")
|
||||||
if post:
|
if post:
|
||||||
self.route_install("POST", route, fn)
|
self.route_install(route, fn, type_="POST")
|
||||||
if put:
|
if put:
|
||||||
self.route_install("PUT", route, fn)
|
self.route_install(route, fn, type_="PUT")
|
||||||
if delete:
|
if delete:
|
||||||
self.route_install("DELETE", route, fn)
|
self.route_install(route, fn, type_="DELETE")
|
||||||
return fn
|
return fn
|
||||||
|
|
||||||
return __wrap_route
|
return __wrap_route
|
||||||
|
|||||||
@@ -1,25 +1,59 @@
|
|||||||
from typing import TypeGuard, override
|
from collections.abc import AsyncGenerator, Callable
|
||||||
|
from typing import Any, TypeGuard, override
|
||||||
|
|
||||||
from app.types_ import AnyScope, LifespanScope, Receive, ReceiveLifespan, Send
|
from app.subroutines.asyncutils import agzip
|
||||||
|
from app.types_ import (
|
||||||
|
AnyScope,
|
||||||
|
AsyncCallable,
|
||||||
|
LifespanScope,
|
||||||
|
Receive,
|
||||||
|
ReceiveLifespan,
|
||||||
|
Send,
|
||||||
|
)
|
||||||
|
|
||||||
from .base import Component as _Component
|
from .base import Component as _Component
|
||||||
|
|
||||||
|
|
||||||
class LifespanComponent(_Component[LifespanScope, ReceiveLifespan]):
|
class LifespanComponent(_Component[LifespanScope, ReceiveLifespan]):
|
||||||
|
startups: list[AsyncCallable[[], None]]
|
||||||
|
shutdowns: list[AsyncCallable[[], None]]
|
||||||
|
contexts: list[Callable[[], AsyncGenerator[Any, None]]]
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwds: Any) -> None:
|
||||||
|
super().__init__(*args, **kwds)
|
||||||
|
self.startups = []
|
||||||
|
self.shutdowns = []
|
||||||
|
self.contexts = []
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def condition(self, scope: AnyScope) -> TypeGuard[LifespanScope]:
|
async def condition(self, scope: AnyScope) -> TypeGuard[LifespanScope]:
|
||||||
return scope["type"] == "lifespan"
|
return scope["type"] == "lifespan"
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def handle(self, scope: LifespanScope, receive: Receive[ReceiveLifespan], send: Send) -> None:
|
async def handle(
|
||||||
while True:
|
self, scope: LifespanScope, receive: Receive[ReceiveLifespan], send: Send
|
||||||
|
) -> None:
|
||||||
|
message = await receive()
|
||||||
|
async for _ in agzip(*[ctx() for ctx in self.contexts]):
|
||||||
|
if message["type"] == "lifespan.startup":
|
||||||
|
for fn in self.startups:
|
||||||
|
await fn()
|
||||||
|
await send({"type": "lifespan.startup.complete"})
|
||||||
|
elif message["type"] == "lifespan.shutdown":
|
||||||
|
for fn in self.shutdowns:
|
||||||
|
await fn()
|
||||||
|
await send({"type": "lifespan.shutdown.complete"})
|
||||||
|
return
|
||||||
message = await receive()
|
message = await receive()
|
||||||
if message['type'] == 'lifespan.startup':
|
|
||||||
... # Do some startup here!
|
def on_startup[Call_T: AsyncCallable[[], None]](self, fn: Call_T) -> Call_T:
|
||||||
print("Startup...")
|
self.startups.append(fn)
|
||||||
await send({'type': 'lifespan.startup.complete'})
|
return fn
|
||||||
elif message['type'] == 'lifespan.shutdown':
|
|
||||||
... # Do some shutdown here!
|
def on_shutdown[Call_T: AsyncCallable[[], None]](self, fn: Call_T) -> Call_T:
|
||||||
print("Shutdown...")
|
self.shutdowns.append(fn)
|
||||||
await send({'type': 'lifespan.shutdown.complete'})
|
return fn
|
||||||
return
|
|
||||||
|
def on_context[Ctx_T: Callable[[], AsyncGenerator[Any, None]]](self, fn: Ctx_T) -> Ctx_T:
|
||||||
|
self.contexts.append(fn)
|
||||||
|
return fn
|
||||||
|
|||||||
18
app/subroutines/asyncutils.py
Normal file
18
app/subroutines/asyncutils.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import asyncio
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
async def agzip(*async_generators: AsyncGenerator[T, None]) -> AsyncGenerator[tuple[T, ...], None]:
|
||||||
|
"""
|
||||||
|
`zip()`-like function for `AsyncGenerator`s.
|
||||||
|
"""
|
||||||
|
iterators = [ag.__aiter__() for ag in async_generators]
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
results = await asyncio.gather(*[iterator.__anext__() for iterator in iterators])
|
||||||
|
yield tuple(results)
|
||||||
|
except StopAsyncIteration:
|
||||||
|
break
|
||||||
27
test.py
27
test.py
@@ -1,15 +1,36 @@
|
|||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from app import App
|
from app import App
|
||||||
from app.components.http import HTTPComponent
|
from app.components.http import HTTPComponent
|
||||||
|
from app.components.lifespan import LifespanComponent
|
||||||
# from app.components.lifespan import LifespanComponent
|
|
||||||
from app.subroutines.http import HTMLResponse
|
from app.subroutines.http import HTMLResponse
|
||||||
|
|
||||||
app = App()
|
app = App()
|
||||||
|
|
||||||
# lifespan = app.use_component(LifespanComponent())
|
lifespan = app.use_component(LifespanComponent())
|
||||||
http = app.use_component(HTTPComponent())
|
http = app.use_component(HTTPComponent())
|
||||||
|
|
||||||
|
|
||||||
|
@lifespan.on_context
|
||||||
|
async def my_context() -> AsyncGenerator[Any, None]:
|
||||||
|
try:
|
||||||
|
print("Start!")
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
print("Stop!")
|
||||||
|
|
||||||
|
|
||||||
|
# @lifespan.on_startup
|
||||||
|
# async def start() -> None:
|
||||||
|
# print("Start!")
|
||||||
|
|
||||||
|
|
||||||
|
# @lifespan.on_shutdown
|
||||||
|
# async def stop() -> None:
|
||||||
|
# print("Stop!")
|
||||||
|
|
||||||
|
|
||||||
@http.route("/teapot", get=True, post=True, put=True, delete=True)
|
@http.route("/teapot", get=True, post=True, put=True, delete=True)
|
||||||
async def teapot() -> HTMLResponse:
|
async def teapot() -> HTMLResponse:
|
||||||
resp = """
|
resp = """
|
||||||
|
|||||||
Reference in New Issue
Block a user