Миграция на 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
по всему коду.