Advanced

init_model_factory

The init_model_factory interface is less user friendly but perhaps of interest to advanced users. It accepts the specification in dictionary format (so it has fewer dependencies than init_yaml and init_json) and does not construct a declarative base. It accepts the following parameters:

  • base: The SQLAlchemy declarative base as a keyword only argument. It is used to as the base class for all SQLAlchemy models.

  • spec: The OpenAPI specification as a dictionary as a keyword only argument.

  • models_filename: The name of the file where the SQLAlchemy models will be written as an optional keyword only argument.

Note

the define_all parameter has been removed and OpenAlchemy behaves as though it is set to True.

The return value is the model_factory as defined as part of the return value of init_yaml.

build_yaml

Used to build a package with the SQLAlchemy models (including type hints) based on a YAML OpenAPI specification which has been extended with any relevant OpenAlchemy extension properties.

The build_yaml interface requires the PyYAML library to be installed. The build_yaml interface accepts the following arguments:

  • spec_filename: The name of the OpenAPI specification file. The file must be a YAML file.

  • package_name: The name of the package to be produced. This will be

    1. the name of the package if it is uploaded to a package index and

    2. the name that is used to import the models after they have been installed).

  • dist_path: The directory to output the package files.

  • format_: Optionally, the format of the package to build:

    • PackageFormat.NONE: no package is built (default)

    • PackageFormat.SDIST: a sdist package is built

    • PackageFormat.WHEEL: a wheel package is built

    The formats can be combined with the bitwise operator “or” (|), for instance, building both sdist and wheel packages can be specified like this:

    format_=PackageFormat.SDIST|PackageFormat.WHEEL
    

    Warning

    In order to build a wheel distributable archive, the wheel package is necessary.

    It can be installed separately with pip install wheel, or with OpenAlchemy directly with pip install OpenAlchemy[wheel].

See also

Package Service

Service that creates pip installable models from your OpenAPI specification.

Models File

OpenAlchemy can optionally generate a file with all the SQLAlchemy models. Each model is constructed based on the OpenApi schema. The class inherits from the SQLAlchemy model defined on open_alchemy.models. The generated classes contain type information only, they do not provide any additional functionality on top of what the SQLAlchemy model provides. They are primarily used to enable IDE auto-complete and type hint support. The models can be used in the same way as the models that can be imported from open_alchemy.models and provide the full functionality of SQLAlchemy models.

