OpenAPI 3 is a powerful way of describing request / response specifications for API endpoints. There are two ways on using OpenAPI 3 within Python server applications:
- Generate the schema from Python data structures (as done in Django REST Framework, FastAPI, aiohttp-apispec and others)
- Rely on OpenAPI 3 schema file (as done in pyramid_openapi3)
While both ways have their pros & cons, rororo library is heavily inspired by pyramid_openapi3 and as result requires valid OpenAPI 3 schema file to be provided.
In total, to build aiohttp.web OpenAPI 3 server applications with rororo you need to:
- Provide valid OpenAPI 3 schema file
- Map
operationId
withaiohttp.web
view handler via :class:`rororo.openapi.OperationTableDef` - Call :func:`rororo.openapi.setup_openapi` to finish setup process
Below more details provided for all significant parts.
From one point of view, generating OpenAPI 3 schemas from Python data structures is more Pythonic way, but it results in several issues:
- Which Python data structure use as a basis? For example,
- Django REST Framework generates OpenAPI 3 schemas from their own serializers
- FastAPI relies on pydantic models
- While aiohttp-apispec built on top of APISpec library, which behind the scenes utilies marshmallow data structures
- Data structure library need to support whole OpenAPI 3 specification on their own
- Sharing OpenAPI 3 schema with other parts of your application (frontend, mobile application, etc) became a tricky task, which in most cases requires to be handled by specific CI/CD job
In same time, as rororo requires OpenAPI 3 schema file it allows to,
- Use any Python data structure library for accessing valid request data and for providing valid response data
- Track changes to OpenAPI specification file directly with source control management system as git or mercurial
- Use all available OpenAPI 3 specification features
To start with OpenAPI 3 schema it is recommended to,
- Check whole OpenAPI 3 specification
- Read OpenAPI 3 Documentation
- Browse through OpenAPI 3 Examples
- Try Swagger Editor online tool
To simplify developer experience rororo expects only on OpenAPI 3 schema path.
However it is possible to pass predefined schema
dict and spec
instance
instead. Consult :func:`rororo.openapi.setup_openapi` to check how to achieve
that.
After OpenAPI 3 schema file is valid and ready to be used, it is needed to map OpenAPI operations with aiohttp.web view handlers.
As operationId field for the operation is,
Unique string used to identify the operation. The id MUST be unique among all operations described in the API.
It makes possible to tell aiohttp.web
to use specific view as a handler
for every given OpenAPI 3 operation.
For example,
- OpenAPI 3 specification has
hello_world
operation api.views
module hashello_world
view handler
To connect both of described parts :class:`rororo.openapi.OperationTableDef`
need to be used as (in views.py
):
from aiohttp import web
from rororo import OperationTableDef
operations = OperationTableDef()
@operations.register
async def hello_world(request: web.Request) -> web.Response:
return web.json_response("Hello, world!")
In case, when operationId does not match view handler name it is needed to
to pass operation_id
string as first argument of @operations.register
decorator,
@operations.register("hello_world")
async def not_a_hello_world(
request: web.Request,
) -> web.Response:
return web.json_response("Hello, world!")
rororo supports class based views as well.
In basic mode it expects that OpenAPI schema contains operationId, which
equals to all view method qualified names. For example, code below expects
OpenAPI schema to declare UsersView.get
& UsersView.post
operation IDs,
@operations.register
class UsersView(web.View):
async def get(self) -> web.Response:
...
async def post(self) -> web.Response:
...
Next, it might be useful to provide different prefix instead of UsersView
.
In example below, rororo expects OpenAPI schema to provide users.get
&
users.post
operation IDs,
@operations.register("users")
class UsersView(web.View):
async def get(self) -> web.Response:
...
async def post(self) -> web.Response:
...
Finally, it might be useful to provide custom operationId instead of guessing
it from view or view method name. Example below, illustrates the case, when
OpenAPI schema contains list_users
& create_user
operation IDs,
@operations.register
class UsersView(web.View):
@operations.register("list_users")
async def get(self) -> web.Response:
...
@operations.register("create_user")
async def post(self) -> web.Response:
...
To access :class:`rororo.openapi.data.OpenAPIContext` in class based views you
need to pass self.request
into :func:`rororo.openapi.openapi_context` or
:func:`rororo.openapi.get_openapi_context` as done below,
@operations.register
class UserView(web.View):
async def patch(self) -> web.Response:
user = get_user_or_404(self.request)
with openapi_context(self.request) as context:
next_user = attr.evolve(user, **context.data)
save_user(next_user)
return web.json_response(next_user.to_api_dict())
Important
On registering class based views with multiple view methods (for example
with get
, patch
& put
) you need to ensure that all methods
could be mapped to operation ID in provided OpenAPI schema file.
Decorating view handler with @operations.register
will ensure that it will
be executed only with valid request body & parameters according to OpenAPI 3
operation specification.
If any parameters are missed or invalid, as well as if request body does not pass validation it will result in 422 response.
To access valid data for given request it is recommended to use :func:`rororo.openapi.openapi_context` context manager as follows,
@operations.register
async def add_pet(request: web.Request) -> web.Response:
with openapi_context(request) as context:
...
Resulted context instance will contain,
request
- untouched :class:`aiohttp.web.Request` instanceapp
- :class:`aiohttp.web.Application` instanceconfig_dict
parameters
- valid parameters mappings (path
,query
,header
,cookie
)security
- security data, if operation is secureddata
- valid data from request body
After the OpenAPI 3 schema is provided and view handlers is mapped to OpenAPI operations it is a time to tell an :class:`aiohttp.web.Application` to use given schema file and operations mapping(s) via :func:`rororo.openapi.setup_openapi`.
In most cases this setup should be done in application factory function as follows,
from pathlib import Path
from typing import List
from aiohttp import web
from rororo import setup_openapi
from .views import operations
OPENAPI_YAML_PATH = Path(__file__).parent / "openapi.yaml"
def create_app(argv: List[str] = None) -> web.Application:
app = web.Application()
setup_openapi(app, OPENAPI_YAML_PATH, operations)
return app
Note
It is recommended to store OpenAPI 3 schema file next to main application module, which semantically will mean: this is an OpenAPI 3 schema file for current application.
But it is not mandatory, and you might want to specify any accessible file path, you want.
Note
By default, OpenAPI schema, which is used for the application will be
available via GET requests to {server_url}/openapi.(json|yaml)
, but
it is possible to not serve the schema by passing
has_openapi_schema_handler
falsy flag to
:func:`rororo.openapi.setup_openapi`
Setting up OpenAPI for aiohttp.web applicaitons via :func:`rororo.openapi.setup_openapi` may result in numerous errors as it relies on many things. While most of the errors designed to be self-descriptive below more information added about most possible cases.
rororo expects that schema_path
is a path to a readable file with
OpenAPI schema. To fix the error, pass proper path.
rororo supports reading OpenAPI 3 schema from JSON & YAML files with
extensions: .json
, .yml
, .yaml
. If the schema_path
file
contains valid OpenAPI 3 schema, but has different extension, consider rename
it. Also, in same time rororo expects that .json
files contain valid
JSON, while .yml
/ .yaml
files contain valid YAML data.
rororo requires your OpenAPI 3 schema file to be a valid one. If the file is not valid consider running openapi-spec-validator against your file to find the issues.
Note
rororo depends on openapi-spec-validator (via openapi-core), which
means after installing rororo, virtual environment (or system) will
have openapi-spec-validator
script available
Please, use valid operationId while mapping OpenAPI operation to aiohttp.web view handler.
Using invalid operationId will result in runtime error, which doesn't allow aiohttp.web application to start up.
After OpenAPI setting up for :class:`aiohttp.web.Application` it is possible to access OpenAPI Schema & Spec inside of any view handler as follows,
from rororo import get_openapi_schema, get_openapi_spec async def something(request: web.Request) -> web.Response: # `Dict[str, Any]` with OpenAPI schema schema = get_openapi_schema(request.app) # `openapi_core.schemas.specs.models.Spec` instance spec = get_openapi_spec(request.config_dict) ...
Under the hood rororo heavily relies on openapi-core library.
- :func:`rororo.openapi.setup_openapi`
- Creates the Spec instance from OpenAPI schema source
- Connects previously registered handlers and views to the application router (:class:`aiohttp.web.UrlDispatcher`)
- Registers hidden
openapi_middleware
to handle request to registered handlers and views
- On handling each OpenAPI request RequestValidator.validate(...) method called. Result of validation as :class:`rororo.openapi.data.OpenAPIContext` supplied to current :class:`aiohttp.web.Request` instance
- If enabled, ResponseValidator.validate(...) method called for each OpenAPI response
While rororo designed to support only OpenAPI 3 Schemas due to openapi-core dependency it is technically able to support Swagger 2.0 for aiohttp.web applications in same manner as well.
Important
Swagger 2.0 support is not tested at all and rororo is not intended to provide it.
With that in mind please consider rororo only as a library to bring
OpenAPI 3 Schemas support for aiohttp.web
applications.