Package: openapify
openapifyFramework agnostic OpenAPI Specification generation for code loversThis library is designed for code-first people who don't want to bother diving
into the details
ofOpenAPI 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.Table of contentsInstallationQuickstartBuilding the OpenAPI DocumentIntegration with web-frameworksaiohttpWriting your own integrationDecoratorsGeneric operation infoRequestResponseSecurity requirementsPluginsschema_helpermedia_type_helperInstallationUse pip to install:$pipinstallopenapifyQuickstart[!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.
SeeIntegration with web-frameworksfor
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 modelBookthat would be used as a response model in a real-life
scenario. A functionget_new_booksis our handler.fromdataclassesimportdataclass@dataclassclassBook:title:strauthor:stryear:intdefget_new_books(...):...Now we want to say that our handler returns a json serialized list of books
limited by the optionalcountparameter. We userequest_schemaandresponse_schemadecorators accordingly:fromopenapifyimportrequest_schema,response_schema@request_schema(query_params={"count":int})@response_schema(list[Book])defget_new_books(...):...And now we need to collect all the route definitions and pass them to thebuild_specfunction. This function returns an object that hasto_yamlmethod.fromopenapifyimportbuild_specfromopenapify.core.modelsimportRouteDefroutes=[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.0info:title:APIversion:1.0.0paths:/books:get:parameters:-name:countin:queryschema:type:integerresponses:'200':description:OKcontent:application/json:schema:type:arrayitems:$ref:'#/components/schemas/Book'components:schemas:Book:type:objecttitle:Bookproperties:title:type:stringauthor:type:stringyear:type:integeradditionalProperties:falserequired:-title-author-yearBuilding the OpenAPI DocumentThe final goal of this library is to build
theOpenAPI Documentfor 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
onapispeclibrary, the OpenAPI
document is presented byAPISpecclass for the convenience of using the
existing ecosystem of plugins. However, openapify has its own
subclassOpenAPIDocumentwhich makes it easier to add some common fields,
such as an array
ofServerobjects or
array of
commonSecurity Schemeobjects.To build the document, there isbuild_specfunction. 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.fromopenapifyimportbuild_specprint(build_spec([]).to_yaml())As a result, we will get the following document:openapi:3.1.0info:title:APIversion:1.0.0paths:{}We can change the common document attributes either by passing them
tobuild_spec:fromopenapifyimportbuild_specfromopenapify.core.openapi.modelsimportHTTPSecuritySchemebuild_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 preparedOpenAPIDocumentobject:fromopenapifyimportOpenAPIDocument,build_specfromopenapify.core.openapi.modelsimportHTTPSecuritySchemespec=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
addPath,Componentand other OpenAPI objects by applyingdecoratorson our route
handlers and constructing route definitions that will be passed to the builder.
A single complete route definition presented byRouteDefclass can look like
this:fromopenapify.core.modelsimportRouteDeffromopenapify.core.openapi.modelsimportParameter,ParameterLocationdefget_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
argumentssummary,description,parametersandtagscan be overridden
or extended byoperation_docsandrequest_schemadecorators.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.
SeeIntegration with web-frameworksfor
details.Integration with web-frameworksThere 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 apull request.aiohttpThe documentation foraiohttpweb-application can be built in three ways:Using an already existingaiohttp.web.ApplicationobjectUsing a set ofaiohttp.web.RouteDefobjectsUsing a set of objects implementingAioHttpRouteDefprotocolAll we need is to pass either an application, or a set of route defs to
modifiedbuild_specfunction. See the example:fromaiohttpimportwebfromopenapifyimportrequest_schema,response_schemafromopenapify.ext.web.aiohttpimportbuild_specroutes=web.RouteTableDef()@response_schema(str,media_type="text/plain")@routes.post("/")asyncdefhello(request):returnweb.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.0info:title:APIversion:1.0.0paths:/:post:responses:'200':description:OKcontent:text/plain:schema:type:stringWriting your own integration🚧 To be describedDecoratorsOpenapify 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
theOperation Objectwhich 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.Generic operation infoDecoratoroperation_docsadds generic information about the Operation object,
which includes summary, description, tags, external documentation and
deprecation marker.fromopenapifyimportoperation_docssummaryAn optional, string summary, intended to apply to the operation. This affects
the value of
thesummaryfield of
the Operation object.Possible typesExamplesstr"Getting new books"descriptionAn optional, string description, intended to apply to the
operation.CommonMark syntaxMAY be used for
rich text representation. This affects the value of
thedescriptionfield of the Operation object.Possible typesExamplesstr"Returns a list of books"tagsA 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 thetagsfield of the Operation object.Possible typesExamplesSequence[str]["book"]operation_idUnique string used to identify the operation. This affects the
value of
theoperationIdfield of the Operation object.Possible typesExamplesstrgetBooksexternal_docsAdditional external documentation for the operation. It can be a single url or
(url, description) pair. This affects the value of
thesummaryfield of
the Operation object.Possible typesExamplesstr"https://example.org/docs/books"Tuple[str, str]("https://example.org/docs/books", "External documentation for /books")deprecatedDeclares the operation to be deprecated. Consumers SHOULD refrain from usage
of the declared operation. Default value is false. This affects the value of
thedeprecatedfield
of the Operation object.Possible typesExamplesboolTrueRequestDecoratorrequest_schemaadds information about the operation requests.
Request can have a body, query parameters, headers and cookies.fromopenapifyimportrequest_schemabodyA request body can be described entirely by onebodyparameter of typeBodyor partially by separatebody_*parameters (see below).In the first case it isopenapify.core.models.Bodyobject that has all the
separatebody_*parameters inside. This affects the value of
therequestBodyfield 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
therequestBodyfield of the Operation object, or more precisely,
theschemafield of
Media Type object inside
the value
ofcontentfield
of Request Body object.Possible typesExamplesTypeBookBodyBody(value_type=Book,media_type="application/json",required=True,description="A book",example={"title":"Anna Karenina","author":"Leo Tolstoy","year":1877,},)media_typeA media type
ormedia type rangeof the
request body. This affects the value of
therequestBodyfield of the Operation object, or more precisely,
the key
ofcontentfield
of Request Body object.The default value is"application/json".Possible typesExamplesstr"application/xml"body_requiredDetermines if the request body is required in the request. Defaults to false.
This affects the value of
therequestBodyfield of the Operation object, or more precisely,
therequiredfield of Request Body object.Possible typesExamplesboolTruebody_descriptionA brief description of the request body. This could contain examples of
use.CommonMark syntaxMAY be used for rich text
representation. This affects the value of
therequestBodyfield of the Operation object, or more precisely,
thedescriptionfield of Request Body object.Possible typesExamplesstr"A book"body_exampleExample of the request body. The example object SHOULD be in the correct format
as specified by the media type. This affects the value of
therequestBodyfield of the Operation object, or more precisely,
theexamplefield
of
Media Type object inside
the value
ofcontentfield
of Request Body object.Possible typesExamplesAny{"title":"Anna Karenina","author":"Leo Tolstoy","year":1877,}body_examplesExamples of the request body. Each example object SHOULD match the media type
and specified schema if present. This affects the value of
therequestBodyfield of the Operation object, or more precisely,
theexamplesfield
of
Media Type object inside
the value
ofcontentfield
of Request Body object.The values of this dictionary could be either examples themselves,
oropenapify.core.openapi.models.Exampleobjects. In the latter case,
extended information about examples, such as a summary and description, can be
added to theExampleobject.Possible typesExamplesMapping[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'",)}query_paramsDictionary 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
aQueryParamobject.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
theparametersfield of the Operation object, or more precisely,
theschemafield of
Parameter object.In the second case it isopenapify.core.models.QueryParamobject that can
have extended information about the parameter, such as a default value,
deprecation marker, examples etc.Possible typesExamplesMapping[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,)}headersDictionary of request headers applicable for the operation, where the key is
the header name and the value can be either a string or aHeaderobject.In the first case it is the header description. This affects the value of
theparametersfield of the Operation object, or more precisely,
thedescriptionfield of Parameter object.In the second case it isopenapify.core.models.Headerobject that can have
extended information about the header, such as a description, deprecation
marker, examples etc.Possible typesExamplesMapping[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",)}cookiesDictionary of request cookies applicable for the operation, where the key is
the cookie name and the value can be either a string or aCookieobject.In the first case it is the cookie description. This affects the value of
theparametersfield of the Operation object, or more precisely,
thedescriptionfield of Parameter object.In the second case it isopenapify.core.models.Cookieobject that can have
extended information about the cookie, such as a description, deprecation
marker, examples etc.Possible typesExamplesMapping[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",)}ResponseDecoratorresponse_schemadescribes 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.fromopenapifyimportresponse_schemabodyA Python data type for the response body for which
the JSON Schema will be built. This affects the value of
theresponsesfield of the Operation object, or more precisely,
theschemafield of
Media Type object inside the value
ofcontentfield
of Response object.Possible typesExamplesTypeBookhttp_codeAn HTTP code of the response. This affects the value of
theresponsesfield of the Operation object, or more precisely, the patterned key in
theResponsesobject.Possible typesExamplesstr"200"int400media_typeA media type
ormedia type rangeof the
response body. This affects the value of
theresponsesfield of the Operation object, or more precisely, the key
ofcontentfield of
Response object.The default value is"application/json".Possible typesExamplesstr"application/xml"descriptionA description of the response.CommonMark syntaxMAY be used for rich text representation. This affects the value of
theresponsesfield of the Operation object, or more precisely,
thedescriptionfield
of Response object.Possible typesExamplesstr"Invalid ID Supplied"headersDictionary of response headers applicable for the operation, where the key is
the header name and the value can be either a string or aHeaderobject.In the first case it is the header description. This affects the value of
theresponsesfield of the Operation object, or more precisely,
thedescriptionfield of Header object.In the second case it isopenapify.core.models.Headerobject that can have
extended information about the header, such as a description, deprecation
marker, examples etc.Possible typesExamplesMapping[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",)}exampleExample of the response body. The example object SHOULD be in the correct format
as specified by the media type. This affects the value of
theresponsesfield of the Operation object, or more precisely,
theexamplefield
of Media Type object inside the value
ofcontentfield of
Response object.Possible typesExamplesAny{"title":"Anna Karenina","author":"Leo Tolstoy","year":1877,}examplesExamples of the response body. Each example object SHOULD match the media type
and specified schema if present. This affects the value of
theresponsesfield of the Operation object, or more precisely,
theexamplesfield
of Media Type object inside the value
ofcontentfield of
Response object.The values of this dictionary could be either examples themselves,
oropenapify.core.openapi.models.Exampleobjects. In the latter case,
extended information about examples, such as a summary and description, can be
added to theExampleobject.Possible typesExamplesMapping[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'",)}Security requirementsDecoratorsecurity_requirementsdeclaressecurity mechanismsthat can be used for the operation.fromopenapifyimportsecurity_requirementsThis decorator takes one or moreSecurityRequirementmappings, where the key
is the requirement name and the value isSecuritySchemeobject. There are
classes for
eachsecurity schemewhich can be imported as follows:fromopenapify.core.openapi.modelsimport(APIKeySecurityScheme,HTTPSecurityScheme,OAuth2SecurityScheme,OpenIDConnectSecurityScheme,)For example, to add authorization by token, you can write something like this:fromopenapifyimportsecurity_requirementsfromopenapify.core.openapi.modelsimport(APIKeySecurityScheme,SecuritySchemeAPIKeyLocation,)XAuthTokenSecurityRequirement={"x-auth-token":APIKeySecurityScheme(name="X-Auh-Token",location=SecuritySchemeAPIKeyLocation.HEADER,)}@security_requirements(XAuthTokenSecurityRequirement)defsecure_operation():...And the generated specification document will look like this:openapi:3.1.0info:title:APIversion:1.0.0paths:/secure_path:get:security:-x-auth-token:[]components:securitySchemes:x-auth-token:type:apiKeyname:X-Auh-Tokenin:headerPluginsSome aspects of creating an OpenAPI document can be changed using plugins.
There isopenapify.plugins.BasePluginbase 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 raisesNotImplementedErroror
returnsNone, it is assumed that this plugin doesn't provide the necessary
functionality. Iteration stops at the first plugin that returned something
other thanNone.Plugins are registered via thepluginsargument of thebuild_specfunction:fromopenapifyimportBasePlugin,build_specclassMyPlugin1(BasePlugin):defschema_helper(...):# return something meaningful here, see the following chapters...build_spec(...,plugins=[MyPlugin1()])schema_helperOpenAPISchemaobject
is built from python types stored in thevalue_typeattribute of the
following openapify dataclasses defined inopenapify.core.models:BodyCookieHeaderQueryParamOut of the box, the schema is generated by usingmashumarolibrary, but support
for third-party entity schema generators can be achieved throughschema_helpermethod. For example, here's what a plugin for pydantic models
might look like:fromtypingimportAnyfromopenapifyimportBasePluginfromopenapify.core.modelsimportBody,Cookie,Header,QueryParamfrompydanticimportBaseModelclassPydanticSchemaPlugin(BasePlugin):defschema_helper(self,obj:Body|Cookie|Header|QueryParam,name:str|None=None,)->dict[str,Any]|None:ifissubclass(obj.value_type,BaseModel):schema=obj.value_type.model_json_schema(ref_template="#/components/schemas/{model}")self.spec.components.schemas.update(schema.pop("$defs",{}))returnschemamedia_type_helperA media type is used in OpenAPI RequestBodyandResponseobjects.
By default,application/octet-streamis applied forbytesorbytearraytypes, andapplication/jsonis applied otherwise. You can support more media
types or override existing ones withmedia_type_helpermethod.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 usetyping.Annotatedwrapper for bytes. Here's what a plugin forimage/pngmedia type might look like:fromtypingimportAnnotated,Any,Dict,OptionalfromopenapifyimportBasePlugin,build_spec,response_schemafromopenapify.core.modelsimportBody,RouteDefImagePNG=Annotated[bytes,"PNG"]classImagePNGPlugin(BasePlugin):defmedia_type_helper(self,body:Body,schema:Dict[str,Any])->Optional[str]:ifbody.value_typeisImagePNG:return"image/png"@response_schema(body=ImagePNG)deffoo():...routes=[RouteDef("/foo","get",foo)]spec=build_spec(routes,plugins=[ImagePNGPlugin()])print(spec.to_yaml())The resulting document will containimage/pngcontent in the response:openapi:3.1.0info:title:APIversion:1.0.0paths:/foo:get:responses:'200':description:OKcontent:image/png:schema:{}