Маршрутизация и создание HTTP-обработчиков
Веб-обработчики (handlers) предназначены для обработки входящих HTTP-запросов.
В Rapidy
маршрутизация и создание обработчиков тесно связаны: маршрутизация определяет, какой обработчик будет вызван в ответ на запрос.
Определение маршрутов
Маршрут — это строка URL
, по которой вызывается обработчик.
Всего существуют два вида маршрутов - статические и динамические.
Разница между статическим и динамическим роутингом
Тип маршрута | Пример URL | Описание |
---|---|---|
Статический | /about | URL фиксирован. |
Динамический | /users/{user_id} | URL меняется в зависимости от параметров |
Rapidy
поддерживает несколько способов определения маршрутов, аналогичных aiohttp
, подробнее о них ниже в разделе Создание и регистрация обработчиков
HTTP-запросов
.
Подробнее про обработчики aiohttp
можно узнать здесь.
Статические маршруты
Статический HTTP-роутинг — это маршрутизация, где путь (URL) заранее известен и не меняется динамически. Это означает, что каждый запрос с определённым маршрутом всегда приводит к одному и тому же обработчику.
Простой статический маршрут
from rapidy.http import get
@get('/hello_rapidy')
async def handler() -> str:
return 'Hello Rapidy!'
Этот маршрут всегда доступен по GET /hello
и возвращает один и тот же ответ.
Таким же образом вы можете определять и другие методы, такие как get
, post
, put
, delete
и так далее.
Динамические маршруты
Динамический роутинг позволяет задавать маршруты, которые принимают переменные параметры.
Это полезно, когда нужно работать с различными сущностями (например, user_id
, post_id
и т.д.), передавая их в URL
.
В примерах ниже будет использоваться PathParam
, который необходим для извлечения параметров-путей. Подробнее о нем можно прочитать здесь.
Простой динамический маршрут
Допустим, у нас есть API для получения информации о пользователе по его user_id:
from rapidy.http import get, PathParam
@get('/users/{user_id}')
async def handler(user_id: int = PathParam()) -> dict[str, int]:
return {'user_id': user_id}
Как работает этот маршрут?
user_id
— динамический параметр, который передаётся в URL.Rapidy
автоматически преобразует его вint
(если передать строку, API вернёт ошибку422
).
Пример запроса:
Ответ:
Динамические маршруты с несколькими параметрами
Можно добавить несколько динамических параметров:
from rapidy.http import get, PathParam
@get('/posts/{post_id}/comments/{comment_id}')
async def handler(
post_id: int = PathParam(),
comment_id: int = PathParam(),
) -> dict[str, int]:
return {'post_id': post_id, 'comment_id': comment_id}
Теперь запрос GET /posts/10/comments/5
вернёт:
Группировка маршрутов
Если у вас много маршрутов, можно использовать один из подходов для группировки HTTP-запросов.
Рекомендуется придерживаться только одного подхода в рамках проекта.
HTTPRouter
Rapidy
предлагает использовать объект HTTPRouter
для группировки запросов.
HTTPRouter
позволяет регистрировать группы обработчиков и играет ключевую роль в маршрутизации (routing),
помогая направлять запросы к нужным обработчикам в зависимости от HTTP-метода, пути, параметров и других условий.
HTTPRouter регистрируется точно так же, как и любой HTTP-обработчик.
from rapidy import Rapidy
from rapidy.http import HTTPRouter, controller, get
@get('/healthcheck') # /healthcheck
async def healthcheck() -> str:
return 'ok'
@get('/hello') # /api/hello
async def hello_handler() -> dict[str, str]:
return {'hello': 'rapidy'}
api_router = HTTPRouter('/api', [hello_handler])
rapidy = Rapidy(http_route_handlers=[healthcheck, api_router])
HTTPRouter
умеет больше!
HTTPRouter
также имеет ряд атрибутов расширяющих его возможности, такие как обработка middleware
, управление фоновыми задачами и тд.
Также вы можете создавать вложенные HTTPRouter
.
Подробнее про HTTPRouter
можно прочитать здесь
Создание и регистрация обработчиков HTTP-запросов
Функциональные обработчики
Простейший способ создания обработчика:
from rapidy import Rapidy
from rapidy.http import post
@post('/')
async def handler() -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy(http_route_handlers=[handler])
Примеры регистрации обработчиков
Регистрация обработчика без декоратора
Добавление обработчика через router
приложения (в стиле aiohttp
)
from rapidy import Rapidy
async def handler() -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy()
rapidy.router.add_post('/', handler)
Поддерживаемые методы соответствуют HTTP-методам с префиксом add_
.
add_get
add_post
add_put
add_patch
add_delete
Исключение — view
.
add_view
Добавление обработчика с декоратором через RouteTableDef
(в стиле aiohttp
)
Добавление обработчика без декоратора через rapidy.web
(в стиле aiohttp
)
Классовые обработчики
Классовые обработчики позволяют объединять несколько методов в одном классе:
from rapidy import Rapidy
from rapidy.http import PathParam, controller, get, post, put, patch, delete
@controller('/')
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 [{'name': 'John'}, {'name': 'Felix'}]
@post()
async def create_user(self) -> str:
return 'ok'
@put()
async def update_user(self) -> str:
return 'ok'
@patch()
async def patch_user(self) -> str:
return 'ok'
@delete()
async def delete_user(self) -> str:
return 'ok'
rapidy = Rapidy(http_route_handlers=[UserController])
Примеры регистрации классовых обработчиков
Регистрация обработчика без декоратора
from rapidy import Rapidy
from rapidy.http import PathParam, controller, get
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 [{'name': 'John'}, {'name': 'Felix'}]
rapidy = Rapidy(
http_route_handlers=[
controller.reg('/', UserController),
]
)
Использование View
(aiohttp style)
Добавление обработчика через router
приложения (в стиле aiohttp
)
from rapidy import Rapidy
from rapidy.web import View
class Handler(View):
async def get(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy()
rapidy.router.add_view('/', Handler)
Добавление обработчика через router
с разными путями (в стиле aiohttp
)
from rapidy import Rapidy
from rapidy.web import View, PathParam
class Handler(View):
async def get(self, user_id: str = PathParam()) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = Rapidy()
rapidy.router.add_get('/{user_id}', Handler)
rapidy.router.add_view('/', Handler)
Добавление обработчика с декоратором через RouteTableDef
(в стиле aiohttp
)
from rapidy import web
routes = web.RouteTableDef()
@routes.view('/')
class Handler(web.View):
async def get(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes(routes)
Добавление обработчика с декоратором через RouteTableDef
с разными путями (в стиле aiohttp
)
from rapidy import web
routes = web.RouteTableDef()
@routes.view('/')
class Handler(web.View):
@routes.get('/{user_id}')
async def get(self, user_id: str = web.PathParam()) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes(routes)
Добавление обработчика без декоратора через rapidy.web
(в стиле aiohttp
)
from rapidy import web
routes = web.RouteTableDef()
class Handler(web.View):
async def get(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes([web.view('/', Handler)])
Добавление обработчика без декоратора через rapidy.web
с разными путями (в стиле aiohttp
)
from rapidy import web
routes = web.RouteTableDef()
class Handler(web.View):
async def get(self, user_id: str = web.PathParam()) -> dict[str, str]:
return {'hello': 'rapidy'}
async def post(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def put(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def patch(self) -> dict[str, str]:
return {'hello': 'rapidy'}
async def delete(self) -> dict[str, str]:
return {'hello': 'rapidy'}
rapidy = web.Application()
rapidy.add_routes([
web.get('/{user_id}', Handler),
web.view('/', Handler),
])
Атрибуты обработчиков
Атрибуты позволяют управлять поведением обработчиков и ответами.
Атрибуты автоматически применяются к ответам обработчиков, если обработчик возвращает что угодно кроме Response
(не относится к атрибутам path
и allow_head
для метода get
).
Атрибуты применяются к ответам.
Атрибут response_content_type
будет применяться к каждому ответу обработчика,
поскольку обработчик возвращает python
объект.
Атрибуты не применяются к ответам.
Атрибут response_content_type
не будет применяться к ответу обработчика,
поскольку обработчик возвращает низкоуровневый Response
объект.
Все способы создания обработчиков поддерживают одинаковые атрибуты для управления веб-запросом.
Основные атрибуты (применяются всегда)
path
path
: str
— маршрут обработчика на сервере.
allow_head
allow_head
: bool = True
— если равен True (по умолчанию), то добавляется маршрут для метода head
с тем же обработчиком, что и для get
.
Аттрибут может быть применен только к методу get
.
Валидация ответа
response_validate
response_validate
: bool = True
— проверять ли ответ обработчика.
@get(
'/',
response_validate=False,
)
async def handler() -> str: # <-- `str` will be ignored
return {'hello': 'rapidy'}
response_type
response_type
: Type[Any] | None = ...
— определяет тип ответа (заменяет аннотацию возврата).
@get(
'/',
response_type=dict[str, str], # <-- `dict[str, str]` will be used for validation
)
async def handler() -> str: # <-- `str` will be ignored
return {'hello': 'rapidy'}
Этот флаг добавляет гибкость в сериализацию и валидацию, но используется редко.
Управление заголовками и кодировкой
response_content_type
response_content_type
: str = 'application/json'
— аттрибут позволяющий управлять заголовком Content-Type
.
Заголовок Content-Type
сообщает клиенту (браузеру, API-клиенту, другому серверу), какой тип данных содержится в теле HTTP-ответа.
from rapidy.enums import ContentType
@get(
'/',
response_content_type=ContentType.text_plain,
)
async def handler() -> str:
return 'hello, rapidy!'
Если указан content_type
, переданные данные будут преобразованы в соответствии с ним.
Если content_type
не указан - content_type
будет определен автоматически в зависимости от типа данных которое отдает сервер.
content_type="application/json
content_type="application/json"
— данные преобразуются в JSON
с использованием jsonify(dumps=True)
и кодируются в соответствии с response_charset.
content_type="text/*
content_type="text/*"
(любой текстовый тип: text/plain
, text/html
и т. д.) - если данные имеют тип str
, они отправляются без изменений.
В противном случае они преобразуются в строку через jsonify(dumps=False).
from rapidy.http import get, ContentType
@get(
'/',
content_type=ContentType.text_any,
)
async def handler() -> str:
return 'hello rapidy!' # "hello rapidy!"
Если после jsonify(dumps=False)
объект все еще не является строкой, он дополнительно преобразуется с помощью response_json_encoder.
content_type - любой другой MIME-type.
Если данные имеют тип bytes
, они отправляются без изменений.
В противном случае они преобразуются в строку с использованием jsonify(dumps=True) и кодируются в соответствии с response_json_encoder.
Если content_type
не указан, он устанавливается автоматически:
-
body: dict | BaseModel | dataclass
→content_type="application/json"
-
body: str | Enum | int | float | Decimal | bool
→content_type="text/plain"
-
body: Any
→content_type="application/octet-stream"
response_charset
response_charset
: str = 'utf-8'
— кодировка, используемая для кодирования данных ответа.
from rapidy.enums import Charset
@get(
'/',
response_charset=Charset.utf8,
)
async def handler() -> str:
return 'hello, rapidy!'
response_json_encoder
response_json_encoder
: Callable = json.dumps
— функция, принимающая объект и возвращающая его JSON-представление.
Автоматически применяется к любому python объекту после валидации его через pydantic
.
from typing import Any
def custom_encoder(obj: Any) -> str:
...
@get(
'/',
response_json_encoder=custom_encoder, # Converts the obtained string above into a JSON object using the `custom_encoder` function
)
async def handler() -> dict[str, str]:
return {'hello': 'rapidy!'} # will be converted to a string by Rapidy's internal tools
Сжатие данных
response_zlib_executor
response_zlib_executor
: concurrent.futures.Executor | None = None
— функция сжатия zlib
.
from concurrent.futures import Executor
class SomeExecutor(Executor):
...
@get(
'/',
response_zlib_executor=SomeExecutor,
)
async def handler() -> str:
return 'hello, rapidy!'
Подробнее о zlib_executor
zlib_executor
— механизм aiohttp
. Подробнее здесь.
response_zlib_executor
response_zlib_executor_size: int | None = None
— размер тела в байтах для включения сжатия.
Управление полями Pydantic
response_include_fields
response_include_fields
**: set[str] | dict[str, Any] | None = None
— параметр include
из Pydantic
, указывающий, какие поля включать.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
another_value: str = Field('another_data', alias='someAnotherValue')
@get(
'/',
response_include_fields={'value'},
)
async def handler() -> Result:
return Result() # {'someValue': 'data'}
response_exclude_fields
response_exclude_fields
: set[str] | dict[str, Any] | None
— список полей для исключения.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
another_value: str = Field('another_data', alias='someAnotherValue')
@get(
'/',
response_exclude_fields={'value'},
)
async def handler() -> Result:
return Result() # {"someValue": "data"}
response_by_alias
response_by_alias
: bool = True
— использовать ли псевдонимы Pydantic
.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
@get(
'/',
response_by_alias=True, # <-- default
)
async def handler() -> Result:
return Result() # {"someValue": "data"}
...
@get(
'/',
response_by_alias=False,
)
async def handler() -> Result:
return Result() # {"value": "data"}
response_exclude_unset
response_exclude_unset
: bool = False
— исключать ли значения по умолчанию.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
another_value: str = Field('another_data', alias='someAnotherValue')
@get(
'/',
exclude_unset=False, # <-- default
)
async def handler() -> Result:
Result(someAnotherValue='new_data') # {"someValue": "data", "someAnotherValue": "new_data"}
...
@get(
'/',
exclude_unset=True,
)
async def handler() -> Result:
return Result(someAnotherValue='new_data') # {"someAnotherValue": "new_data"}
response_exclude_defaults
response_exclude_defaults
: bool = False
— исключать ли явно заданные значения, если они совпадают с дефолтными.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
@get(
'/',
exclude_defaults=False, # <-- default
)
async def handler() -> Result:
return Result() # {"value": "data"}
...
@get(
'/',
exclude_defaults=True,
)
async def handler() -> Result:
return Result() # {}
response_exclude_none
response_exclude_none
: bool = False
— исключать ли None
-значения.
from pydantic import BaseModel, Field
class Result(BaseModel):
value: str = Field('data', alias='someValue')
none_value: None = None
@get(
'/',
exclude_none=False, # <-- default
)
async def handler() -> Result:
return Result() # {"someValue": "data", "none_value": null}
...
@get(
'/',
exclude_none=True,
)
async def handler() -> Result:
return Result() # {"someValue": "data"}
response_custom_encoder
response_custom_encoder
: Callable | None = None
— параметр custom_encoder из Pydantic
, позволяющий задать пользовательский кодировщик.