Перейти к содержанию

Миграция на 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)

Изменения:

  1. Обновлены импорты.
  2. Добавлен host: str = web.Header(alias='Host') в HTTP-обработчике create_user.
  3. 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 по всему коду.