middle-schema
¶
Translate your middle model declarations to OpenAPI, JSONSchema or any other schema you need!
In a nutshell¶
>>> import enum
>>> import json
>>> import typing as t
>>> import middle
>>> from middle_schema.openapi import parse
>>> @enum.unique
... class PlatformEnum(str, enum.Enum):
... XBOX1 = "XBOX1"
... PLAYSTATION4 = "PLAYSTATION4"
... PC = "PC"
>>> @enum.unique
... class LanguageEnum(enum.IntEnum):
... ENGLISH = 1
... JAPANESE = 2
... SPANISH = 3
... GERMAN = 4
... PORTUGUESE = 5
>>> @enum.unique
... class CityRegionEnum(str, enum.Enum):
... TROPICAL = "TROPICAL"
... TEMPERATE = "TEMPERATE"
... BOREAL = "BOREAL"
>>> class City(middle.Model):
... __description__ = "One awesome city built"
... name = middle.field(type=str, description="The city name")
... region = middle.field(
... default=CityRegionEnum.TEMPERATE,
... type=CityRegionEnum,
... description="The region this city is located",
... )
>>> class Player(middle.Model):
... nickname = middle.field(
... type=str, description="The nickname of the player over the internet"
... )
... youtube_channel = middle.field(
... type=str, description="The YouTube channel of the player", default=None
... )
>>> class Game(middle.Model):
... __description__ = "An electronic game model"
... name = middle.field(type=str, description="The name of the game")
... platform = middle.field(
... type=PlatformEnum, description="Which platform it runs on"
... )
... score = middle.field(
... type=float,
... description="The average score of the game",
... minimum=0,
... maximum=10,
... multiple_of=0.1,
... )
... resolution_tested = middle.field(
... type=str,
... description="The resolution which the game was tested",
... pattern="^\d+x\d+$",
... )
... genre = middle.field(
... type=t.List[str],
... description="One or more genres this game is part of",
... min_items=1,
... unique_items=True,
... )
... rating = middle.field(
... type=t.Dict[str, float],
... description="Ratings given on specialized websites",
... min_properties=3,
... )
... players = middle.field(
... type=t.Set[str],
... description="Some of the notorious players of this game",
... )
... language = middle.field(
... type=LanguageEnum, description="The main language of the game"
... )
... awesome_city = middle.field(type=City)
... remarkable_resources = middle.field(
... type=t.Union[Player, City],
... description="Some remarkable resources of this game over the internet",
... )
>>> api = parse(Game)
>>> json.dumps(api.specification, indent=4, sort_keys=True)
{
"description": "An electronic game model",
"properties": {
"awesome_city": {
"description": "One awesome city built",
"properties": {
"name": {
"description": "The city name",
"type": "string"
},
"region": {
"choices": [
"TROPICAL",
"TEMPERATE",
"BOREAL"
],
"description": "The region this city is located",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"genre": {
"description": "One or more genres this game is part of",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array",
"uniqueItems": true
},
"language": {
"choices": [
1,
2,
3,
4,
5
],
"description": "The main language of the game",
"format": "int64",
"type": "integer"
},
"name": {
"description": "The name of the game",
"type": "string"
},
"platform": {
"choices": [
"XBOX1",
"PLAYSTATION4",
"PC"
],
"description": "Which platform it runs on",
"type": "string"
},
"players": {
"description": "Some of the notorious players of this game",
"items": {
"properties": {
"nickname": {
"description": "The nickname of the player over the internet",
"type": "string"
},
"youtube_channel": {
"description": "The YouTube channel of the player",
"type": "string"
}
},
"required": [
"nickname"
],
"type": "object"
},
"type": "array"
},
"rating": {
"additionalProperties": {
"format": "double",
"type": "number"
},
"description": "Ratings given on specialized websites",
"minProperties": 3,
"type": "object"
},
"remarkable_resources": {
"anyOf": [
{
"properties": {
"nickname": {
"description": "The nickname of the player over the internet",
"type": "string"
},
"youtube_channel": {
"description": "The YouTube channel of the player",
"type": "string"
}
},
"required": [
"nickname"
],
"type": "object"
},
{
"description": "One awesome city built",
"properties": {
"name": {
"description": "The city name",
"type": "string"
},
"region": {
"choices": [
"TROPICAL",
"TEMPERATE",
"BOREAL"
],
"description": "The region this city is located",
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
}
],
"description": "Some remarkable resources of this game over the internet"
},
"resolution_tested": {
"description": "The resolution which the game was tested",
"pattern": "^\\d+x\\d+$",
"type": "string"
},
"score": {
"description": "The average score of the game",
"format": "double",
"maximum": 10,
"minimum": 0,
"multipleOf": 0.1,
"type": "number"
}
},
"required": [
"name",
"platform",
"score",
"resolution_tested",
"genre",
"rating",
"players",
"language",
"awesome_city",
"remarkable_resources"
],
"type": "object"
}
Warning
IMPORTANT: middle
and middle-schema
are in very early stages of development! Use with caution and be aware that some functionalities and APIs may change between versions until they’re out of alpha.
Documentation¶
License¶
middle-schema
is a free software distributed under the MIT license.
Installing¶
To install middle-schema
, use pip
(or your favorite Python package manager), like:
pip install middle-schema
And you’re ready to the next step. Hooray!
Warning
At this time, middle-schema
is only available for Python 3.5+ (and probably will be).
Using middle-schema
¶
For now, middle-schema
only supports the generation of OpenAPI 3.0 schemas for models based on middle. It should be used to generate schemas for documentation and/or client generation, independently if you’re going to use it as reference or integrate with a real world framework or API.
OpenAPI 3.0¶
To generate OpenAPI 3.0 compliant schemas (and components), you should import middle_schema.openapi
to generate a OpenAPI
instance, that contains two attributes:
specification
: the actual specification of the model given (as adict
);components
: the components created for the given model (as adict
);
Attention
Both attributes can have different outputs given changes in the configuration, as can be seen in the next topic.
Configuration¶
middle-schema
has two configuration options regarding the schema and components generation that can be applied to recursive models or enum classes, mostly to switch between transforming them into components or leave them inline.
openapi_enum_as_component
¶
Default: True
(boolean)
With this option enabled, all enum types will be generated as components and will end up inside the components
attribute of your OpenAPI
instance, with only references (as {"$ref": "#/components/schema/MyEnumName"}
) inside the specification.
>>> import enum
>>> import json
>>> import middle
>>> from middle_schema.openapi import parse
>>> @enum.unique
... class TestIntEnum(enum.IntEnum):
... TEST_1 = 1
... TEST_2 = 2
... TEST_3 = 3
>>> class TestModel(middle.Model):
... some_enum = middle.field(
... type=TestIntEnum, description="Some test enumeration"
... )
>>> api = parse(TestModel)
>>> json.dumps(api.specification, indent=4)
{
"$ref": "#/components/schemas/TestModel"
}
>>> json.dumps(api.components, indent=4)
{
"TestIntEnum": {
"type": "integer",
"format": "int64",
"choices": [
1,
2,
3
]
},
"TestModel": {
"type": "object",
"properties": {
"some_enum": {
"$ref": "#/components/schemas/TestIntEnum",
"description": "Some test enumeration"
}
},
"required": [
"some_enum"
]
}
}
>>> middle.config.openapi_enum_as_component = False
>>> api = parse(TestModel)
>>> json.dumps(api.specification, indent=4)
{
"$ref": "#/components/schemas/TestModel"
}
>>> json.dumps(api.components, indent=4)
{
"TestModel": {
"type": "object",
"properties": {
"some_enum": {
"type": "integer",
"format": "int64",
"description": "Some test enumeration",
"choices": [
1,
2,
3
]
}
},
"required": [
"some_enum"
]
}
}
openapi_model_as_component
¶
Default: True
(boolean)
With this option enabled, all middle.Model
subclasses will be generated as components and will end up inside the components
attribute of your OpenAPI
instance, with only references (as {"$ref": "#/components/schema/AnotherModel"}
) inside the specification.
>>> import json
>>> import middle
>>> from middle_schema.openapi import parse
>>> class InnerModel(middle.Model):
... name = middle.field(
... type=str, min_length=3, description="The person name"
... )
... age = middle.field(type=int, minimum=18, description="The person age")
>>> class TestModel(middle.Model):
... person = middle.field(
... type=InnerModel, description="The person to access this resource"
... )
... active = middle.field(
... type=bool, description="If the resource is active"
... )
>>> api = parse(TestModel)
>>> json.dumps(api.specification, indent=4)
{
"$ref": "#/components/schemas/TestModel"
}
>>> json.dumps(api.components, indent=4)
{
"InnerModel": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 3,
"description": "The person name"
},
"age": {
"type": "integer",
"format": "int64",
"minimum": 18,
"description": "The person age"
}
},
"required": [
"name",
"age"
],
"description": "The person to access this resource"
},
"TestModel": {
"type": "object",
"properties": {
"person": {
"$ref": "#/components/schemas/InnerModel"
},
"active": {
"type": "boolean",
"description": "If the resource is active"
}
},
"required": [
"person",
"active"
]
}
}
>>> middle.config.openapi_model_as_component = False
>>> api = parse(TestModel)
>>> json.dumps(api.specification, indent=4)
{
"type": "object",
"properties": {
"person": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 3,
"description": "The person name"
},
"age": {
"type": "integer",
"format": "int64",
"minimum": 18,
"description": "The person age"
}
},
"required": [
"name",
"age"
],
"description": "The person to access this resource"
},
"active": {
"type": "boolean",
"description": "If the resource is active"
}
},
"required": [
"person",
"active"
]
}
>>> json.dumps(api.components, indent=4)
{}
Attention
Every middle.Model
object is intended to be generated as a component, that’s why the specification (when the config key openapi_model_as_component
is True
) ends up being just a $ref
to a component and, being False
, would generate all models and inner models inline, as one.
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
Bug reports¶
When reporting a bug please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Documentation improvements¶
middle-schema could always use more documentation, whether as part of the official middle-schema docs, in docstrings, or even on the web in blog posts, articles, and such.
Feature requests and feedback¶
The best way to send feedback is to file an issue at https://github.com/vltr/middle-schema/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that code contributions are welcome :)
Development¶
To set up middle-schema
for local development:
Fork middle-schema (look for the “Fork” button).
Clone your fork locally:
git clone git@github.com:your_name_here/middle-schema.git
Create a branch for local development:
git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
When you’re done making changes, run all the checks, doc builder and spell checker with tox one command:
tox
Commit your changes and push your branch to GitHub:
git add . git commit -m "Your detailed description of your changes." git push origin name-of-your-bugfix-or-feature
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
If you need some code review or feedback while you’re developing the code just make the pull request.
For merging, you should:
- Include passing tests (run
tox
) [1]. - Update documentation when there’s new API, functionality etc.
- Add a note to
CHANGELOG.rst
about the changes. - Add yourself to
AUTHORS.rst
.
[1] | If you don’t have all the necessary python versions available locally you can rely on Travis - it will run the tests for each change you add in the pull request. It will be slower though … |
Tips¶
To run a subset of tests:
tox -e envname -- pytest -k test_myfeature
To run all the test environments in parallel (you need to pip install detox
):
detox
Changelog¶
v0.2.0 on 2018-08-01¶
- Small refactoring on the Skeleton parser;
- OpenAPI component and schema generation of
middle
models; - 99%+ of code coverage.
v0.1.0 on 2018-07-26¶
- First release on PyPI. Not stable.
Authors¶
- Richard Kuesters - https://vltr.github.io/