Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Add check enforcing unique `x-tablename` values.
- Add check enforcing unique `x-secondary` values.
- Add custom association schemas validation
- Add support for custom association tables
- Add `openalchemy` CLI with a first subcommand to build a Python package from a
specification file. [#201]
- Add a CLI subcommand to regenerate models. [#202]
- Add check enforcing unique `x-tablename` values. [#189]
- Add check enforcing unique `x-secondary` values. [#189]
- Add custom association schemas validation [#189]
- Add support for custom association tables [#189]
- Add `openalchemy` CLI with a first sub command to build a Python package
from a specification file. [#201]
- Add a CLI sub command to regenerate models. [#202]
- Add support for database default values using `x-server-default`. [#196]

### Changed

- Change the association table to no longer be noted on the models based on
the `x-secondary` value and instead be noted based on converting the
`x-secondary` value from snake_case to PascalCase. Name clashes are avoided
by pre-pending `Autogen` as many times as required.
by pre-pending `Autogen` as many times as required. [#189]
- Change the association table to no longer be constructed as a table and
instead to be constructed as another model.
- Refactor column factory to use the schemas artifacts
- Refactor model factory to use the schemas artifacts
instead to be constructed as another model. [#189]
- Refactor column factory to use the schemas artifacts [#196]
- Refactor model factory to use the schemas artifacts [#196]

### Fixed

- Fix bug where the association table defined for `many-to-many` relationships
did not make the foreign key columns referencing the two sides of the
relationship primary keys. _This may require a database migration if alembic
was used to generate the database schema._
was used to generate the database schema._ [#189]
- Fix bug where some properties were incorrectly picked from a reference even
though they existed locally (only impacts relationship properties where, for
example, `x-secondary` was defined both on the relationship property in
`allOf` and on the referenced model).
`allOf` and on the referenced model). [#189]

### Removed

- Remove `define_all` parameter for `init_model_factory`, `init_json` and
`init_yaml`. OpenAlchemy now behaves as though `define_all` is set to
`True`. _This means that a pure model reference (a schema with only the
`$ref` key) can no longer be used to change the name of a model._
`$ref` key) can no longer be used to change the name of a model._ [#189]

## [1.6.0] - 2020-10-10

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ An example API has been defined using connexion and Flask here:
- composite unique constraints,
- column nullability,
- foreign keys,
- default values for columns,
- default values for columns (both application and database side),
- many to one relationships,
- one to one relationships,
- one to many relationships,
Expand Down
1 change: 1 addition & 0 deletions docs/source/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Examples
alembic
simple
default
server_default
read_only
write_only
relationship/index
Expand Down
44 changes: 44 additions & 0 deletions docs/source/examples/server_default.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Server Default
==============

OpenAlchemy supports defining a default value generated by the database
through the :samp:`x-server-default` extension property. It is similar to the
OpenAPI :samp:`default` property except that the default value is calculated
by the database rather than the application.

.. seealso::

:ref:`server-default`
OpenAlchemy documentation for the server default value.

:ref:`default`
OpenAlchemy documentation for the default value.

`SQLAlchemy Server Default <https://docs.sqlalchemy.org/en/13/core/metadata.html#sqlalchemy.schema.Column.params.server_default>`_
Documentation for the SQLAlchemy server default.

The following example defines a default value for the :samp:`name` property of
:samp:`Employee`:

.. literalinclude:: ../../../examples/server_default/example-spec.yml
:language: yaml
:linenos:

The following file uses OpenAlchemy to generate the SQLAlchemy models:

.. literalinclude:: ../../../examples/server_default/models.py
:language: python
:linenos:

The SQLAlchemy models generated by OpenAlchemy are equivalent to the following
traditional models file:

.. literalinclude:: ../../../examples/server_default/models_traditional.py
:language: python
:linenos:

OpenAlchemy will generate the following typed models:

.. literalinclude:: ../../../examples/server_default/models_auto.py
:language: python
:linenos:
39 changes: 39 additions & 0 deletions docs/source/technical_details/column_modifiers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,45 @@ OpenAlchemy.
`SQLAlchemy "Scalar Default" <https://docs.sqlalchemy.org/en/13/core/defaults.html#scalar-defaults>`_
Documentation for the scalar default value in SQLAlchemy.

.. _server-default:

Server Default
--------------

To add a default value for a column to be generated by the database, use the
:samp:`x-server-default` extension property:

.. code-block:: yaml
:linenos:

Employee:
type: object
x-tablename: employee
properties:
id:
type: integer
name:
type: string
x-server-default: Unknown

The default value is added to the column constructor using the "Server
Default" in SQLAlchemy. The following property types support a server default
value (including all their formats supported by OpenAlchemy):

* :samp:`integer`,
* :samp:`number`,
* :samp:`string` and
* :samp:`boolean`.

Adding a server default to a :samp:`object` or :samp:`array` type is not valid
in OpenAlchemy. Server default is also not supported by any property that sets
:samp:`x-json` to :samp:`true`.

.. seealso::

`SQLAlchemy Server Default <https://docs.sqlalchemy.org/en/13/core/metadata.html#sqlalchemy.schema.Column.params.server_default>`_
Documentation for the SQLAlchemy server default.

.. _read-only:

readOnly
Expand Down
38 changes: 38 additions & 0 deletions examples/server_default/example-spec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
openapi: "3.0.0"

info:
title: Test Schema
description: API to illustrate the OpenAlchemy default feature.
version: "0.1"

paths:
/employee:
get:
summary: Used to retrieve all employees.
responses:
200:
description: Return all employees from the database.
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/Employee"

components:
schemas:
Employee:
description: Person that works for a company.
type: object
x-tablename: employee
properties:
id:
type: integer
description: Unique identifier for the employee.
example: 0
x-primary-key: true
name:
type: string
description: The name of the employee.
example: David Andersson
x-server-default: Unknown
3 changes: 3 additions & 0 deletions examples/server_default/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from open_alchemy import init_yaml

init_yaml("example-spec.yml", models_filename="models_auto.py")
104 changes: 104 additions & 0 deletions examples/server_default/models_auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""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 EmployeeDict(typing.TypedDict, total=False):
"""TypedDict for properties that are not required."""

id: typing.Optional[int]
name: str


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.

"""

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

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

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

Args:
id: Unique identifier for the employee.
name: The name of the employee.

"""
...

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

Args:
id: Unique identifier for the employee.
name: The name of the employee.

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
12 changes: 12 additions & 0 deletions examples/server_default/models_traditional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Employee(Base):
"""Person that works for a company."""

__tablename__ = "employee"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String, server_default=sa.text("Unknown"))
10 changes: 9 additions & 1 deletion open_alchemy/facades/sqlalchemy/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import typing

import sqlalchemy

from ... import exceptions
from ... import helpers
from ... import types as oa_types
Expand Down Expand Up @@ -37,11 +39,16 @@ def construct(*, artifacts: oa_types.SimplePropertyArtifacts) -> types.Column:
format_=artifacts.open_api.format,
)

# Calculate server default
server_default = None
if artifacts.extension.server_default is not None:
server_default = sqlalchemy.text(artifacts.extension.server_default)

# Calculate nullable
nullable = helpers.calculate_nullable(
nullable=artifacts.open_api.nullable,
generated=artifacts.extension.autoincrement is True,
defaulted=default is not None,
defaulted=default is not None or artifacts.extension.server_default is not None,
required=artifacts.required,
)

Expand All @@ -64,6 +71,7 @@ def construct(*, artifacts: oa_types.SimplePropertyArtifacts) -> types.Column:
foreign_key,
nullable=nullable,
default=default,
server_default=server_default,
**opt_kwargs,
**kwargs,
)
Expand Down
4 changes: 4 additions & 0 deletions open_alchemy/helpers/ext_prop/extension-schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"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"
Expand Down
Loading