This library is designed for code-first people who don't want to bother diving into the details of OpenAPI Specification, but who instead want to use advantages of Python typing system, IDE code-completion and static type checkers to continuously build the API documentation and keep it always up to date.
Openapify is based on the idea of applying decorators on route handlers. Any web-framework has a routing system that let us link a route to a handler (a high-level function or a class method). By using decorators, we can add information about requests, responses and other details that will then be used to create an entire OpenAPI document.
Warning
This library is currently in pre-release stage and may have backward incompatible changes prior to version 1.0. Please use caution when using this library in production environments and be sure to thoroughly test any updates before upgrading to a new version.
- Installation
- Quickstart
- Building the OpenAPI Document
- Integration with web-frameworks
- Decorators
- Plugins
Use pip to install:
$ pip install openapify
Note
In the following example, we will intentionally demonstrate the process of creating an OpenAPI document without being tied to a specific web-framework. However, this process may be easier on a supported web-framework. See Integration with web-frameworks for more info.
Let's see how to build an OpenAPI document with openapify. Suppose we are
writing an app for a bookstore that return a list of new books. Here we have a
dataclass model Book
that would be used as a response model in a real-life
scenario. A function get_new_books
is our handler.
from dataclasses import dataclass
@dataclass
class Book:
title: str
author: str
year: int
def get_new_books(...):
...
Now we want to say that our handler returns a json serialized list of books
limited by the optional count
parameter. We use request_schema
and response_schema
decorators accordingly:
from openapify import request_schema, response_schema
@request_schema(query_params={"count": int})
@response_schema(list[Book])
def get_new_books(...):
...
And now we need to collect all the route definitions and pass them to the
build_spec
function. This function returns an object that has to_yaml
method.
from openapify import build_spec
from openapify.core.models import RouteDef
routes = [RouteDef("/books", "get", get_new_books)]
spec = build_spec(routes)
print(spec.to_yaml())
As a result, we will get the following OpenAPI document which can be rendered using tools such as Swagger UI:
openapi: 3.1.0
info:
title: API
version: 1.0.0
paths:
/books:
get:
parameters:
- name: count
in: query
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Book'
components:
schemas:
Book:
type: object
title: Book
properties:
title:
type: string
author:
type: string
year:
type: integer
additionalProperties: false
required:
- title
- author
- year
The final goal of this library is to build the OpenAPI Document for your web-application. This document consists of common information about the application, such as a title and version, and specific information that outlines the functionalities of the API.
Since openapify is now based
on apispec library, the OpenAPI
document is presented by APISpec
class for the convenience of using the
existing ecosystem of plugins. However, openapify has its own
subclass OpenAPIDocument
which makes it easier to add some common fields,
such as an array
of Server objects or
array of
common Security Scheme
objects.
To build the document, there is build_spec
function. The very basic document
can be created by calling it with an empty list of route definitions, leaving
all the parameters with their default values.
from openapify import build_spec
print(build_spec([]).to_yaml())
As a result, we will get the following document:
openapi: 3.1.0
info:
title: API
version: 1.0.0
paths: {}
We can change the common document attributes either by passing them
to build_spec
:
from openapify import build_spec
from openapify.core.openapi.models import HTTPSecurityScheme
build_spec(
routes=[],
title="My Bookstore API",
version="1.1.0",
openapi_version="3.1.0",
servers=["http://127.0.0.1"],
security_schemes={"basic_auth": HTTPSecurityScheme()}
)
or using a prepared OpenAPIDocument
object:
from openapify import OpenAPIDocument, build_spec
from openapify.core.openapi.models import HTTPSecurityScheme
spec = OpenAPIDocument(
title="My Bookstore API",
version="1.1.0",
openapi_version="3.1.0",
servers=["http://127.0.0.1"],
security_schemes={"basic_auth": HTTPSecurityScheme()},
)
build_spec([], spec)
To add meaning to our document, we can
add Path,
Component
and other OpenAPI objects by applying decorators on our route
handlers and constructing route definitions that will be passed to the builder.
A single complete route definition presented by RouteDef
class can look like
this:
from openapify.core.models import RouteDef
from openapify.core.openapi.models import Parameter, ParameterLocation
def get_book_by_id_handler(...):
...
RouteDef(
path="/book/{id}",
method="get",
handler=get_book_by_id_handler,
summary="Getting the book",
description="Getting the book by id",
parameters=[
Parameter(
name="id",
location=ParameterLocation.PATH,
required=True,
schema={"type": "integer"},
)
],
tags=["book"],
)
As will be shown further, optional
arguments summary
, description
, parameters
and tags
can be overridden
or extended by operation_docs
and request_schema
decorators.
The creating of these route definitions can be automated and adapted to a specific web-framework, and openapify has built-in support for a few of them. See Integration with web-frameworks for details.
There is built-in support for a few web-frameworks, which makes creating the documentation even easier and more fun. Any other frameworks can be integrated with a little effort. If you are ready to take on this, you are very welcome to create a pull request.
The documentation for aiohttp web-application can be built in three ways:
- Using an already existing
aiohttp.web.Application
object - Using a set of
aiohttp.web.RouteDef
objects - Using a set of objects implementing
AioHttpRouteDef
protocol
All we need is to pass either an application, or a set of route defs to
modified build_spec
function. See the example:
from aiohttp import web
from openapify import request_schema, response_schema
from openapify.ext.web.aiohttp import build_spec
routes = web.RouteTableDef()
@response_schema(str, media_type="text/plain")
@routes.post("/")
async def hello(request):
return web.Response(text="Hello, world")
app = web.Application()
app.add_routes(routes)
print(build_spec(app).to_yaml())
As a result, we will get the following document:
openapi: 3.1.0
info:
title: API
version: 1.0.0
paths:
/:
post:
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
🚧 To be described
Openapify has several decorators that embed necessary specific information for later use when building the OpenAPI document. In general, decorators will define the information that will be included in the Operation Object which describes a single API operation on a path. We will look at what each decorator parameter is responsible for and how it is reflected in the final document.
Decorator operation_docs
adds generic information about the Operation object,
which includes summary, description, tags, external documentation and
deprecation marker.
from openapify import operation_docs
An optional, string summary, intended to apply to the operation. This affects
the value of
the summary
field of
the Operation object.
Possible types | Examples |
---|---|
str |
"Getting new books" |
An optional, string description, intended to apply to the
operation. CommonMark syntax MAY be used for
rich text representation. This affects the value of
the description
field of the Operation object.
Possible types | Examples |
---|---|
str |
"Returns a list of books" |
A list of tags for API documentation control. Tags can be used for logical
grouping of operations by resources or any other qualifier. This affects the
value of the tags
field of the Operation object.
Possible types | Examples |
---|---|
Sequence[str] |
["book"] |
Unique string used to identify the operation. This affects the
value of
the operationId
field of the Operation object.
Possible types | Examples |
---|---|
str |
getBooks |
Additional external documentation for the operation. It can be a single url or
(url, description) pair. This affects the value of
the summary
field of
the Operation object.
Possible types | Examples |
---|---|
str |
"https://example.org/docs/books" |
Tuple[str, str] |
("https://example.org/docs/books", "External documentation for /books") |
Declares the operation to be deprecated. Consumers SHOULD refrain from usage
of the declared operation. Default value is false. This affects the value of
the deprecated
field
of the Operation object.
Possible types | Examples |
---|---|
bool |
True |
Decorator request_schema
adds information about the operation requests.
Request can have a body, query parameters, headers and cookies.
from openapify import request_schema
A request body can be described entirely by one body
parameter of type Body
or partially by separate body_*
parameters (see below).
In the first case it is openapify.core.models.Body
object that has all the
separate body_*
parameters inside. This affects the value of
the requestBody
field of the Operation object.
In the second case it is the request body Python data type for which the JSON
Schema will be built. This affects the value of
the requestBody
field of the Operation object, or more precisely,
the schema
field of
Media Type object inside
the value
of content
field
of Request Body object.
Possible types | Examples |
---|---|
Type |
Book |
Body |
Body(
value_type=Book,
media_type="application/json",
required=True,
description="A book",
example={
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
},
) |
A media type
or media type range of the
request body. This affects the value of
the requestBody
field of the Operation object, or more precisely,
the key
of content
field
of Request Body object.
The default value is "application/json"
.
Possible types | Examples |
---|---|
str |
"application/xml" |
Determines if the request body is required in the request. Defaults to false.
This affects the value of
the requestBody
field of the Operation object, or more precisely,
the required
field of Request Body object.
Possible types | Examples |
---|---|
bool |
True |
A brief description of the request body. This could contain examples of
use. CommonMark syntax MAY be used for rich text
representation. This affects the value of
the requestBody
field of the Operation object, or more precisely,
the description
field of Request Body object.
Possible types | Examples |
---|---|
str |
"A book" |
Example of the request body. The example object SHOULD be in the correct format
as specified by the media type. This affects the value of
the requestBody
field of the Operation object, or more precisely,
the example
field
of
Media Type object inside
the value
of content
field
of Request Body object.
Possible types | Examples |
---|---|
Any |
{
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
} |
Examples of the request body. Each example object SHOULD match the media type
and specified schema if present. This affects the value of
the requestBody
field of the Operation object, or more precisely,
the examples
field
of
Media Type object inside
the value
of content
field
of Request Body object.
The values of this dictionary could be either examples themselves,
or openapify.core.openapi.models.Example
objects. In the latter case,
extended information about examples, such as a summary and description, can be
added to the Example
object.
Possible types | Examples |
---|---|
Mapping[str, Any] |
{
"Anna Karenina": {
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
}
} |
Mapping[str, Example] |
{
"Anna Karenina": Example(
value={
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
},
summary="The book 'Anna Karenina'",
)
} |
Dictionary of query parameters applicable for the operation, where the key is
the parameter name and the value can be either a Python data type or
a QueryParam
object.
In the first case it is the Python data type for the query parameter for which
the JSON Schema will be built. This affects the value of
the parameters
field of the Operation object, or more precisely,
the schema
field of
Parameter object.
In the second case it is openapify.core.models.QueryParam
object that can
have extended information about the parameter, such as a default value,
deprecation marker, examples etc.
Possible types | Examples |
---|---|
Mapping[str, Type] |
{"count": int} |
Mapping[str, QueryParam] |
{
"count": QueryParam(
value_type=int,
default=10,
required=True,
description="Limits the number of books returned",
deprecated=False,
allowEmptyValue=False,
example=42,
)
} |
Dictionary of request headers applicable for the operation, where the key is
the header name and the value can be either a string or a Header
object.
In the first case it is the header description. This affects the value of
the parameters
field of the Operation object, or more precisely,
the description
field of Parameter object.
In the second case it is openapify.core.models.Header
object that can have
extended information about the header, such as a description, deprecation
marker, examples etc.
Possible types | Examples |
---|---|
Mapping[str, str] |
{"X-Requested-With": "Information about the creation of the request"} |
Mapping[str, Header] |
{
"X-Requested-With": Header(
description="Information about the creation of the request",
required=True,
value_type=str,
deprecated=False,
allowEmptyValue=False,
example="XMLHttpRequest",
)
} |
Dictionary of request cookies applicable for the operation, where the key is
the cookie name and the value can be either a string or a Cookie
object.
In the first case it is the cookie description. This affects the value of
the parameters
field of the Operation object, or more precisely,
the description
field of Parameter object.
In the second case it is openapify.core.models.Cookie
object that can have
extended information about the cookie, such as a description, deprecation
marker, examples etc.
Possible types | Examples |
---|---|
Mapping[str, str] |
{"__ga": "A randomly generated number as a client ID"} |
Mapping[str, Cookie] |
{
"__ga": Cookie(
description="A randomly generated number as a client ID",
required=True,
value_type=str,
deprecated=False,
allowEmptyValue=False,
example="1.2.345678901.2345678901",
)
} |
Decorator response_schema
describes a single response from the API Operation.
Response can have an HTTP code, body and headers. If the Operation supports
more than one response, then the decorator must be applied multiple times to
cover each of them.
from openapify import response_schema
A Python data type for the response body for which
the JSON Schema will be built. This affects the value of
the responses
field of the Operation object, or more precisely,
the schema
field of
Media Type object inside the value
of content
field
of Response object.
Possible types | Examples |
---|---|
Type |
Book |
An HTTP code of the response. This affects the value of
the responses
field of the Operation object, or more precisely, the patterned key in
the Responses object.
Possible types | Examples |
---|---|
str |
"200" |
int |
400 |
A media type
or media type range of the
response body. This affects the value of
the responses
field of the Operation object, or more precisely, the key
of content
field of
Response object.
The default value is "application/json"
.
Possible types | Examples |
---|---|
str |
"application/xml" |
A description of the response. CommonMark syntax
MAY be used for rich text representation. This affects the value of
the responses
field of the Operation object, or more precisely,
the description
field
of Response object.
Possible types | Examples |
---|---|
str |
"Invalid ID Supplied" |
Dictionary of response headers applicable for the operation, where the key is
the header name and the value can be either a string or a Header
object.
In the first case it is the header description. This affects the value of
the responses
field of the Operation object, or more precisely,
the description
field of Header object.
In the second case it is openapify.core.models.Header
object that can have
extended information about the header, such as a description, deprecation
marker, examples etc.
Possible types | Examples |
---|---|
Mapping[str, str] |
{"Content-Location": "An alternate location for the returned data"} |
Mapping[str, Header] |
{
"Content-Location": Header(
description="An alternate location for the returned data",
example="/index.htm",
)
} |
Example of the response body. The example object SHOULD be in the correct format
as specified by the media type. This affects the value of
the responses
field of the Operation object, or more precisely,
the example
field
of Media Type object inside the value
of content
field of
Response object.
Possible types | Examples |
---|---|
Any |
{
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
} |
Examples of the response body. Each example object SHOULD match the media type
and specified schema if present. This affects the value of
the responses
field of the Operation object, or more precisely,
the examples
field
of Media Type object inside the value
of content
field of
Response object.
The values of this dictionary could be either examples themselves,
or openapify.core.openapi.models.Example
objects. In the latter case,
extended information about examples, such as a summary and description, can be
added to the Example
object.
Possible types | Examples |
---|---|
Mapping[str, Any] |
{
"Anna Karenina": {
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
}
} |
Mapping[str, Example] |
{
"Anna Karenina": Example(
value={
"title": "Anna Karenina",
"author": "Leo Tolstoy",
"year": 1877,
},
summary="The book 'Anna Karenina'",
)
} |
Decorator security_requirements
declares security mechanisms
that can be used for the operation.
from openapify import security_requirements
This decorator takes one or more SecurityRequirement
mappings, where the key
is the requirement name and the value is SecurityScheme
object. There are
classes for
each security scheme
which can be imported as follows:
from openapify.core.openapi.models import (
APIKeySecurityScheme,
HTTPSecurityScheme,
OAuth2SecurityScheme,
OpenIDConnectSecurityScheme,
)
For example, to add authorization by token, you can write something like this:
from openapify import security_requirements
from openapify.core.openapi.models import (
APIKeySecurityScheme,
SecuritySchemeAPIKeyLocation,
)
XAuthTokenSecurityRequirement = {
"x-auth-token": APIKeySecurityScheme(
name="X-Auh-Token",
location=SecuritySchemeAPIKeyLocation.HEADER,
)
}
@security_requirements(XAuthTokenSecurityRequirement)
def secure_operation():
...
And the generated specification document will look like this:
openapi: 3.1.0
info:
title: API
version: 1.0.0
paths:
/secure_path:
get:
security:
- x-auth-token: []
components:
securitySchemes:
x-auth-token:
type: apiKey
name: X-Auh-Token
in: header
Some aspects of creating an OpenAPI document can be changed using plugins.
There is openapify.plugins.BasePlugin
base class, which has all the methods
available for definition. If you want to write a plugin that, for example, will
only generate schema for request parameters, then it will be enough for you to
define only one appropriate method, and leave the rest non-implemented.
Plugin system works by going through all registered plugins and calling
the appropriate method. If such a method raises NotImplementedError
or
returns None
, it is assumed that this plugin doesn't provide the necessary
functionality. Iteration stops at the first plugin that returned something
other than None
.
Plugins are registered via the plugins
argument of the build_spec
function:
from openapify import BasePlugin, build_spec
class MyPlugin1(BasePlugin):
def schema_helper(...):
# return something meaningful here, see the following chapters
...
build_spec(..., plugins=[MyPlugin1()])
OpenAPI Schema object
is built from python types stored in the value_type
attribute of the
following openapify dataclasses defined in openapify.core.models
:
Body
Cookie
Header
QueryParam
Out of the box, the schema is generated by using
mashumaro
library (see the note
below), but support for third-party entity schema generators can be achieved
through schema_helper
method. For example, here's what a plugin for pydantic
models might look like:
from typing import Any
from openapify import BasePlugin
from openapify.core.models import Body, Cookie, Header, QueryParam
from pydantic import BaseModel
class PydanticSchemaPlugin(BasePlugin):
def schema_helper(
self,
obj: Body | Cookie | Header | QueryParam,
name: str | None = None,
) -> dict[str, Any] | None:
if issubclass(obj.value_type, BaseModel):
schema = obj.value_type.model_json_schema(
ref_template="#/components/schemas/{model}"
)
self.spec.components.schemas.update(schema.pop("$defs", {}))
return schema
Note
The BaseSchemaPlugin
,
which is enabled by default and has the lowest priority, is responsible for
generating the schema. This plugin utilizes the mashumaro library for schema
generation, which in turn incorporates its own plugin system,
enabling customization of JSON Schema generation and support for additional
data types. For more nuanced modifications, particularly within nested data
models, you can employ BaseSchemaPlugin
with a specified list of mashumaro JSON
Schema plugins. This approach allows for finer control over schema generation
when needed:
from mashumaro.jsonschema.plugins import DocstringDescriptionPlugin
from openapify import build_spec
from openapify.core.base_plugins import BaseSchemaPlugin
spec = build_spec(
routes=[...],
plugins=[
BaseSchemaPlugin(
plugins=[
DocstringDescriptionPlugin(),
]
),
],
)
A media type is used in OpenAPI Request
Body and
Response objects.
By default, application/octet-stream
is applied for bytes
or bytearray
types, and application/json
is applied otherwise. You can support more media
types or override existing ones with media_type_helper
method.
Let's imagine that you have an API route that returns PNG images as the body.
You can have a separate model class representing images, but the more common
case is to use typing.Annotated
wrapper for bytes. Here's what a plugin for
image/png
media type might look like:
from typing import Annotated, Any, Dict, Optional
from openapify import BasePlugin, build_spec, response_schema
from openapify.core.models import Body, RouteDef
ImagePNG = Annotated[bytes, "PNG"]
class ImagePNGPlugin(BasePlugin):
def media_type_helper(
self, body: Body, schema: Dict[str, Any]
) -> Optional[str]:
if body.value_type is ImagePNG:
return "image/png"
@response_schema(body=ImagePNG)
def foo():
...
routes = [RouteDef("/foo", "get", foo)]
spec = build_spec(routes, plugins=[ImagePNGPlugin()])
print(spec.to_yaml())
The resulting document will contain image/png
content in the response:
openapi: 3.1.0
info:
title: API
version: 1.0.0
paths:
/foo:
get:
responses:
'200':
description: OK
content:
image/png:
schema: {}