Миграция на Rapidy с aiohttp
Описание
rapidy аккуратно расширяет aiohttp, что означает: весь код, написанный на aiohttp, будет работать без изменений.
rapidy использует те же имена модулей, что и aiohttp.
Если нужного объекта нет в rapidy, его можно импортировать напрямую из aiohttp.
Проблемы aiohttp и их решения в Rapidy
Фреймворк aiohttp имеет ряд особенностей и ограничений, которые Rapidy решает более удобным и элегантным способом.
Упрощение работы с параметром request
В aiohttp обработчики маршрутов требуют явного указания параметра request, даже если он не используется.
Пример aiohttp:
from aiohttp import web
router = web.RouteTableDef()
@router.get('/hello')
async def hello(request: web.Request) -> web.Response: # `request` required
return web.json_response({'hello': 'aiohttp'})
app = web.Application()
app.add_routes(router)
В Rapidy параметр request указывать не нужно, что делает код лаконичнее:
Пример Rapidy:
from rapidy import Rapidy
from rapidy.http import get
@get('/hello')
async def handler() -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy(http_route_handlers=[handler])
Подробнее о возможностях обработки Request можно прочитать здесь.
Упрощённая работа с фоновыми задачами
В aiohttp обработчики фоновых задач требуют явного указания параметра app:
from typing import AsyncGenerator
from aiohttp import web
async def bg_task(app: web.Application) -> None:
print('run task')
async def app_ctx(app: web.Application) -> AsyncGenerator[None, None]:
print('starting background task')
yield
print('finish')
app = web.Application()
app.on_startup.append(bg_task)
app.on_shutdown.append(bg_task)
app.on_cleanup.append(bg_task)
app.cleanup_ctx.append(app_ctx)
В Rapidy параметр app не требуется:
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from rapidy import Rapidy
async def bg_task() -> None:
print('run task')
@asynccontextmanager
async def app_ctx() -> AsyncGenerator[None, None]:
print('starting background task')
yield
print('finishing background task')
rapidy = Rapidy(
on_startup=[bg_task],
on_shutdown=[bg_task],
on_cleanup=[bg_task],
lifespan=[app_ctx()],
)
# or
rapidy.lifespan.on_startup.append(bg_task)
rapidy.lifespan.on_shutdown.append(bg_task)
rapidy.lifespan.on_cleanup.append(bg_task)
rapidy.lifespan.append(app_ctx())
Передать атрибут app по-прежнему можно, подробнее здесь.
Подробнее о возможностях работы с фоновыми задачами можно прочитать здесь.
Улучшенный роутинг классовых обработчиков
В aiohttp маршрутизация классовых обработчиков привязана к именам HTTP-методов, что ограничивает гибкость.
В Rapidy можно удобно группировать обработчики:
from rapidy import Rapidy
from rapidy.http import controller, get, PathParam
@controller('/user')
class UserController:
@get('/{user_id}')
async def get_by_id(self, user_id: str = PathParam()) -> dict[str, str]:
return {'user_id': user_id}
@get()
async def get_all_users(self) -> list[dict[str, str]]:
return [{'user_id': '1'}, {'user_id': '2'}]
rapidy = Rapidy(http_route_handlers=[UserController])
Подробнее о возможностях работы с HTTP-обработчиками можно прочитать здесь.
Простая валидация и сериализация данных
В Rapidy можно легко валидировать и сериализовать данные запросов с помощью pydantic:
from pydantic import BaseModel
from rapidy.http import get, Body
class UserData(BaseModel):
name: str
age: int
@get('/user')
async def handler(data: UserData = Body()) -> dict[str, str]:
return {'message': f'User {data.name}, {data.age} years old'}
Более простое деление приложения на модули
В aiohttp подключение подприложений требует явного добавления маршрутов и инициализации объектов Application:
from aiohttp import web
v1_app = web.Application()
app = web.Application()
app.add_subapp('/v1', v1_app)
В Rapidy это реализуется проще:
from rapidy import Rapidy
from rapidy.http import HTTPRouter
v1_app = HTTPRouter('/v1')
rapidy = Rapidy(http_route_handlers=[v1_app])
Ключевые преимущества Rapidy
- Более лаконичный код: минимизация шаблонного кода без потери читаемости.
- Удобная работа с маршрутизацией: возможность группировки обработчиков в классах.
- Гибкая валидация и сериализация данных: встроенная поддержка
pydantic. - Упрощённая работа с запросами и ответами: доступ к
RequestиResponseчерез аннотации типов. - Продвинутое управление жизненным циклом приложения: удобные хуки
on_startup,on_shutdownиon_cleanup. - Лучшая интеграция с асинхронными возможностями Python: меньше лишних параметров и ручной обработки данных.
Подробнее о возможностях Rapidy можно прочитать здесь.
Как мигрировать с aiohttp на Rapidy
Миграция с aiohttp на Rapidy значительно упрощает код, устраняя шаблонный код, улучшая валидацию и упрощая работу с зависимостями.
Полная миграция
Допустим, у вас есть aiohttp-приложение с обработчиком запроса, параметрами запроса, валидацией и хуками жизненного цикла:
Код на aiohttp:
from aiohttp import web
from pydantic import BaseModel, ValidationError
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(request: web.Request) -> web.Response:
data = await request.json()
try:
user = UserData(**data)
except ValidationError as validation_err:
return web.json_response({'error': validation_err.errors()}, status=400)
return web.json_response({'message': f'User {user.name}, {user.age} years old'})
async def on_startup(app: web.Application) -> None:
print("App is starting...")
app = web.Application()
app.on_startup.append(on_startup)
app.add_routes(routes)
web.run_app(app)
Код на Rapidy:
from rapidy import Rapidy
from rapidy.http import post, Body
from pydantic import BaseModel
class UserData(BaseModel):
name: str
age: int
@post('/user')
async def create_user(user: UserData = Body()) -> dict[str, str]:
return {'message': f'User {user.name}, {user.age} years old'}
async def on_startup() -> None:
print("App is starting...")
rapidy = Rapidy(
http_route_handlers=[create_user],
on_startup=[on_startup],
)
Основные улучшения:
- Меньше шаблонного кода — нет явного
request: web.Request,Rapidyсам парсит JSON и валидирует его сpydantic. - Проще работа с жизненным циклом —
on_startupпередаётся списком, безapp.on_startup.append(). - Маршрутизация без
app.add_routes()— обработчики передаются черезhttp_route_handlers.
Частичная миграция
Иногда полная миграция невозможна сразу. Например, если нужно добавить функциональность, но сохранить часть кода на aiohttp.
В таком случае можно адаптировать код постепенно.
Этот подход позволяет переносить код на Rapidy, заменяя части aiohttp на новый синтаксис без переписывания всего проекта.
Добавление небольшой функциональности
Допустим, вам нужно получить заголовок Host и передать его в ответ, не меняя остальную логику.
Было (aiohttp):
from aiohttp import web
from pydantic import BaseModel, ValidationError
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(
request: web.Request,
) -> web.Response:
data = await request.json()
try:
user = UserData(**data)
except ValidationError as validation_err:
return web.json_response({'error': validation_err.errors()}, status=400)
return web.json_response({'message': f'User {user.name}, {user.age} years old'})
async def on_startup(app: web.Application) -> None:
print("App is starting...")
app = web.Application()
app.on_startup.append(on_startup)
app.add_routes(routes)
Стало (Rapidy):
from rapidy import Rapidy, web
from pydantic import BaseModel, ValidationError
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(
request: web.Request,
host: str = web.Header(alias='Host'),
) -> web.Response:
data = await request.json()
try:
user = UserData(**data)
except ValidationError as validation_err:
return web.Response({'error': validation_err.errors()}, status=400)
return web.Response({'message': f'User {user.name}, {user.age} years old, host: {host}'})
async def on_startup(app: web.Application) -> None:
print("App is starting...")
app = web.Application()
app.on_startup.append(on_startup)
app.add_routes(routes)
Изменения:
- Обновлены импорты.
- Добавлен
host: str = web.Header(alias='Host')в HTTP-обработчикеcreate_user. json_responseзаменён наResponse, так как теперь его функции встроены вResponse.
Response в Rapidy умеет больше, подробнее здесь.
Rapidy может все (почти все)
Пример выше описывает как можно дорабатывать код при объявлении HTTP-обработчиков с помощью web.RouteTableDef(),
но Rapidy также поддерживает все другие возможные типы объявления HTTP-обработчиков в стиле aiohttp,
подробнее о способах объявления обработчиков в стеле aiohttp читайте здесь.
Добавление новой функциональности через под-приложения
Вы можете легко расширить существующее приложение на aiohttp, добавляя новые возможности с помощью под-приложений.
Было (aiohttp):
from aiohttp import web
from pydantic import BaseModel
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.post('/user')
async def create_user(request: web.Request) -> web.Response:
# ... some aiohttp code
return web.Response(text='User')
v1_app = web.Application()
v1_app.add_routes(routes)
app = web.Application()
app.add_subapp('/v1', v1_app)
Стало (Rapidy):
from rapidy import web
from rapidy.http import HTTPRouter, get
from pydantic import BaseModel
routes = web.RouteTableDef()
class UserData(BaseModel):
name: str
age: int
@routes.get('/user')
async def get_user_aiohttp(request: web.Request) -> web.Response:
# ... some aiohttp code
return web.Response(text='User aiohttp')
v1_app = web.Application()
v1_app.add_routes(routes)
# --- new functionality
@get('/user')
async def get_user_rapidy() -> str:
return 'User rapidy'
v2_router = HTTPRouter('/v2', route_handlers=[get_user_rapidy])
# ---
app = web.Application(http_route_handlers=[v2_router])
app.add_subapp('/v1', v1_app)
Этот подход позволяет плавно интегрировать Rapidy в существующий код, не требуя значительных изменений.
Тем не менее, вам потребуется обновить импорты aiohttp на rapidy и заменить json_response на Response по всему коду.