Generating Documentation¶
SpansServer offers automatic documentation of your API by generating openapi v3 documentation which can be displayed by tools like swagger and redoc.
Spanserver still supports Responder’s method of writing openapi yaml in docstrings, but also introduces it’s own set of convenience functions.
Basic API Information¶
The API title, description and version can all be passed to the init method of
SpanAPI
.
from spanserver import SpanRoute, Request, Response, SpanAPI
grievous = SpanAPI(
title="Grievous", description="A four-armed sickly general.", version="1.0.0", openapi="3.0.0"
)
print(grievous.openapi)
Output:
info:
description: A four-armed sickly general.
title: Grievous
version: 1.0.0
openapi: 3.0.0
Route Method¶
Summaries and descriptions for routes will be pulled directly from the docstrings.
# set up the route
@grievous.route("/greet")
class Greet(SpanRoute):
async def on_get(self, req: Request, resp: Response):
"""
Provoke a greeting.
Grievous likes to shout things. He is a bold one.
"""
# route logic goes here.
print(grievous.openapi)
Output:
info:
...
paths:
/greet:
get:
description: Grievous likes to shout things. He is a bold one.
responses:
'200':
description: Ok.
default:
description: Error.
headers:
error-code:
description: An API error code that identifies the error-type.
required: true
schema:
default: 1000
type: integer
error-data:
description: 'JSON-serialized data about the error. For instance: request
body validation errors will return a dict with details about all offending
fields.'
required: false
schema:
format: dict
type: string
error-id:
description: A unique ID with details about this error. Please reference
when reporting errors.
required: false
schema:
format: uuid
type: string
error-message:
description: Message containing information about the error.
required: true
schema:
default: An unknown error has occurred.
type: string
error-name:
description: Human-readable error name.
required: true
schema:
default: APIError
type: string
summary: Provoke a greeting.
Wow! That’s a lot of info. Let’s make a quick breakdown of what got added automatically:
The route’s
summary
is pulled from the first paragraph of the docstring.The route’s description is pulled from all subsequent paragraphs.
Default elements detailed below.
Default Documentation¶
Some elements are added by default when no explicit configuration is given:
A default response of
'200'
is generated when no other non-error response is specified.A
'default'
error response is generated when no other error response is specified.A default description
'Ok.'
is given to any non-error response codes without a descriptionA default description
'Error.'
is given to any error response codes without a descriptionResponse headers for spanserver-style errors are automatically added to any error response codes.
What is an error response?¶
Spanserver will consider any response code between 400 and 599 to be an error, and automatically add response header documentation / default response descriptions accordingly.
Non-default Responses¶
We can add information about a route’s http method by inserting a Document
class
into the SpanRoute
we wish to document, and setting a DocInfo
object
to the method’s name.
DocRespInfo
is used to add information about each response code.
from spanserver import DocInfo, DocRespInfo
# set up the route
@grievous.route("/greet")
class Greet(SpanRoute):
async def on_get(self, req: Request, resp: Response):
"""
Provoke a greeting.
Grievous likes to shout things. He is a bold one.
"""
# route logic goes here.
class Document:
get = DocInfo(
responses={
201: DocRespInfo(
description="Created."
)
}
)
print(grievous.openapi)
Output:
info:
...
paths:
/greet:
get:
description: Grievous likes to shout things. He is a bold one.
responses:
'201':
description: Created.
default:
...
summary: Provoke a greeting
Schemas¶
Schemas are automatically added to the documentation when invoked through
SpanAPI.use_schema()
from spanserver import MimeType
from dataclasses import dataclass, asdict
from marshmallow import Schema, fields, post_load, pre_dump
@dataclass
class Enemy:
title: str
name: str
class EnemySchema(Schema):
"""Pesky Jedi scum who can add a lightsaber to Grievous' collection"""
title = fields.Str(required=True, description="Military Rank.")
name = fields.Str(required=True, description="How the enemy is called.")
@pre_dump
def convert_dataclass(self, data: Enemy, *, many: bool):
return asdict(data)
@post_load
def load_name(self, data: dict, *, many: bool):
return Enemy(**data)
# set up the route
@grievous.route("/quip")
class QuipRoute(SpanRoute):
@grievous.use_schema(req=EnemySchema(), resp=MimeType.TEXT)
async def on_post(self, req: Request, resp: Response):
"""Make a snide remark at an enemy."""
# route logic goes here.
print(grievous.openapi)
Output:
components:
schemas:
EnemyPostReq1:
properties:
name:
description: How the enemy is called.
type: string
title:
description: Military Rank.
type: string
required:
- name
- title
type: object
info:
...
paths:
/greet:
...
/quip:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/EnemyPostReq1'
responses:
'200':
content:
text/plain:
schema:
type: string
description: Ok.
default:
description: Error.
headers:
...
summary: Make a snide remark at an enemy.
tags:
- Enemies
tags:
- description: Pesky Jedi scum who can add a lightsaber to Grievous' collection.
name: Enemies
The following elements have been added:
'EnemyPostReq1'
has been registered undercomponents.schemas
.Schema field descriptions are pulled into the spec.
A reference to the schema has been placed under
post.requestBody.content.application/json.schema
The
'200'
response has been marked as being'text/plain'
content return with the corresponding schema. This comes from usingMimeType.TEXT
as the response schema for the route.An
'Enemies'
tag has been registered with a description from the Schema’s docstring, and added to the appropriate route’s
Schema names can be customized via the req_name=
and resp_name_
params of
SpanAPI.use_schema.()
Automatic names are generated with the following pattern:
{Schema Name Minus "Schema"}{HTTP Method}{Increasing Number}
Note
Schema spec rendering is handled via api_spec. Please see more details there.
Note
Grahamcracker is a dataclass / marshmallow library built for use with SpanServer that has a number of convenience features for documenting schema descriptions and fields directly in a dataclass.
Example Data¶
Example data can also be passed by declaring a Document
class in the
SpanRoute
.
@grievous.route("/quip")
class QuipRoute(SpanRoute):
@grievous.use_schema(req=EnemySchema(), resp=MimeType.TEXT)
async def on_post(self, req: Request, resp: Response):
"""Make a snide remark at an enemy."""
# route logic goes here.
class Document:
post = DocInfo(
req_example=Enemy("General", "Kenobi"),
responses={
201: DocRespInfo(
description="Created.",
example="General Kenobi, you are a bold one!"
)
}
)
print(grievous.openapi)
Output:
components:
...
info:
...
openapi: 3.0.0
paths:
/quip:
post:
requestBody:
content:
application/json:
example:
name: Kenobi
title: General
schema:
$ref: '#/components/schemas/EnemyPostReq2'
responses:
'201':
content:
text/plain:
example: General Kenobi, you are a bold one!
schema:
type: string
description: Created.
default:
...
tags:
- Enemies
tags:
- description: Pesky Jedi scum who can add a lightsaber to Grievous' collection.
name: Enemies
URL Parameters¶
Parameters declared in the route method’s signature with type annotations will be added to the openapi spec automatically.
from typing import Union
# set up the route
@grievous.route("/arm/{arm_id}/status")
class ArmStatusRoute(SpanRoute):
async def on_get(
self,
req: Request,
resp: Response,
*,
arm_id: Union[int, str]
):
# route logic goes here.
print(grievous.openapi)
Output:
components:
...
info:
...
openapi: 3.0.0
paths:
...
/arm/{arm_id}/status:
get:
parameters:
- in: path
name: arm_id
required: true
schema:
anyOf:
- type: integer
- type: string
responses:
...
...
Paging Parameters¶
When using SpanAPI.paged()
, paging url and response header params are
automatically added to the spec.
@grievous.route("/hands")
class QuipRoute(SpanRoute):
@grievous.paged(limit=4)
async def on_get(self, req: Request, resp: Response):
# route logic goes here.
print(grievous.openapi)
Output:
info:
...
openapi: 3.0.0
paths:
/hands:
get:
parameters:
- description: Index of first item to be returned in response body.
in: query
name: paging-offset
required: false
schema:
default: 0
type: integer
- description: Maximum number of items allowed in response body.
in: query
name: paging-limit
required: false
schema:
maximum: 4
type: integer
responses:
'200':
description: Ok.
headers:
paging-current-page:
description: Page number of item set in response body given current
limit-per-page.
required: true
schema:
type: integer
paging-limit:
description: Maximum number of items allowed in response body.
required: true
schema:
maximum: 4
type: integer
paging-next:
description: URL to next page.
required: true
schema:
type: string
paging-offset:
description: Index of first item returned in response body.
required: true
schema:
default: 0
type: integer
paging-previous:
description: URL to previous page.
required: true
schema:
type: string
paging-total-items:
description: Total number of items that match request.
required: true
schema:
type: integer
paging-total-pages:
description: Total number of pages that match request given current
limit-per-page.
required: true
schema:
type: integer
default:
description: Error.
headers:
error-code:
...
Paging params are not added to responses with error status codes.
Custom Parameters¶
Custom parameters can be added through the Document
class declaration.
from spanserver import ParamInfo, ParamTypes
@grievous.route("/minions")
class MinionCount(SpanRoute):
async def on_get(self, req: Request, resp: Response):
# route logic goes here.
class Document:
get = DocInfo(
req_params=[
ParamInfo(
param_type=ParamTypes.QUERY,
name="droid-model",
decode_types=[str],
description="Model of droid to get current count of.",
)
]
)
print(grievous.openapi)
Output:
info:
...
openapi: 3.0.0
paths:
/minions:
get:
parameters:
- description: Model of droid to get current count of.
in: query
name: droid-model
required: true
schema:
type: string
responses:
...
DocRespInfo
can also store response headers in its params
field.