The following is a sample file generated for the above example:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""Autogenerated SQLAlchemy models based on OpenAlchemy models."""
# pylint: disable=no-member,super-init-not-called,unused-argument

import typing

import sqlalchemy
from sqlalchemy import orm

from open_alchemy import models

Base = models.Base  # type: ignore


class _EmployeeDictBase(typing.TypedDict, total=True):
    """TypedDict for properties that are required."""

    name: str
    division: str


class EmployeeDict(_EmployeeDictBase, total=False):
    """TypedDict for properties that are not required."""

    id: int
    salary: typing.Optional[float]


class TEmployee(typing.Protocol):
    """
    SQLAlchemy model protocol.

    Person that works for a company.

    Attrs:
        id: Unique identifier for the employee.
        name: The name of the employee.
        division: The part of the company the employee works in.
        salary: The amount of money the employee is paid.

    """

    # SQLAlchemy properties
    __table__: sqlalchemy.Table
    __tablename__: str
    query: orm.Query

    # Model properties
    id: "sqlalchemy.Column[int]"
    name: "sqlalchemy.Column[str]"
    division: "sqlalchemy.Column[str]"
    salary: "sqlalchemy.Column[typing.Optional[float]]"

    def __init__(
        self,
        name: str,
        division: str,
        id: typing.Optional[int] = None,
        salary: typing.Optional[float] = None,
    ) -> None:
        """
        Construct.

        Args:
            id: Unique identifier for the employee.
            name: The name of the employee.
            division: The part of the company the employee works in.
            salary: The amount of money the employee is paid.

        """
        ...

    @classmethod
    def from_dict(
        cls,
        name: str,
        division: str,
        id: typing.Optional[int] = None,
        salary: typing.Optional[float] = None,
    ) -> "TEmployee":
        """
        Construct from a dictionary (eg. a POST payload).

        Args:
            id: Unique identifier for the employee.
            name: The name of the employee.
            division: The part of the company the employee works in.
            salary: The amount of money the employee is paid.

        Returns:
            Model instance based on the dictionary.

        """
        ...

    @classmethod
    def from_str(cls, value: str) -> "TEmployee":
        """
        Construct from a JSON string (eg. a POST payload).

        Returns:
            Model instance based on the JSON string.

        """
        ...

    def to_dict(self) -> EmployeeDict:
        """
        Convert to a dictionary (eg. to send back for a GET request).

        Returns:
            Dictionary based on the model instance.

        """
        ...

    def to_str(self) -> str:
        """
        Convert to a JSON string (eg. to send back for a GET request).

        Returns:
            JSON string based on the model instance.

        """
        ...


Employee: typing.Type[TEmployee] = models.Employee  # type: ignore

The following information is recorded in the models file:

  • The name and type of each property of a schema.

  • The from_dict and to_dict function signatures, including the type of the arguments and return values.

  • The from_str and to_str function signatures, including the type of the arguments and return values.

  • The properties created on instance objects due to any Backref.

  • Special SQLAlchemy properties for interacting with the database.

  • The object and property descriptions from the OpenAPI specification in the class and function docstrings.

Note

To be able to add relationships created by x-backrefs to the type annotations of the models file, the schema stored alongside a model, which is accessible at the _schema class variable (not a public interface so it should not be used or relied upon), will use the x-backrefs extension property to record the schema for all back references for the model. x-backrefs is not a public interface and should not be relied upon as it is subject to change.

It is worth noting that the auto-generated models cannot be imported directly. The generated models are added to the open_alchemy.models package by the init_* functions.

1
2
3
4
5
from open_alchemy import init_yaml
from open_alchemy import models

init_yaml('openapi.yml')
employee = models.Employee(name="David Andersson", division="Engineering")

To use the models in another project, refer to the build_yaml or build_json functions.

Model Utilities

There are a few common utility functions that are added to the models. The from_dict utility function constructs a model instance from a dictionary. The to_dict function converts a model instance to a dictionary.

from_dict

The from_dict function is available on all constructed models. It accepts a dictionary and constructs a model instance based on the dictionary. It is similar to Employee(**employee_dict) with a few advantages:

  • The dictionary based on which the model is constructed is checked against the schema used to define the model.

  • If the model includes a relationship, the relationship is constructed recursively.

For example:

>>> employee_dict = {
    "id": 1,
    "name": "David Andersson",
    "division": "engineering",
    "salary": 1000000,
}
>>> employee = Employee.from_dict(**employee_dict)
>>> employee.name
'David Andersson'

Note

To be able to support relationships, the schema stored alongside a model, which is accessible at the _schema class variable (not a public interface so it should not be used or relied upon), won’t store the actual schema for the referenced object. Instead, the object type is noted for the property alongside the x-de-$ref extension property which stores the name of the referenced model.

from_str

The from_str function is available on all constructed models. It accepts a JSON formatted string and constructs a model instance by de-serializing the JSON string and then using from_dict. For example:

>>> employee_str = '''{
    "id": 1,
    "name": "David Andersson",
    "division": "engineering",
    "salary": 1000000
}'''
>>> employee = Employee.from_str(employee_str)
>>> employee.name
'David Andersson'

to_dict

The to_dict function is available on all constructed models. It converts a model instance into a dictionary based on the schema that was used to define the model. If the model includes a relationship, the to_dict function is called recursively on the relationship.

For example:

>>> employee_dict = {
    "id": 1,
    "name": "David Andersson",
    "division": "engineering",
    "salary": 1000000,
}
>>> employee = Employee.from_dict(**employee_dict)
>>> employee.to_dict()
{'id': 1, 'name': 'David Andersson', 'division': 'engineering', 'salary': 1000000}

to_str

The to_str function is available on all constructed models. It converts a model instance into a JSON formatted string by serializing the output of to_dict.

For example:

>>> employee_str = '''{
    "id": 1,
    "name": "David Andersson",
    "division": "engineering",
    "salary": 1000000
}'''
>>> employee = Employee.from_str(employee_str)
>>> employee.to_str()
'{"id": 1, "name": "David Andersson", "division": "engineering", "salary": 1000000}'

__str__

It is possible to convert any model instance to a string using the str function. This is supported as there is a __str__ alias for the to_str function.

__repr__

Each model includes a __repr__ implementation to support calling repr in any model instance. The returned string is the source code required to construct an equivalent model instance.

For example:

>>> employee_dict = {
    "id": 1,
    "name": "David Andersson",
    "division": "engineering",
    "salary": 1000000,
}
>>> employee = Employee.from_dict(**employee_dict)
>>> repr(employee)
"open_alchemy.models.Employee(id=1, name='David Andersson', division='engineering', salary=1000000)"

Extension Property Prefix

OpenAlchemy currently supports 2 extension property prefixes. The shorter x- and the longer x-open-alchemy-. Both prefixes behave in the same way. The longer prefix is offered to avoid extension property name clashes with other tools.

For example, the tablename can be specified either using the x-tablename or the x-open-alchemy-tablename extension property.

How Does It Work?

Given a name for a schema, OpenAlchemy looks for that schema in the schemas section of the specification. The schema must have the x-tablename property which defines the name of the table. The schema is required to be an object. For each property of the schema, a column is generated for the table mapping OpenAPI types to equivalent SQLAlchemy types.

On top of the information in the OpenAPI specification, certain extension properties are used to define the database schema. The following specification defines the format and provides a description for each of the supported extension properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
{
  "x-backref": {
    "description": "Add a back reference to a relationship.",
    "type": "string"
  },
  "x-uselist": {
    "description": "Turn a many to one into a one to one relationship.",
    "type": "boolean"
  },
  "x-secondary": {
    "description": "Turn a one to many into a many to many relationship. The value of x-secondary is used as the name of the association table.",
    "type": "string"
  },
  "x-primary-key": {
    "description": "Make a column a primary key.",
    "type": "boolean"
  },
  "x-autoincrement": {
    "description": "Make an integer primary key column auto increment.",
    "type": "boolean"
  },
  "x-index": {
    "description": "Add index to a column.",
    "type": "boolean"
  },
  "x-composite-index": {
    "description": "Add composite index to a table.",
    "$ref": "#/CompositeIndex"
  },
  "x-unique": {
    "description": "Add unique constraint to a column.",
    "type": "boolean"
  },
  "x-composite-unique": {
    "description": "Add composite unique constraint to a table.",
    "$ref": "#/CompositeUnique"
  },
  "x-json": {
    "description": "Treat the property as a JSON object rather than a particular type.",
    "type": "boolean"
  },
  "x-foreign-key": {
    "description": "Add a foreign key constraint to a column. Must have the format \"<table name>.<column name>\".",
    "type": "string",
    "pattern": "^.+\\..+$"
  },
  "x-foreign-key-column": {
    "description": "Customize the column used for the foreign key constraint of a relationship.",
    "type": "string"
  },
  "x-foreign-key-kwargs": {
    "description": "Define kwargs to be passed to the foreign key constructor.",
    "type": "object",
    "additionalProperties": true
  },
  "x-server-default": {
    "description": "Get the server to calculate a default value.",
    "type": "string"
  },
  "x-tablename": {
    "description": "Define the name of a table.",
    "type": "string"
  },
  "x-schema-name": {
    "description": "Define the name of a schema the table is on.",
    "type": "string"
  },
  "x-inherits": {
    "description": "Define that a schema inherits from another schema or specify which schema to inherit from.",
    "oneOf": [{ "type": "string" }, { "type": "boolean" }]
  },
  "x-kwargs": {
    "description": "Define kwargs to be passed to a function based on the context.",
    "type": "object",
    "additionalProperties": true
  },
  "x-mixins": {
    "description": "The import path for a mixin class to be added as a parent for a model.",
    "$ref": "#/Mixins"
  },
  "x-backrefs": {
    "description": "INTERNAL USE ONLY: Note on a schema that a back reference was created.",
    "$ref": "#/BackRef"
  },
  "x-de-$ref": {
    "description": "INTERNAL USE ONLY: Track the name of a constructed model for a property referencing an object.",
    "type": "string"
  },
  "x-dict-ignore": {
    "description": "INTERNAL USE ONLY: Do not add to the schema record used by from_- and to_dict functions.",
    "type": "boolean"
  },
  "x-generated": {
    "description": "INTERNAL USE ONLY: The property is generated automatically (eg. auto incremented or has a default value) which is used to help determine whether the column is nullable.",
    "type": "boolean"
  }
}

To find out more about an extension property, go to the following section of the documentation:

property

documentation section

x-backref

Backref

x-uselist

One to One

x-secondary

Many to Many

x-primary-key

Primary Key

x-autoincrement

Auto Increment

x-index

Column Index

x-composite-index

Composite Index

x-unique

Column Unique Constraint

x-composite-unique

Composite Unique Constraint

x-foreign-key

Foreign Key Constraint

x-tablename

How Does It Work?

x-schema-name

Define Custom Schema name

x-inherits

x-inherits

x-foreign-key-colum

Custom Foreign Key

x-mixins

Mixin Classes

x-backrefs

Models File Note

x-de-$ref

from_dict Note

x-dict-ignore

One to Many Note

x-generated

Nullable Note

x-kwargs

x-foreign-key-kwargs

Foreign Key kwargs

The SQLAlchemy Base and any constructed database models are dynamically added to the models module that is available from OpenAlchemy.

Pylint

The auto-generated models file don’t play nice with the pylint rule no-member (E1101) because the models are generated in code which is not currently supported by pylint. To avoid spurious pylint errors add the following line to your .pylintrc file in the [IMPORTS] section:

generated-members=models.*