A Python OpenAPI 3 Specification client and validator for Python 3.
This project is a fork of Dorthu/openapi3.
- implements …
- Swagger 2.0
- OpenAPI 3.0.3
- OpenAPI 3.1.0
- object parsing via pydantic
- request body model creation via pydantic
- pydantic compatible "format"-type coercion (e.g. datetime.interval)
- blocking and nonblocking (asyncio) interface via httpx
- tests with pytest
- providing access to methods and arguments via the sad smiley ._. interface
- api to modify description documents/requests/responses to adapt to non compliant services
This library also functions as an interactive client for arbitrary OpenAPI 3
specs. For example, using Linode's OpenAPI 3 Specification
_ for reference:
Unfortunately I do not have access to the Linode API to validate object creation
from aiopenapi3 import OpenAPI
url = "https://www.linode.com/docs/api/openapi.yaml"
api = await OpenAPI.load_async(url)
# call operations and receive result models
regions = await api._.getRegions()
from aiopenapi3 import OpenAPI
url = "https://www.linode.com/docs/api/openapi.yaml"
my_token = "Gae6aikaegainoor"
api = OpenAPI.load_sync(url)
# call operations and receive result models
regions = api._.getRegions()
pydantic is used for the models. https://pydantic-docs.helpmanual.io/usage/exporting_models/
from aiopenapi3 import OpenAPI
url = "https://www.linode.com/docs/api/openapi.yaml"
api = await OpenAPI.load_sync(url)
# call operations and receive result models
regions = await api._.getRegions()
regions.__fields_set__
{'results', 'page', 'pages', 'data'}
import json
print(json.dumps((list(filter(lambda x: 'eu-west' in x.id, regions.data))[0]).dict(), indent=2))
{
"id": "eu-west",
"country": "uk",
"capabilities": [
"Linodes",
"NodeBalancers",
"Block Storage",
"Kubernetes",
"Cloud Firewall"
],
"status": "ok",
"resolvers": {
"ipv4": "178.79.182.5,176.58.107.5,176.58.116.5,176.58.121.5,151.236.220.5,212.71.252.5,212.71.253.5,109.74.192.20,109.74.193.20,109.74.194.20",
"ipv6": "2a01:7e00::9,2a01:7e00::3,2a01:7e00::c,2a01:7e00::5,2a01:7e00::6,2a01:7e00::8,2a01:7e00::b,2a01:7e00::4,2a01:7e00::7,2a01:7e00::2"
}
}
discriminators are supported as well, but the linode api can't be used to show how to use them. look at aiopenapi3/tests/model_test.py test_model.
my_token = "Gae6aikaegainoor"
api.authenticate(personalAccessToken=my_token)
# call an operation that requires authentication
linodes = api._.getLinodeInstances()
HTTP basic authentication and HTTP digest authentication works like this:
# authenticate using a securityScheme defined in the spec's components.securitySchemes
# Tuple with (username, password) as second argument
api.authenticate(basicAuth=('username', 'password'))
Resetting authentication tokens:
api.authenticate(None)
# call an opertaion with parameters
linode = api._.getLinodeInstance(parameters={"linodeId": 123})
body = api._.createLinodeInstance.args()["data"].model({"region":"us-east", "type":"g6-standard-2"})
print(json.dumps(body.dict(), indent=2))
{
"image": null,
"root_pass": null,
"authorized_keys": null,
"authorized_users": null,
"stackscript_id": null,
"stackscript_data": null,
"booted": null,
"backup_id": null,
"backups_enabled": null,
"swap_size": null,
"type": "g6-standard-2",
"region": "us-east",
"label": null,
"tags": null,
"group": null,
"private_ip": null,
"interfaces": null
}
print(json.dumps(body.dict(exclude_unset=True), indent=2))
{
"type": "g6-standard-2",
"region": "us-east"
}
>>>
new_linode = api._.createLinodeInstance(data=body)
Installing with the extra [cli] or running thodule allows to validate specs:
aiopenapi3 -h
usage: aiopenapi3 [-h] [-C] [-D [YAML_DISABLE_TAG]] [-l] [-v] name
Swagger 2.0, OpenAPI 3.0, OpenAPI 3.1 validator
positional arguments:
name
optional arguments:
-h, --help show this help message and exit
-C, --yaml-compatibility
disables type coercion for yaml types such as datetime, bool …
-D [YAML_DISABLE_TAG], --yaml-disable-tag [YAML_DISABLE_TAG]
disable this tag from the YAML loader
-l, --yaml-list-tags list tags
-v, --verbose be verbose
The module can be run against a spec file to validate it like so::
python3 -m aiopenapi3 tests/fixtures/with-broken-links.yaml
6 validation errors for OpenAPISpec
paths -> /with-links -> get -> responses -> 200 -> links -> exampleWithBoth -> __root__
operationId and operationRef are mutually exclusive, only one of them is allowed (type=value_error.spec; message=operationId and operationRef are mutually exclusive, only one of them is allowed; element=None)
paths -> /with-links -> get -> responses -> 200 -> links -> exampleWithBoth -> $ref
field required (type=value_error.missing)
paths -> /with-links -> get -> responses -> 200 -> $ref
field required (type=value_error.missing)
paths -> /with-links-two -> get -> responses -> 200 -> links -> exampleWithNeither -> __root__
operationId and operationRef are mutually exclusive, one of them must be specified (type=value_error.spec; message=operationId and operationRef are mutually exclusive, one of them must be specified; element=None)
paths -> /with-links-two -> get -> responses -> 200 -> links -> exampleWithNeither -> $ref
field required (type=value_error.missing)
paths -> /with-links-two -> get -> responses -> 200 -> $ref
field required (type=value_error.missing)
In case the yaml not well formed, there are options to disable certain tags:
python -m aiopenapi3 -D tag:yaml.org,2002:timestamp -l -v linode.yaml
removing tag:yaml.org,2002:timestamp
tags:
tag:yaml.org,2002:bool
tag:yaml.org,2002:float
tag:yaml.org,2002:int
tag:yaml.org,2002:merge
tag:yaml.org,2002:null
tag:yaml.org,2002:value
tag:yaml.org,2002:yaml
OK
The description document may no be valid yaml. YAML type coercion can cause this.
>>> yaml.safe_load(str(datetime.datetime.now().date()))
datetime.date(2022, 1, 12)
>>> yaml.safe_load("name: on")
{'name': True}
>>> yaml.safe_load('12_24: "test"')
{1224: 'test'}
Those can be turned of using the yload yaml.Loader argument to the Loader.
import aiopenapi3.loader
OpenAPI.load…(…, loader=FileSystemLoader(pathlib.Path(dir), yload = aiopenapi3.loader.YAMLCompatibilityLoader))
All but these get disabled:
python -m aiopenapi3 -C -l -v linode.yaml
tags:
tag:yaml.org,2002:float
tag:yaml.org,2002:merge
tag:yaml.org,2002:null
tag:yaml.org,2002:yaml
In case the description document does not match the protocol, it may be required to alter the description, objects or data sent/received. The Plugin interface can be used to alter any of those. It can even be used to alter an invalid description document to be usable.
This project includes a test suite, run via pytest
. To run the test suite,
ensure that you've installed the dependencies and then run pytest
in the root
of this project.
PYTHONPATH=. pytest --cov=./ --cov-report=xml .