Create basic sloth app with basic static routing

This commit is contained in:
2025-07-16 03:06:16 +03:00
parent 939ef6073f
commit 751748f82a
3 changed files with 135 additions and 80 deletions

View File

@@ -0,0 +1 @@
from .app import SlothApp

View File

@@ -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({

128
src/turbosloth/app.py Normal file
View File

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