Refactor to composable KayaMixin architecture
Replace wrapper-based SessionMiddleware/OIDCApp with KayaMixin subclasses applied via KayaApp(mixins=[...]). Mixins hook into handle_request and handle_websocket via before/after hooks, so both ASGI and RSGI keep working. Mixin dependencies are applied automatically and deduplicated.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from ._app import AbstractKayaApp, KayaApp
|
||||
from ._http_method import HttpMethod
|
||||
from ._http_context import HttpContext
|
||||
from ._mixin import KayaMixin
|
||||
from ._tree import Tree, PathIterator
|
||||
from ._path_handler import PathHandler, Matches
|
||||
from ._websocket import WebSocket, WebSocketMessage
|
||||
@@ -10,6 +11,7 @@ __all__ = [
|
||||
'AbstractKayaApp',
|
||||
'HttpMethod',
|
||||
'KayaApp',
|
||||
'KayaMixin',
|
||||
'HttpContext',
|
||||
'Tree',
|
||||
'PathHandler',
|
||||
|
||||
@@ -6,9 +6,10 @@ from typing import Callable, Awaitable, Any, Mapping, Sequence, Optional, Tuple,
|
||||
from pwo import Maybe, AsyncQueueIterator
|
||||
from ._http_context import HttpContext
|
||||
from ._http_method import HttpMethod
|
||||
from ._mixin import KayaMixin
|
||||
from ._path_handler import Context
|
||||
from ._types import StrOrStrings
|
||||
from ._websocket import WebSocket
|
||||
from ._websocket import WebSocket, WebSocketMessage
|
||||
from ._asgi import AsgiContext, AsgiWebSocket
|
||||
from ._tree import Tree
|
||||
from ._types.asgi import LifespanScope, HTTPScope as ASGIHTTPScope, WebSocketScope as ASGIWebSocketScope
|
||||
@@ -18,6 +19,10 @@ log = getLogger(__name__)
|
||||
|
||||
type HttpHandler = Callable[[HttpContext, Unpack[Any]], Awaitable[None]]
|
||||
type WebSocketHandler = Callable[[WebSocket, Unpack[Any]], Awaitable[None]]
|
||||
type BeforeRequestHook = Callable[[HttpContext], Awaitable[Optional[HttpContext]]]
|
||||
type AfterRequestHook = Callable[[HttpContext], Awaitable[None]]
|
||||
type BeforeWebSocketHook = Callable[[WebSocket], Awaitable[Optional[WebSocket]]]
|
||||
type AfterWebSocketHook = Callable[[WebSocket], Awaitable[None]]
|
||||
|
||||
|
||||
class AbstractKayaApp(ABC):
|
||||
@@ -89,25 +94,84 @@ class AbstractKayaApp(ABC):
|
||||
|
||||
class KayaApp(AbstractKayaApp):
|
||||
_tree: Tree
|
||||
_mixins: list[KayaMixin]
|
||||
_applied_ids: set[int]
|
||||
_before_request_hooks: list[BeforeRequestHook]
|
||||
_after_request_hooks: list[AfterRequestHook]
|
||||
_before_websocket_hooks: list[BeforeWebSocketHook]
|
||||
_after_websocket_hooks: list[AfterWebSocketHook]
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, mixins: Sequence[KayaMixin] = ()) -> None:
|
||||
self._tree = Tree()
|
||||
self._mixins = []
|
||||
self._applied_ids = set()
|
||||
self._before_request_hooks = []
|
||||
self._after_request_hooks = []
|
||||
self._before_websocket_hooks = []
|
||||
self._after_websocket_hooks = []
|
||||
for mixin in mixins:
|
||||
self._apply_mixin(mixin)
|
||||
|
||||
def _apply_mixin(self, mixin: KayaMixin) -> None:
|
||||
if id(mixin) in self._applied_ids:
|
||||
return
|
||||
for dependency in mixin.dependencies:
|
||||
self._apply_mixin(dependency)
|
||||
mixin.apply(self)
|
||||
self._applied_ids.add(id(mixin))
|
||||
self._mixins.append(mixin)
|
||||
|
||||
def add_before_request_hook(self, hook: BeforeRequestHook) -> None:
|
||||
self._before_request_hooks.append(hook)
|
||||
|
||||
def add_after_request_hook(self, hook: AfterRequestHook) -> None:
|
||||
self._after_request_hooks.append(hook)
|
||||
|
||||
def add_before_websocket_hook(self, hook: BeforeWebSocketHook) -> None:
|
||||
self._before_websocket_hooks.append(hook)
|
||||
|
||||
def add_after_websocket_hook(self, hook: AfterWebSocketHook) -> None:
|
||||
self._after_websocket_hooks.append(hook)
|
||||
|
||||
async def handle_request(self, ctx: HttpContext) -> None:
|
||||
result = self._tree.get_handler(ctx.path, ctx.method)
|
||||
if result is not None:
|
||||
handler, captured = result
|
||||
await handler.handle_request(ctx, captured)
|
||||
else:
|
||||
await ctx.send_empty(404)
|
||||
for hook in self._before_request_hooks:
|
||||
new_ctx = await hook(ctx)
|
||||
if new_ctx is not None:
|
||||
ctx = new_ctx
|
||||
try:
|
||||
result = self._tree.get_handler(ctx.path, ctx.method)
|
||||
if result is not None:
|
||||
handler, captured = result
|
||||
await handler.handle_request(ctx, captured)
|
||||
else:
|
||||
await ctx.send_empty(404)
|
||||
finally:
|
||||
for hook in reversed(self._after_request_hooks):
|
||||
await hook(ctx)
|
||||
|
||||
async def handle_websocket(self, ws: WebSocket) -> None:
|
||||
result = self._tree.get_handler(ws.path, HttpMethod.WS)
|
||||
if result is not None:
|
||||
handler, captured = result
|
||||
await handler.handle_request(ws, captured)
|
||||
else:
|
||||
await ws.close(1000)
|
||||
for hook in self._before_websocket_hooks:
|
||||
new_ws = await hook(ws)
|
||||
if new_ws is not None:
|
||||
ws = new_ws
|
||||
try:
|
||||
result = self._tree.get_handler(ws.path, HttpMethod.WS)
|
||||
if result is not None:
|
||||
handler, captured = result
|
||||
await handler.handle_request(ws, captured)
|
||||
else:
|
||||
await ws.close(1000)
|
||||
finally:
|
||||
for hook in reversed(self._after_websocket_hooks):
|
||||
await hook(ws)
|
||||
|
||||
def setup(self, loop: AbstractEventLoop) -> None:
|
||||
for mixin in self._mixins:
|
||||
mixin.setup(loop)
|
||||
|
||||
def shutdown(self, loop: AbstractEventLoop) -> None:
|
||||
for mixin in self._mixins:
|
||||
mixin.shutdown(loop)
|
||||
|
||||
def route(self,
|
||||
paths: StrOrStrings,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from asyncio import AbstractEventLoop
|
||||
from typing import TYPE_CHECKING, Sequence
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._app import KayaApp
|
||||
|
||||
|
||||
class KayaMixin(ABC):
|
||||
"""Base class for composable Kaya app extensions.
|
||||
|
||||
A mixin modifies a ``KayaApp`` instance in place by registering routes,
|
||||
adding request/websocket hooks, or exposing helper methods on the mixin
|
||||
instance itself. Because the app remains a ``KayaApp``, both ASGI and RSGI
|
||||
protocols keep working regardless of which mixins are applied.
|
||||
|
||||
Mixins may declare other mixins they depend on via ``dependencies``. The
|
||||
``KayaApp`` constructor applies dependencies first and guarantees each
|
||||
mixin is applied at most once.
|
||||
"""
|
||||
|
||||
@property
|
||||
def dependencies(self) -> Sequence['KayaMixin']:
|
||||
return ()
|
||||
|
||||
@abstractmethod
|
||||
def apply(self, app: 'KayaApp') -> None:
|
||||
"""Configure the app: register routes, add hooks, etc."""
|
||||
pass
|
||||
|
||||
def setup(self, loop: AbstractEventLoop) -> None:
|
||||
"""Called on lifespan startup (default: no-op)."""
|
||||
pass
|
||||
|
||||
def shutdown(self, loop: AbstractEventLoop) -> None:
|
||||
"""Called on lifespan shutdown (default: no-op)."""
|
||||
pass
|
||||
Reference in New Issue
Block a user