Skip to content

Commit c9f80f6

Browse files
authored
Merge pull request #243 from jdkandersson/enhancement/236-extension-property-cleanup
Enhancement/236 extension property cleanup
2 parents bd510d0 + f5efc91 commit c9f80f6

49 files changed

Lines changed: 1039 additions & 652 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add support for namespaced `x-open-alchemy-` prefix on top of the shorter
13+
`x-` prefix for extension properties. [#236]
14+
1015
## [v2.0.2] - 2020-12-19
1116

1217
### Changed
@@ -483,3 +488,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
483488
[#198]: https://github.com/jdkandersson/OpenAlchemy/issues/198
484489
[#201]: https://github.com/jdkandersson/OpenAlchemy/issues/201
485490
[#202]: https://github.com/jdkandersson/OpenAlchemy/issues/202
491+
[#236]: https://github.com/jdkandersson/OpenAlchemy/issues/236

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ An example API has been defined using connexion and Flask here:
137137
- `date-time`,
138138
- generic JSON data,
139139
- `$ref` references for columns and models,
140-
- remote `$ref` to other files on the same file system (_not supported on Windows_),
140+
- remote `$ref` to other files on the same file system
141+
(_not supported on Windows_),
141142
- remote `$ref` to other files at a URL,
142143
- primary keys,
143144
- auto incrementing,
@@ -163,10 +164,13 @@ An example API has been defined using connexion and Flask here:
163164
- `__str__` model methods to support the python `str` function,
164165
- `__repr__` model methods to support the python `repr` function,
165166
- `to_dict` model methods to convert instances to dictionaries,
166-
- `readOnly` and `writeOnly` for influence the conversion to and from dictionaries,
167+
- `readOnly` and `writeOnly` for influence the conversion to and from
168+
dictionaries,
167169
- exposing created models under `open_alchemy.models` removing the need for
168-
`models.py` files and
169-
- ability to mix in arbitrary classes into a model.
170+
`models.py` files,
171+
- ability to mix in arbitrary classes into a model and
172+
- can use the short `x-` prefix or a namespaced `x-open-alchemy-` prefix for
173+
extension properties.
170174

171175
## Contributing
172176

docs/source/examples/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Examples
88
connexion
99
alembic
1010
simple
11+
namespaced
1112
default
1213
server_default
1314
read_only
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
Namespaced
2+
==========
3+
4+
OpenAlchemy supports namespaced extension properties. The default is to prefix
5+
extension properties with :samp:`x-`. To avoid clashes with other tools, the
6+
:samp:`x-open-alchemy-` prefix is also supported:
7+
8+
.. literalinclude:: ../../../examples/namespaced/example-spec.yml
9+
:language: yaml
10+
:linenos:
11+
12+
The behavior of OpenAlchemy is identical.
13+
14+
The following example models file makes use of the OpenAPI specification to
15+
define the SQLAlchemy models:
16+
17+
.. literalinclude:: ../../../examples/namespaced/models.py
18+
:language: python
19+
:linenos:
20+
21+
This models file instructs OpenAlchemy to construct the SQLAlchemy models
22+
equivalent to the following traditional SQLAlchemy models.py file:
23+
24+
.. literalinclude:: ../../../examples/namespaced/models_traditional.py
25+
:language: python
26+
:linenos:
27+
28+
OpenAlchemy also generates a fully type hinted version of the generated
29+
SQLAlchemy models:
30+
31+
.. literalinclude:: ../../../examples/namespaced/models_auto.py
32+
:language: python
33+
:linenos:

docs/source/index.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,17 @@ alembic is supported. The following instructions show how to get started:
374374
.. literalinclude:: ../../examples/alembic/readme.md
375375
:language: md
376376

377+
Extension Property Prefix
378+
-------------------------
379+
380+
OpenAlchemy currently supports 2 extension property prefixes. The shorter
381+
:samp:`x-` and the longer :samp:`x-open-alchemy-`. Both prefixes behave in the
382+
same way. The longer prefix is offered to avoid extension property name clashes
383+
with other tools.
384+
385+
For example, the tablename can be specified either using the
386+
:samp:`x-tablename` or the :samp:`x-open-alchemy-tablename` extension property.
387+
377388
.. _how-does-it-work:
378389

379390
How Does It Work?
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
openapi: "3.0.0"
2+
3+
info:
4+
title: Test Schema
5+
description: API to illustrate OpenAlchemy using namespaced extension properties (x-open-alchemy* instead of x-*).
6+
version: "0.1"
7+
8+
paths:
9+
/employee:
10+
get:
11+
summary: Used to retrieve all employees.
12+
responses:
13+
200:
14+
description: Return all employees from the database.
15+
content:
16+
application/json:
17+
schema:
18+
type: array
19+
items:
20+
"$ref": "#/components/schemas/Employee"
21+
22+
components:
23+
schemas:
24+
Employee:
25+
description: Person that works for a company.
26+
type: object
27+
x-open-alchemy-tablename: employee
28+
properties:
29+
id:
30+
type: integer
31+
description: Unique identifier for the employee.
32+
example: 0
33+
x-open-alchemy-primary-key: true
34+
x-open-alchemy-autoincrement: true
35+
name:
36+
type: string
37+
description: The name of the employee.
38+
example: David Andersson
39+
x-open-alchemy-index: true
40+
division:
41+
type: string
42+
description: The part of the company the employee works in.
43+
example: Engineering
44+
x-open-alchemy-index: true
45+
salary:
46+
type: number
47+
description: The amount of money the employee is paid.
48+
example: 1000000.00
49+
required:
50+
- name
51+
- division

examples/namespaced/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from open_alchemy import init_yaml
2+
3+
init_yaml("example-spec.yml", models_filename="models_auto.py")

examples/namespaced/models_auto.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""Autogenerated SQLAlchemy models based on OpenAlchemy models."""
2+
# pylint: disable=no-member,super-init-not-called,unused-argument
3+
4+
import typing
5+
6+
import sqlalchemy
7+
from sqlalchemy import orm
8+
9+
from open_alchemy import models
10+
11+
Base = models.Base # type: ignore
12+
13+
14+
class _EmployeeDictBase(typing.TypedDict, total=True):
15+
"""TypedDict for properties that are required."""
16+
17+
name: str
18+
division: str
19+
20+
21+
class EmployeeDict(_EmployeeDictBase, total=False):
22+
"""TypedDict for properties that are not required."""
23+
24+
id: int
25+
salary: typing.Optional[float]
26+
27+
28+
class TEmployee(typing.Protocol):
29+
"""
30+
SQLAlchemy model protocol.
31+
32+
Person that works for a company.
33+
34+
Attrs:
35+
id: Unique identifier for the employee.
36+
name: The name of the employee.
37+
division: The part of the company the employee works in.
38+
salary: The amount of money the employee is paid.
39+
40+
"""
41+
42+
# SQLAlchemy properties
43+
__table__: sqlalchemy.Table
44+
__tablename__: str
45+
query: orm.Query
46+
47+
# Model properties
48+
id: "sqlalchemy.Column[int]"
49+
name: "sqlalchemy.Column[str]"
50+
division: "sqlalchemy.Column[str]"
51+
salary: "sqlalchemy.Column[typing.Optional[float]]"
52+
53+
def __init__(
54+
self,
55+
name: str,
56+
division: str,
57+
id: typing.Optional[int] = None,
58+
salary: typing.Optional[float] = None,
59+
) -> None:
60+
"""
61+
Construct.
62+
63+
Args:
64+
id: Unique identifier for the employee.
65+
name: The name of the employee.
66+
division: The part of the company the employee works in.
67+
salary: The amount of money the employee is paid.
68+
69+
"""
70+
...
71+
72+
@classmethod
73+
def from_dict(
74+
cls,
75+
name: str,
76+
division: str,
77+
id: typing.Optional[int] = None,
78+
salary: typing.Optional[float] = None,
79+
) -> "TEmployee":
80+
"""
81+
Construct from a dictionary (eg. a POST payload).
82+
83+
Args:
84+
id: Unique identifier for the employee.
85+
name: The name of the employee.
86+
division: The part of the company the employee works in.
87+
salary: The amount of money the employee is paid.
88+
89+
Returns:
90+
Model instance based on the dictionary.
91+
92+
"""
93+
...
94+
95+
@classmethod
96+
def from_str(cls, value: str) -> "TEmployee":
97+
"""
98+
Construct from a JSON string (eg. a POST payload).
99+
100+
Returns:
101+
Model instance based on the JSON string.
102+
103+
"""
104+
...
105+
106+
def to_dict(self) -> EmployeeDict:
107+
"""
108+
Convert to a dictionary (eg. to send back for a GET request).
109+
110+
Returns:
111+
Dictionary based on the model instance.
112+
113+
"""
114+
...
115+
116+
def to_str(self) -> str:
117+
"""
118+
Convert to a JSON string (eg. to send back for a GET request).
119+
120+
Returns:
121+
JSON string based on the model instance.
122+
123+
"""
124+
...
125+
126+
127+
Employee: typing.Type[TEmployee] = models.Employee # type: ignore
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import sqlalchemy as sa
2+
from sqlalchemy.ext.declarative import declarative_base
3+
4+
Base = declarative_base()
5+
6+
7+
class Employee(Base):
8+
"""Person that works for a company."""
9+
10+
__tablename__ = "employee"
11+
id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
12+
name = sa.Column(sa.String, index=True)
13+
division = sa.Column(sa.String, index=True)
14+
salary = sa.Column(sa.Float)

open_alchemy/helpers/all_of.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,14 @@ def _merge(
5050
merged_sub_schema = _merge(ref_schema, schemas, skip_name)
5151

5252
# Capturing required arrays
53-
merged_required = merged_schema.get("required")
54-
sub_required = merged_sub_schema.get("required")
53+
merged_required = merged_schema.get(types.OpenApiProperties.REQUIRED)
54+
sub_required = merged_sub_schema.get(types.OpenApiProperties.REQUIRED)
5555
# Capturing properties
56-
merged_properties = merged_schema.get("properties")
57-
sub_properties = merged_sub_schema.get("properties")
56+
merged_properties = merged_schema.get(types.OpenApiProperties.PROPERTIES)
57+
sub_properties = merged_sub_schema.get(types.OpenApiProperties.PROPERTIES)
5858
# Capturing backrefs
59-
merged_backrefs = merged_schema.get("x-backrefs")
60-
sub_backrefs = merged_sub_schema.get("x-backrefs")
59+
merged_backrefs = merged_schema.get(types.ExtensionProperties.BACKREFS)
60+
sub_backrefs = merged_sub_schema.get(types.ExtensionProperties.BACKREFS)
6161

6262
# Combining sub into merged specification
6363
merged_schema = {**merged_schema, **merged_sub_schema}
@@ -66,16 +66,22 @@ def _merge(
6666
if merged_required is not None and sub_required is not None:
6767
# Both have a required array, need to merge them together
6868
required_set = set(merged_required).union(sub_required)
69-
merged_schema["required"] = list(required_set)
69+
merged_schema[types.OpenApiProperties.REQUIRED] = list(required_set)
7070

7171
# Checking whether properties was present on both specs
7272
if merged_properties is not None and sub_properties is not None:
7373
# Both have properties, merge properties
74-
merged_schema["properties"] = {**merged_properties, **sub_properties}
74+
merged_schema[types.OpenApiProperties.PROPERTIES] = {
75+
**merged_properties,
76+
**sub_properties,
77+
}
7578

7679
# Checking whether backrefs was present on both specs
7780
if merged_backrefs is not None and sub_backrefs is not None:
7881
# Both have backrefs, merge backrefs
79-
merged_schema["x-backrefs"] = {**merged_backrefs, **sub_backrefs}
82+
merged_schema[types.ExtensionProperties.BACKREFS] = {
83+
**merged_backrefs,
84+
**sub_backrefs,
85+
}
8086

8187
return merged_schema

0 commit comments

Comments
 (0)