Nested resources и actions#
В этом руководстве показано, как:
- реализовать вложенные ресурсы (например,
/foos/<uuid>/bars/), - реализовать actions над ресурсами (например,
/v1/files/<id>/actions/download).
Руководство опирается на базовый пример CRUD и использует идеи из examples/restapi_foo_bar_service.py и examples/openapi_app.py.
1. Nested resources#
Вложенные ресурсы представляют иерархические отношения, например:
- родительский ресурс:
FooModelпо пути/v1/foos/<uuid>. - вложенная коллекция:
BarModelпо пути/v1/foos/<uuid>/bars/.
1.1. DM-модели#
Как в базовом примере CRUD:
class FooModel(models.ModelWithUUID):
foo_field1 = properties.property(types.Integer(), required=True)
foo_field2 = properties.property(types.String(), default="foo_str")
class BarModel(models.ModelWithUUID):
bar_field1 = properties.property(types.String(min_length=1, max_length=10))
foo = relationships.relationship(FooModel)
1.2. Вложенный контроллер#
Вы можете использовать BaseNestedResourceController для работы с вложенными ресурсами или реализовать контроллер вручную.
Пример с явной обработкой родителя (упрощённо):
class BarController1(controllers.Controller):
"""Handle /v1/foos/<uuid>/bars/."""
__resource__ = resources.ResourceByRAModel(BarModel, process_filters=True)
def create(self, bar_field1, parent_resource):
bar = BarModel(bar_field1=bar_field1, foo=parent_resource)
bar_storage[str(bar.get_id())] = bar
return bar
def filter(self, filters, parent_resource, order_by=None):
# Add parent filter to restrict bars to this foo
return [
bar for bar in bar_storage.values() if bar.foo == parent_resource
]
В качестве альтернативы, BaseNestedResourceController предоставляет встроенные паттерны для работы с parent_resource.
1.3. Вложенный роут#
class BarRoute1(routes.Route):
__controller__ = BarController1
__allow_methods__ = [routes.CREATE, routes.FILTER]
class FooRoute(routes.Route):
__controller__ = FooController
__allow_methods__ = [routes.FILTER, routes.CREATE, routes.GET]
bars = routes.route(BarRoute1, resource_route=True)
resource_route=Trueговорит RA, чтоBarRoute1работает с ресурсами, вложенными под конкретную родительскую инстанцию.- Для
/v1/foos/<foo_uuid>/bars/RA резолвит родительскийFooModelи передаёт его в контроллер какparent_resource.
2. Actions над ресурсами#
Actions представляют операции, которые не являются чистым CRUD для основного ресурса. Типичные примеры:
/v1/files/<id>/actions/download— скачать файл./v1/objects/<id>/actions/some_business_operation.
Отдельно см. нюансы сериализации результатов actions:
2.1. Определение action через ActionHandler#
Модуль: restalchemy.api.actions
from restalchemy.api import actions, constants
from restalchemy.api import controllers, resources
class FileModel(models.ModelWithUUID):
name = properties.property(types.String(), required=True)
class FilesController(controllers.Controller):
__resource__ = resources.ResourceByRAModel(
model_class=FileModel,
process_filters=True,
convert_underscore=False,
)
@actions.get
def download(self, resource, **kwargs):
# Suppose resource represents a file description and you
# want to return binary content.
data = resource.download_file() # user-implemented method
headers = {
"Content-Type": constants.CONTENT_TYPE_OCTET_STREAM,
"Content-Disposition": f'attachment; filename="{resource.name}"',
}
return data, 200, headers
@actions.getоборачиваетdownloadвActionHandler.- Метод контроллера получает
selfкакcontroller, аresource— как текущий экземпляр DM. - Для построения ответа используется
controller.process_result()(черезActionHandler.do_*).
2.2. Определение Action route#
from restalchemy.api import routes
class FileDownloadAction(routes.Action):
"""Handler for /v1/files/<id>/actions/download endpoint."""
__controller__ = FilesController
Затем вы подключаете этот action к resource route:
class FilesRoute(routes.Route):
"""Handler for /v1/files/<id> endpoint."""
__controller__ = FilesController
__allow_methods__ = [
routes.CREATE,
routes.DELETE,
routes.FILTER,
routes.GET,
routes.UPDATE,
]
download = routes.action(FileDownloadAction, invoke=True)
routes.action(FileDownloadAction, invoke=True)означает:- action доступен по пути
/v1/files/<id>/actions/download/invoke. - HTTP-методы
GET/POST/PUTмаппятся наdo_get/do_post/do_putвActionHandler.
2.3. Request/response flow для actions#
Для URL вида /v1/files/<id>/actions/download/invoke:
Route.do()распознаёт сегментactionsв пути.- Загружает ресурс по
<id>через контроллер. - Резолвит action (
download). - Инстанцирует
FileDownloadActionс request. - Вызывает
Action.do(resource=resource, **kwargs). ActionиспользуетActionHandler, чтобы вызвать правильныйdo_*метод обёрнутой функции.
3. Комбинация nested resources и actions#
Actions можно использовать и на вложенных ресурсах:
- Например,
/v1/foos/<foo_id>/bars/<bar_id>/actions/archive.
В этом случае:
- дерево роутов описывает вложенные пути до
BarRoute; - action под
BarRouteможет работать с вложенной инстанциейBarModel.
Паттерн тот же, что и для top-level ресурсов; меняется только routing path.
Резюме#
- Nested resources выражаются через вложенные
Routeклассы иresource_route=True. - Контроллеры могут использовать
parent_resource(илиBaseNestedResourceController) для реализации логики, завязанной на родительскую DM-инстанцию. - Actions реализуются через декораторы
ActionHandlerиroutes.Action/routes.action, что даёт чистый способ описывать не-CRUD операции над ресурсами.