# dos [](https://pypi.python.org/pypi/dos/) []((https://www.python.org/downloads/))  [](https://github.com/pr/dos/blob/master/LICENSE)
## Introduction
dos is a Python package to make it easy to document and validate a Flask API. Write a single chunk of code to create
endpoints with both built in validation and automatically generated documentation. The documentation is Open API 3.0
(formerly known as Swagger) in JSON form.
## Installation
You can install the latest version of dos with pip.
```bash
pip install dos
```
## Hello World
> All of this code is found in the [pet_shop](https://github.com/pr/dos/tree/master/examples/tutorial/pet_shop) example.
> For a more substantive look at dos, please see [dos in depth](#dos-in-depth)
Let's look at the structure of a typical dos endpoint. The following code defines the `/dog/get` endpoint.
```python
from http import HTTPStatus
from dos.schema import ErrorFields
from pet_shop.model import DogFields
def handler():
# ... database query looking for the dog ...
if dog_found:
dog = {
"name": "Spot",
"breed": "Poodle"
}
return HTTPStatus.OK, dog
else:
return HTTPStatus.NOT_FOUND, {"message": "No dog by that name found!"}
def input_schema():
return DogFields().specialize(only=["name"])
def output_schema():
return {
HTTPStatus.OK: DogFields().all(),
HTTPStatus.NOT_FOUND: ErrorFields().all()
}
```
Each endpoint is made up of 3 critical components.
1. `handler()`
The handler defines the endpoint functionality. Adding to the database, calling another endpoint, it all happens here.
2. `input_schema()`
The input_schema defines what fields the endpoint expects. These are typically defined elsewhere and imported, but they
don't have to be.
3. `output_schema()`
The output_schema defines what fields the endpoint is allowed to expose. Critically, if the handler sets a field that is
not defined in the output_schema, that field will not be exposed by the API. Because endpoints can produce different HTTP
statuses, the output_schema is a dictionary where the keys are all the statuses produced by the endpoint.
---
The endpoints import fields typically defined in another file. Here is the DogFields class from above.
```python
from dos import prop
from dos.schema import Fields
class DogFields(Fields):
base_schema = {
"name": prop.String("The dog's name."),
"breed": prop.String("The dog's breed.")
}
def __init__(self):
super().__init__(self.base_schema)
```
Every Fields class needs to have a base_schema, a dictionary made up of dos props. Read more about props [here](#props).
The Field class gives additional functionality outlined [here](#the-field-class).
---
Now that we've defined an endpoint, we can create out flask app. It will look something like this:
```python
from dos.open_api import OpenAPI
from dos.flask_wrappers import wrap_validation, wrap_handler, wrap_route
from flask import Flask, jsonify, render_template
from pet_shop.api.dog import get as dog_get
def create_app():
app = Flask(__name__)
open_api = OpenAPI("Pet Shop API", "1.0")
handler_mapping = [
(dog_get, "/dog/get", "get"),
]
for module, path, http_method in handler_mapping:
handler = wrap_handler(module.__name__, module.handler)
handler = wrap_validation(handler, module)
wrap_route(app, handler, path, http_method)
open_api.document(module, path, http_method)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/docs")
def docs():
return render_template("docs.html")
@app.route("/open_api.json")
def open_api_endpoint():
return jsonify(open_api)
return app
```
This will create a flask app with the endpoint we just defined, as well as documenting it with Open API 3.0 JSON.
For more about the Flask Wrappers, please look [here](#flask-wrappers)
That's all there is to it. Once the general structure is set up, each additional endpoint should be relatively simple
to implement.
To run the full working example, please see [pet_shop](https://github.com/pr/dos/tree/master/examples/tutorial/pet_shop).
## dos in Depth
### Props
The foundation of dos is props. There are nine different prop types, 6 which are represented by Open API:
Name | Python Type | OpenAPI Representation | Additional Notes
--- | --- | --- | ---
Integer | `int` | Yes
Number | `int`, `float`, `decimal.Decimal` | Yes
Numeric | `int`, `float`, `decimal.Decimal`, `str` | No | The string must contain a valid number
String | `str` | Yes
DateTime | `str`, `arrow.Arrow` | No | The string must contain a valid arrow DateTime
Enum | `enum.Enum` | No
Boolean | `bool` | Yes
Object | `dict` | Yes
Array | `list` | Yes
Props are used to capture the structure of the inputs and outputs of endpoints.
Initializing a Prop is simple, and is always done in the context of a python dictionary
capturing the structure of the JSON.
```python
from dos import prop
base_schema = {
"name": prop.String(),
}
```
#### Customizing Props
Props take four optional arguments.
Description is a string explaining what the prop represents, and is displayed in the documentation.
```python
from dos import prop
base_schema = {
"name": prop.String(description="The dog's name."),
}
```
Required and nullable capture whether the prop is required and nullable. These are used for both validation and Open API.
```python
from dos import prop
base_schema = {
"name": prop.String(required=False, nullable=True),
}
```
All props have these three arguments, and a final one called validators.
#### Prop Validation
dos has a few validators built in as exemplars, but feel free to write your own Validators, specific to the domain your API is capturing.
All validators define `supported_prop_classes`, because not all validation is applicable to every prop.
(You wouldn't validate if an array was a Social Security Number!)
Using a Validator looks like this:
```python
from dos import prop
from dos.validators import ExactLength
base_schema = {
"name": prop.String("This string must be 8 characters long", validators=ExactLength(8)),
}
```
The validator itself looks like this:
```python
from dos import prop
from dos.validators import Validator
class ExactLength(Validator):
supported_prop_classes = [prop.String, prop.Number, prop.Numeric, prop.Integer]
def __init__(self, exact_length=None):
self.exact_length = exact_length
def validate_prop(self, prop_class, prop_value):
super().validate_prop(prop_class, prop_value)
if len(prop_value) != self.exact_length:
return (f"{prop_class.__name__} is not the correct length! The string \'{prop_value}\' is "
f"{len(prop_value)} characters long, not {self.exact_length}!")
return None
```
Every validator needs to define `supported_prop_classes` and a `validate_prop` function.
If you have a good one, feel free to submit a pull request.
#### Objects and Arrays
Objects and Arrays take additional arguments, due to their special nature.
Objects take their structure, looking something like this:
```python
from dos import prop
base_schema = {
"name": prop.Object(structure={
"name": prop.String("The object has a string in it"),
"boolean_field": prop.Boolean("And also a boolean")
}),
}
```
Structure is mandatory, and is a dictionary of props. Validation will look for dictionaries in the JSON that match the
outlined structure.
Array does a similar thing with the repeated_structure argument.
```python
from dos import prop
base_schema = {
"names": prop.Array(repeated_structure=prop.String("just a list of strings")),
}
```
You can even put these together, and have an array of objects!
```python
from dos import prop
base_schema = {
"names": prop.Array(repeated_structure=prop.String("just a list of strings")),
"array_of_objects": prop.Array(
repeated_structure=prop.Object(
structure={
"sub_string": prop.String("the string", required=True, nullable=False),
"required_one": prop.String(required=True, nullable=False)
}
),
description="A list of plans."
)
}
```
#### Prop Wrappers
Prop Wrappers are another way to capture what a JSON field expects. Currently, they are used for fields with multiple valid inputs.
Say a field can take either a string or a boolean. dos captures this idea with a prop wrapper.
```python
from dos import prop
from dos import prop_wrapper
base_schema = {
"boolean_or_string": prop_wrapper.OneOf([
prop.String(),
prop.Boolean()
]),
}
```
Critically, the OneOf prop wrapper is a just an array of props, meaning all the customization outlined above is still possible.
A convoluted example could be something like this:
```python
from dos import prop
from dos import prop_wrapper
from dos.validators import ExactLength
base_schema = {
"boolean_or_string": prop_wrapper.OneOf([
prop.String(validators=ExactLength(7)),
prop.Boolean(required=False, nullable=False)
]),
}
```
All of this is enforced and valid.
### The Field Class
Fields are a collection of Props and Prop Wrappers that make up an object. They are a way to give semantically meaningful names to
collections of Props and Prop Wrappers, and capture the object oriented nature of some APIs.
```python
from dos import prop
from dos.schema import Fields
class DogFields(Fields):
base_schema = {
"name": prop.String("The dog's name."),
"breed": prop.String("The dog's breed.")
}
def __init__(self):
super().__init__(self.base_schema)
```
All fields need a base_schema, which is where the Props and Prop Wrappers that make up the collection are stored.
#### Field Customization
The Fields class gives many opportunities for customization of input and output schema.
```python
from http import HTTPStatus
from pet_shop.model import DogFields
def input_schema():
return DogFields().specialize(only=["name"])
def output_schema():
return {
HTTPStatus.OK: DogFields().all(),
}
```
`specialize` allows picking and choosing props, while `all` will use every prop defined by the Field.
`specialize` means any Field object can be customized to it's application, by overriding fields on specific props,
only using some fields, and/or excluding other fields.
```python
from pet_shop.model import DogFields
def input_schema():
return DogFields().specialize(overrides={
"breed": {
"required": False,
},
}, exclude=["name"])
```
Thus, it is possible to capture objects coming in and out of the API, while tailoring them to specific use cases.
### Flask Wrappers
Flask Wrappers are how the modules that define API endpoints are integrated with Flask.
```python
from dos.flask_wrappers import wrap_validation, wrap_handler, wrap_route
from flask import Flask
from pet_shop.api.dog import get as dog_get
app = Flask(__name__)
handler_mapping = [
(dog_get, "/dog/get", "get"),
]
for module, path, http_method in handler_mapping:
handler = wrap_handler(module.__name__, module.handler)
handler = wrap_validation(handler, module)
wrap_route(app, handler, path, http_method)
```
The handler_mapping is a list of every endpoint that needs to be documented and implemented with dos. The module,
paired with a string representation of its path and the HTTP method it supports, is then processed with flask wrappers.
`wrap_handler` takes the module and extracts the handler. `wrap_validation` parses the input_schema and the output_schema
and adds validation to the handler to enforce their constraints. Finally, `wrap_route` adds the endpoint to the flask app
itself.
### Open API
In same place you create your Flask app, it is easy to also create Open API documentation for that app.
```python
from dos.open_api import OpenAPI
from flask import Flask
def create_app():
app = Flask(__name__)
open_api = OpenAPI("Your API Name", "1.0")
```
You can customize the Open API with contact information, a logo, and tags.
```python
from dos.open_api import OpenAPI
open_api = OpenAPI("Your API Name", "1.0")
open_api.add_contact("Pet Shop Dev Team", "https://www.example.com", "pet_shop@example.com")
open_api.add_logo("/static/pet_shop.png", "#7D9FC3", "Pet Shop", "/")
open_api.add_tag(
"introduction",
"Welcome! This is the documentation for the API.",
)
```
Tags are important for organizing endpoints. If you have a `dog/create` and a `/dog/delete` endpoint, create a
dog tag to group them together.
```python
from dos.open_api import OpenAPI
open_api = OpenAPI("Your API Name", "1.0")
open_api.add_tag(
"dog",
"Endpoints for interacting with dogs.",
)
```
If you want to add text to the top of the Open API JSON, so others know how it was made, use the
disclaimer functionality.
```python
from dos.open_api import OpenAPI
open_api = OpenAPI("Your API Name", "1.0")
open_api.add_disclaimer(
"This file is generated automatically. Do not edit it directly! Edit "
"the input_schema and output_schema of the endpoint you are changing."
)
```
Finally, to take the `input_schema` and `output_schema` defined in each endpoint module and make Open API out of it,
call document.
```python
from dos.open_api import OpenAPI
from pet_shop.api.dog import get as dog_get
open_api = OpenAPI("Your API Name", "1.0")
handler_mapping = [
(dog_get, "/dog/get", "get"),
]
for module, path, http_method in handler_mapping:
open_api.document(module, path, http_method)
```
The same code you used for validation will also be used for documentation!
# Acknowledgements
Developed at [Capital Rx](https://cap-rx.com/) with team input and assistance, open sourced with
permission from [Ryan Kelley, CTO](https://github.com/f0rk).