middle-schema

PyPI - Status PyPI Package latest release Supported versions Travis-CI Build Status AppVeyor Build Status Documentation Status Coverage Status Codacy Grade Packages status

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.

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 a dict);
  • components: the components created for the given model (as a dict);

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.

Reference

middle-schema

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:

  1. Fork middle-schema (look for the “Fork” button).

  2. Clone your fork locally:

    git clone git@github.com:your_name_here/middle-schema.git
    
  3. Create a branch for local development:

    git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  4. When you’re done making changes, run all the checks, doc builder and spell checker with tox one command:

    tox
    
  5. 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
    
  6. 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:

  1. Include passing tests (run tox) [1].
  2. Update documentation when there’s new API, functionality etc.
  3. Add a note to CHANGELOG.rst about the changes.
  4. 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