.. role:: python(code) :language: python Advanced ======== .. _init-model-factory: :samp:`init_model_factory` -------------------------- The :samp:`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 :ref:`init-yaml` and :ref:`init-json`) and does not construct a declarative base. It accepts the following parameters: * :samp:`base`: The SQLAlchemy declarative base as a keyword only argument. It is used to as the base class for all SQLAlchemy models. * :samp:`spec`: The OpenAPI specification as a dictionary as a keyword only argument. * :samp:`models_filename`: The name of the file where the SQLAlchemy models will be written as an optional keyword only argument. .. note:: the :samp:`define_all` parameter has been removed and OpenAlchemy behaves as though it is set to :samp:`True`. The return value is the :samp:`model_factory` as defined as part of the return value of :ref:`init-yaml`. .. _build-yaml: :samp:`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 :samp:`build_yaml` interface requires the :samp:`PyYAML` library to be installed. The :samp:`build_yaml` interface accepts the following arguments: * :samp:`spec_filename`: The name of the OpenAPI specification file. The file must be a YAML file. * :samp:`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). * :samp:`dist_path`: The directory to output the package files. * :samp:`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: .. code-block:: python 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]``. .. seealso:: :ref:`package-service` Service that creates pip installable models from your OpenAPI specification. .. _models-file: Models File ----------- :samp:`OpenAlchemy` can optionally generate a file with all the :samp:`SQLAlchemy` models. Each model is constructed based on the :samp:`OpenApi` schema. The class inherits from the :samp:`SQLAlchemy` model defined on :samp:`open_alchemy.models`. The generated classes contain type information only, they do not provide any additional functionality on top of what the :samp:`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 :samp:`open_alchemy.models` and provide the full functionality of :samp:`SQLAlchemy` models. The following is a sample file generated for the above example: .. literalinclude:: ../../examples/simple/models_auto.py :language: python :linenos: The following information is recorded in the models file: * The name and type of each property of a schema. * The :ref:`from-dict` and :ref:`to-dict` function signatures, including the type of the arguments and return values. * The :ref:`from-str` and :ref:`to-str` function signatures, including the type of the arguments and return values. * The properties created on instance objects due to any :ref:`backref`. * Special SQLAlchemy properties for interacting with the database. * The object and property descriptions from the OpenAPI specification in the class and function docstrings. .. _backrefs: .. note:: To be able to add relationships created by :samp:`x-backrefs` to the type annotations of the models file, the schema stored alongside a model, which is accessible at the :samp:`_schema` class variable (not a public interface so it should not be used or relied upon), will use the :samp:`x-backrefs` extension property to record the schema for all back references for the model. :samp:`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 :samp:`open_alchemy.models` package by the :samp:`init_*` functions. .. code-block:: python :linenos: 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 :ref:`build-yaml` or :ref:`build-json` functions. .. _model-utilities: Model Utilities --------------- There are a few common utility functions that are added to the models. The :ref:`from-dict` utility function constructs a model instance from a dictionary. The :ref:`to-dict` function converts a model instance to a dictionary. .. _from-dict: :samp:`from_dict` ^^^^^^^^^^^^^^^^^ The :samp:`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 :python:`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' .. _de-ref: .. note:: To be able to support relationships, the schema stored alongside a model, which is accessible at the :samp:`_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 :samp:`object` type is noted for the property alongside the :samp:`x-de-$ref` extension property which stores the name of the referenced model. .. _from-str: :samp:`from_str` ^^^^^^^^^^^^^^^^ The :samp:`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 :ref:`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: :samp:`to_dict` ^^^^^^^^^^^^^^^ The :samp:`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 :samp:`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} .. seealso:: :ref:`child-parent-reference` .. _to-str: :samp:`to_str` ^^^^^^^^^^^^^^ The :samp:`to_str` function is available on all constructed models. It converts a model instance into a JSON formatted string by serializing the output of :ref:`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: :samp:`__str__` ^^^^^^^^^^^^^^^ It is possible to convert any model instance to a string using the :python:`str` function. This is supported as there is a :samp:`__str__` alias for the :ref:`to-str` function. .. _repr: :samp:`__repr__` ^^^^^^^^^^^^^^^^ Each model includes a :samp:`__repr__` implementation to support calling :python:`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 :samp:`x-` and the longer :samp:`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 :samp:`x-tablename` or the :samp:`x-open-alchemy-tablename` extension property. .. _how-does-it-work: How Does It Work? ----------------- Given a name for a schema, :samp:`OpenAlchemy` looks for that schema in the schemas section of the specification. The schema must have the :samp:`x-tablename` property which defines the name of the table. The schema is required to be an :samp:`object`. For each :samp:`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. .. literalinclude:: ../../open_alchemy/helpers/ext_prop/extension-schemas.json :language: json :linenos: To find out more about an extension property, go to the following section of the documentation: +------------------------------+----------------------------------------------------+ | property | documentation section | +==============================+====================================================+ | :samp:`x-backref` | :ref:`backref` | +------------------------------+----------------------------------------------------+ | :samp:`x-uselist` | :ref:`one-to-one` | +------------------------------+----------------------------------------------------+ | :samp:`x-secondary` | :ref:`many-to-many` | +------------------------------+----------------------------------------------------+ | :samp:`x-primary-key` | :ref:`primary-key` | +------------------------------+----------------------------------------------------+ | :samp:`x-autoincrement` | :ref:`autoincrement` | +------------------------------+----------------------------------------------------+ | :samp:`x-index` | :ref:`column-index` | +------------------------------+----------------------------------------------------+ | :samp:`x-composite-index` | :ref:`composite-index` | +------------------------------+----------------------------------------------------+ | :samp:`x-unique` | :ref:`column-unique` | +------------------------------+----------------------------------------------------+ | :samp:`x-composite-unique` | :ref:`composite-unique` | +------------------------------+----------------------------------------------------+ | :samp:`x-foreign-key` | :ref:`foreign-key` | +------------------------------+----------------------------------------------------+ | :samp:`x-tablename` | :ref:`how-does-it-work` | +------------------------------+----------------------------------------------------+ | :samp:`x-schema-name` | :ref:`schema-name` | +------------------------------+----------------------------------------------------+ | :samp:`x-inherits` | :ref:`x-inherits` | +------------------------------+----------------------------------------------------+ | :samp:`x-foreign-key-colum` | :ref:`custom-foreign-key` | +------------------------------+----------------------------------------------------+ | :samp:`x-mixins` | :ref:`mixins` | +------------------------------+----------------------------------------------------+ | :samp:`x-backrefs` | :ref:`Models File Note ` | +------------------------------+----------------------------------------------------+ | :samp:`x-de-$ref` | :ref:`from_dict Note ` | +------------------------------+----------------------------------------------------+ | :samp:`x-dict-ignore` | :ref:`One to Many Note ` | +------------------------------+----------------------------------------------------+ | :samp:`x-generated` | :ref:`Nullable Note ` | +------------------------------+----------------------------------------------------+ | :samp:`x-kwargs` | * :ref:`Relationship kwargs ` | | | * :ref:`Model kwargs ` | | | * :ref:`Column kwargs ` | +------------------------------+----------------------------------------------------+ | :samp:`x-foreign-key-kwargs` | :ref:`Foreign Key kwargs ` | +------------------------------+----------------------------------------------------+ The SQLAlchemy :samp:`Base` and any constructed database models are dynamically added to the :samp:`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.*