diff --git a/src/turbosloth/__init__.py b/src/turbosloth/__init__.py index e69de29..75c5be3 100644 --- a/src/turbosloth/__init__.py +++ b/src/turbosloth/__init__.py @@ -0,0 +1 @@ +from .app import SlothApp diff --git a/src/turbosloth/__main__.py b/src/turbosloth/__main__.py index b06ca37..07b8021 100644 --- a/src/turbosloth/__main__.py +++ b/src/turbosloth/__main__.py @@ -1,86 +1,12 @@ from __future__ import annotations +from turbosloth import SlothApp +from turbosloth.types import Scope, Receive, Send + +app = SlothApp() - -# Экземпляр глобального роутера -router = Router() - - -# Декоратор для удобства регистрации -def route(method: MethodType, path_pattern: str): - def decorator(fn: HandlerType): - router.add(method, path_pattern, fn) - return fn - - return decorator - - -# Сам ASGI-приложение -async def core_app(scope: Scope, receive: Receive, send: Send): - assert scope["type"] == "http", "Unsupported scope type" - method = scope["method"] - path = scope["path"] - - try: - handler, path_params = router.match(method, path) - except KeyError: - # 404 - await send({ - "type": "http.response.start", - "status": 404, - "headers": [(b"content-type", b"text/plain")], - }) - await send({ - "type": "http.response.body", - "body": b"Not Found", - }) - return - - # Вызов хендлера - # Можно передавать path_params, query, body и т.д. - await handler(scope, receive, send, **path_params) - - -def lifespan(app): - async def wrapper(scope, receive, send): - if scope['type'] == 'lifespan': - while True: - event = await receive() - if event['type'] == 'lifespan.startup': - # Инициализация глобальных ресурсов - await on_startup() - await send({'type': 'lifespan.startup.complete'}) - elif event['type'] == 'lifespan.shutdown': - # Очистка перед завершением - await on_shutdown() - await send({'type': 'lifespan.shutdown.complete'}) - break - else: - await app(scope, receive, send) - - return wrapper - - -# Пример функций-обработчиков -async def on_startup(): - print('Подключаем БД, кэш, внешние сервисы') - # await db.connect() - # await cache.initialize() - - -async def on_shutdown(): - print('Отключаем БД, очищаем кэш') - # await db.disconnect() - # await cache.close() - - -app = lifespan(core_app) - - -# Пример регистрации роутов - -@route("GET", "/") +@app.get("/") async def index(scope: Scope, receive: Receive, send: Send): body = b"Hello, ASGI Router!" await send({ @@ -94,7 +20,7 @@ async def index(scope: Scope, receive: Receive, send: Send): }) -@route("GET", "/user/") +@app.get("/user/") async def get_user(scope: Scope, receive: Receive, send: Send): text = f"User ID: ".encode("utf-8") await send({ diff --git a/src/turbosloth/app.py b/src/turbosloth/app.py new file mode 100644 index 0000000..7a9d9cb --- /dev/null +++ b/src/turbosloth/app.py @@ -0,0 +1,128 @@ +from typing import Optional, Callable, Awaitable, Protocol + +from .router import Router +from .types import Scope, Receive, Send, MethodType, HandlerType + + +class ASGIApp(Protocol): + router: Router + + def route(self, method: MethodType, path_pattern: str): + raise RuntimeError('stub!') + + +class HTTPApp(ASGIApp): + async def _do_http(self, scope: Scope, receive: Receive, send: Send): + method = scope['method'] + path = scope['path'] + + try: + handler = self.router.match(method, path) + except KeyError: + # 404 + await send({ + 'type': 'http.response.start', + 'status': 404, + 'headers': [(b'content-type', b'text/plain')], + }) + await send({ + 'type': 'http.response.body', + 'body': b'Not Found', + }) + return + + await handler(scope, receive, send) + + +class WSApp(ASGIApp): + async def _do_websocket(self, scope: Scope, receive: Receive, send: Send): + raise NotImplementedError() + + +class LifespanApp: + _on_startup: Optional[Callable[[], Awaitable[None]]] + _on_shutdown: Optional[Callable[[], Awaitable[None]]] + + async def _do_startup(self, send: Send): + if self._on_startup: + try: + await self._on_startup() + await send({'type': 'lifespan.startup.complete'}) + except Exception as e: + await send({'type': 'lifespan.startup.failed', 'message': str(e)}) + else: + await send({'type': 'lifespan.startup.complete'}) + + async def _do_shutdown(self, send: Send): + if self._on_shutdown: + await self._on_shutdown() + await send({'type': 'lifespan.shutdown.complete'}) + + async def _do_lifespan(self, receive: Receive, send: Send): + while True: + event = await receive() + if event['type'] == 'lifespan.startup': + await self._do_startup(send) + elif event['type'] == 'lifespan.shutdown': + await self._do_shutdown(send) + break + + +class MethodRoutersApp(ASGIApp): + def get(self, path_pattern: str): + return self.route('GET', path_pattern) + + def post(self, path_pattern: str): + return self.route('POST', path_pattern) + + def push(self, path_pattern: str): + return self.route('PUSH', path_pattern) + + def put(self, path_pattern: str): + return self.route('PUT', path_pattern) + + def patch(self, path_pattern: str): + return self.route('PATCH', path_pattern) + + def delete(self, path_pattern: str): + return self.route('DELETE', path_pattern) + + def head(self, path_pattern: str): + return self.route('HEAD', path_pattern) + + def connect(self, path_pattern: str): + return self.route('CONNECT', path_pattern) + + def options(self, path_pattern: str): + return self.route('OPTIONS', path_pattern) + + def trace(self, path_pattern: str): + return self.route('TRACE', path_pattern) + + +class SlothApp(HTTPApp, WSApp, LifespanApp, MethodRoutersApp): + + def __init__(self, + on_startup: Optional[Callable[[], Awaitable[None]]] = None, + on_shutdown: Optional[Callable[[], Awaitable[None]]] = None): + self.router = Router() + self._on_startup = on_startup + self._on_shutdown = on_shutdown + + async def __call__(self, scope: Scope, receive: Receive, send: Send): + t = scope['type'] + if t == 'http': + await self._do_http(scope, receive, send) + elif t == 'lifespan': + await self._do_lifespan(receive, send) + elif t == 'websocket': + await self._do_websocket(scope, receive, send) + + raise RuntimeError(f'Unsupported scope type: {t}') + + def route(self, method: MethodType, path_pattern: str): + def decorator(fn: HandlerType): + self.router.add(method, path_pattern, fn) + return fn + + return decorator