diff --git a/.gitignore b/.gitignore
index db29281..03d23a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -93,6 +93,7 @@ celerybeat-schedule
# virtualenv
.venv
venv/
+.py35
.python38
.py38
.python37
diff --git a/.travis.yml b/.travis.yml
index 8cc748c..0a333f2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,6 +9,284 @@ env:
matrix:
include:
+## PYTHON 3.10
+## SIMPLEJSON
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson30
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson31
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson32
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson33
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson34
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson35
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson36
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson37
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson38
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson39
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson310
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson311
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson312
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson313
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson314
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson315
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson316
+# PYYAML
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml42b2-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml54-simplejson316
+#### SQLALCHEMY
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml313-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy10-pyyaml42b2-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy11-pyyaml313-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy12-pyyaml313-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy12-pyyaml42b1-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy12-pyyaml42b2-simplejson316
+ - python: '3.10'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python310-sqlalchemy12-pyyaml54-simplejson316
+## PYTHON 3.9
+## SIMPLEJSON
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson30
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson31
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson32
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson33
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson34
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson35
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson36
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson37
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson38
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson39
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson310
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson311
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson312
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson313
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson314
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson315
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson316
+# PYYAML
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml42b2-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml54-simplejson316
+#### SQLALCHEMY
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml313-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy10-pyyaml42b2-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy11-pyyaml313-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy12-pyyaml313-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy12-pyyaml42b1-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy12-pyyaml42b2-simplejson316
+ - python: '3.9'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python39-sqlalchemy12-pyyaml54-simplejson316
## PYTHON 3.8
## SIMPLEJSON
- python: '3.8'
@@ -107,6 +385,11 @@ matrix:
sudo: true
env:
- TOXENV=.python38-sqlalchemy10-pyyaml42b2-simplejson316
+ - python: '3.8'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python38-sqlalchemy10-pyyaml54-simplejson316
#### SQLALCHEMY
- python: '3.8'
dist: bionic
@@ -138,6 +421,11 @@ matrix:
sudo: true
env:
- TOXENV=.python38-sqlalchemy12-pyyaml42b2-simplejson316
+ - python: '3.8'
+ dist: bionic
+ sudo: true
+ env:
+ - TOXENV=.python38-sqlalchemy12-pyyaml54-simplejson316
## PYTHON 3.7
## SIMPLEJSON
- python: '3.7'
@@ -236,6 +524,11 @@ matrix:
sudo: true
env:
- TOXENV=.python37-sqlalchemy10-pyyaml42b2-simplejson316
+ - python: '3.7'
+ dist: xenial
+ sudo: true
+ env:
+ - TOXENV=.python37-sqlalchemy10-pyyaml54-simplejson316
#### SQLALCHEMY
- python: '3.7'
dist: xenial
@@ -267,6 +560,11 @@ matrix:
sudo: true
env:
- TOXENV=.python37-sqlalchemy12-pyyaml42b2-simplejson316
+ - python: '3.7'
+ dist: xenial
+ sudo: true
+ env:
+ - TOXENV=.python37-sqlalchemy12-pyyaml54-simplejson316
## PYTHON 3.6
## SIMPLEJSON
- python: '3.6'
diff --git a/CHANGES.rst b/CHANGES.rst
index 2a36f77..4fe57af 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,43 @@
-----------
+Release 0.8.0
+=========================================
+
+.. image:: https://travis-ci.com/insightindustry/sqlathanor.svg?branch=v.0.8.0
+ :target: https://travis-ci.com/insightindustry/sqlathanor
+ :alt: Build Status (Travis CI)
+
+.. image:: https://codecov.io/gh/insightindustry/sqlathanor/branch/v.0.8.0/graph/badge.svg
+ :target: https://codecov.io/gh/insightindustry/sqlathanor
+ :alt: Code Coverage Status (Codecov)
+
+.. image:: https://readthedocs.org/projects/sqlathanor/badge/?version=v.0.8.0
+ :target: http://sqlathanor.readthedocs.io/en/latest/?badge=v.0.8.0
+ :alt: Documentation Status (ReadTheDocs)
+
+New Features
+-----------------
+
+* #99: Added `Pydantic `_ support. This includes:
+
+ * the ability to generate **SQLAthanor** model classes from Pydantic models
+ (``generate_model_from_pydantic()``)
+ * the ability to generate ``Table`` objects from Pydantic models
+ (``Table.from_pydantic()``)
+ * the ability to create ``AttributeConfiguration`` instances from fields in a Pydantic
+ model (``AttributeConfiguration.from_pydantic_model()``)
+ * updates to documentation to reflect new functionality
+
+Other Changes
+------------------
+
+* Updated PyYAML requirement to v.5.4 to address security vulnerability.
+* #100: Fixed missing documentation.
+* Cleaned up ``requirements.txt`` to reduce dependencies.
+* Minor bug fixes.
+
+-----------
+
Release 0.7.0
=========================================
diff --git a/README.rst b/README.rst
index 17d4a3b..6edf7a1 100644
--- a/README.rst
+++ b/README.rst
@@ -29,6 +29,20 @@ SQLAthanor
:target: http://sqlathanor.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status (ReadTheDocs)
+ * - `v.0.8 `_
+ -
+ .. image:: https://travis-ci.com/insightindustry/sqlathanor.svg?branch=v.0.8.0
+ :target: https://travis-ci.com/insightindustry/sqlathanor
+ :alt: Build Status (Travis CI)
+
+ .. image:: https://codecov.io/gh/insightindustry/sqlathanor/branch/v.0.8.0/graph/badge.svg
+ :target: https://codecov.io/gh/insightindustry/sqlathanor
+ :alt: Code Coverage Status (Codecov)
+
+ .. image:: https://readthedocs.org/projects/sqlathanor/badge/?version=v.0.8.0
+ :target: http://sqlathanor.readthedocs.io/en/latest/?badge=v.0.8.0
+ :alt: Documentation Status (ReadTheDocs)
+
* - `v.0.7 `_
-
.. image:: https://travis-ci.com/insightindustry/sqlathanor.svg?branch=v.0.7.0
@@ -148,7 +162,7 @@ easy-to-use record serialization/de-serialization with support for:
* JSON
* CSV
* YAML
- * Python dict
+ * Python ``dict``
The library works as a drop-in extension - change one line of existing code, and
it should just work. Furthermore, it has been extensively tested on Python 2.7,
@@ -183,11 +197,11 @@ Dependencies
* - Python 3.x
- Python 2.7
* - | * `SQLAlchemy v.0.9 `_ or higher
- | * `PyYAML v3.10 `_ or higher
+ | * `PyYAML v5.4 `_ or higher
| * `simplejson v3.0 `_ or higher
| * `Validator-Collection v1.4.0 `_ or higher
- | * `SQLAlchemy v.0.9 `_ or higher
- | * `PyYAML v3.10 `_ or higher
+ | * `PyYAML v5.4 `_ or higher
| * `simplejson v3.0 `_ or higher
| * `Validator-Collection v1.4.0 `_ or higher
@@ -266,8 +280,10 @@ Key SQLAthanor Features
* Customize the validation used when de-serializing particular columns to match
your needs.
* Works with Declarative Reflection and the SQLAlchemy Automap extension.
-* Programmatically generate Declarative Base Models from serialized data.
-* Programmatically generate SQLAlchemy ``Table`` objects from serialized data.
+* Programmatically generate Declarative Base Models from serialized data or Pydantic
+ models.
+* Programmatically generate SQLAlchemy ``Table`` objects from serialized data or Pydantic
+ models.
**SQLAthanor** vs Alternatives
diff --git a/docs/_dependencies.rst b/docs/_dependencies.rst
index 66ac6ab..1440c61 100644
--- a/docs/_dependencies.rst
+++ b/docs/_dependencies.rst
@@ -3,13 +3,13 @@
.. tab:: Python 3.x
* `SQLAlchemy v.0.9 `_ or higher
- * `PyYAML v3.10 `_ or higher
+ * `PyYAML v5.4 `_ or higher
* `simplejson v3.0 `_ or higher
* `Validator-Collection v1.4.0 `_ or higher
.. tab:: Python 2.x
* `SQLAlchemy v.0.9 `_ or higher
- * `PyYAML v3.10 `_ or higher
+ * `PyYAML v5.4 `_ or higher
* `simplejson v3.0 `_ or higher
* `Validator-Collection v1.4.0 `_ or higher
diff --git a/docs/_import_sqlathanor.rst b/docs/_import_sqlathanor.rst
index 581cb6a..d5024f3 100644
--- a/docs/_import_sqlathanor.rst
+++ b/docs/_import_sqlathanor.rst
@@ -164,3 +164,6 @@ The table below shows how `SQLAlchemy`_ classes and functions map to their
.. code-block:: python
from sqlathanor.automap import automap_base
+
+.. _SQLAlchemy: http://www.sqlalchemy.org
+.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/2.3/
diff --git a/docs/_unit_tests_code_coverage.rst b/docs/_unit_tests_code_coverage.rst
index a2bca8c..eedc1f6 100644
--- a/docs/_unit_tests_code_coverage.rst
+++ b/docs/_unit_tests_code_coverage.rst
@@ -18,20 +18,33 @@
:target: http://sqlathanor.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status (ReadTheDocs)
-* - `v.0.7 `_
- -
- .. image:: https://travis-ci.com/insightindustry/sqlathanor.svg?branch=v.0.7.0
- :target: https://travis-ci.com/insightindustry/sqlathanor
- :alt: Build Status (Travis CI)
+ * - `v.0.8 `_
+ -
+ .. image:: https://travis-ci.com/insightindustry/sqlathanor.svg?branch=v.0.8.0
+ :target: https://travis-ci.com/insightindustry/sqlathanor
+ :alt: Build Status (Travis CI)
+
+ .. image:: https://codecov.io/gh/insightindustry/sqlathanor/branch/v.0.8.0/graph/badge.svg
+ :target: https://codecov.io/gh/insightindustry/sqlathanor
+ :alt: Code Coverage Status (Codecov)
+
+ .. image:: https://readthedocs.org/projects/sqlathanor/badge/?version=v.0.8.0
+ :target: http://sqlathanor.readthedocs.io/en/latest/?badge=v.0.8.0
+ :alt: Documentation Status (ReadTheDocs)
- .. image:: https://codecov.io/gh/insightindustry/sqlathanor/branch/v.0.7.0/graph/badge.svg
- :target: https://codecov.io/gh/insightindustry/sqlathanor
- :alt: Code Coverage Status (Codecov)
+ * - `v.0.7 `_
+ -
+ .. image:: https://travis-ci.com/insightindustry/sqlathanor.svg?branch=v.0.7.0
+ :target: https://travis-ci.com/insightindustry/sqlathanor
+ :alt: Build Status (Travis CI)
- .. image:: https://readthedocs.org/projects/sqlathanor/badge/?version=v.0.7.0
- :target: http://sqlathanor.readthedocs.io/en/latest/?badge=v.0.7.0
- :alt: Documentation Status (ReadTheDocs)
+ .. image:: https://codecov.io/gh/insightindustry/sqlathanor/branch/v.0.7.0/graph/badge.svg
+ :target: https://codecov.io/gh/insightindustry/sqlathanor
+ :alt: Code Coverage Status (Codecov)
+ .. image:: https://readthedocs.org/projects/sqlathanor/badge/?version=v.0.7.0
+ :target: http://sqlathanor.readthedocs.io/en/latest/?badge=v.0.7.0
+ :alt: Documentation Status (ReadTheDocs)
* - `v.0.6 `_
-
diff --git a/docs/_versus_alternatives.rst b/docs/_versus_alternatives.rst
index d49e837..ddcba7a 100644
--- a/docs/_versus_alternatives.rst
+++ b/docs/_versus_alternatives.rst
@@ -28,6 +28,72 @@ it might be helpful to compare **SQLAthanor** to some commonly-used alternatives
find that I never really roll my own serialization/de-serialization approach
when working `SQLAlchemy`_ models any more.
+ .. tab:: Pydantic
+
+ .. tip::
+
+ Because `Pydantic`_ is growing in popularity, we have decided to integrate `Pydantic`_
+ support within **SQLAthanor**.
+
+ Using
+ :func:`generate_model_from_pydantic() `,
+ you can now programmatically generate a **SQLAthanor**
+ :class:`BaseModel ` from your
+ :term:`Pydantic models `.
+
+ This allows you to only maintain *one* representation of your data model (the
+ `Pydantic`_ one), while still being able to use SQLAthanor's rich serialization /
+ de-serialization configuration functionality.
+
+ `Pydantic`_ is an amazing object parsing library that leverages native Python typing
+ to provide simple syntax and rich functionality. While I have philosophical quibbles
+ about some of its API semantics and architectural choices, I cannot deny that it is
+ elegant, extremely performant, and all around excellent.
+
+ Since `FastAPI`_, one of the fastest-growing web application frameworks in the Python
+ ecosystem is tightly coupled with `Pydantic`_, it has gained significant ground within
+ the community.
+
+ However, when compared to **SQLAthanor** it has a number of architectural limitations:
+
+ While `Pydantic`_ has excellent serialization and deserialization functionality to
+ JSON, it is extremely limited with its serialization/deserialization support for other
+ common data formats like CSV or YAML.
+
+ Second, by its design `Pydantic`_ forces you to maintain **multiple** representations
+ of your data model. On the one hand, you will need your `SQLAlchemy`_ ORM
+ representation, but then you will *also* need one or more `Pydantic`_ models that will
+ be used to serialize/de-serialize your model instances.
+
+ Third, by its design, `Pydantic`_ tends to lead to significant amounts of duplicate
+ code, maintaining similar-but-not-quite-identical versions of your data models (with
+ one `Pydantic`_ schema for each context in which you might serialize/de-serialize your
+ data model).
+
+ Fourth, its API semantics can get extremely complicated when trying to use it as a
+ true serialization/de-serialization library.
+
+ While `Pydantic`_ has made efforts to integrate ORM support into its API, that
+ functionality is relatively limited in its current form.
+
+ .. tip::
+
+ **When to use it?**
+
+ `Pydantic`_ is easy to use when building simple web applications due to its
+ reliance on native Python typing. The need to maintain multiple representations
+ of your data models is a trivial burden with small applications or relatively
+ simple data models.
+
+ `Pydantic`_ is also practically required when building applications using the
+ excellent `FastAPI`_ framework.
+
+ So given these two things, we recommend using `Pydantic`_ *in combination* with
+ **SQLAthanor** to get the best of both words: native Python typing for validation
+ against your Python model (via `Pydantic`_) with rich configurable
+ serialization/de-serialization logic (via **SQLAthanor**), all integrated into
+ the underlying `SQLAlchemy`_ ORM.
+
.. tab:: Marshmallow
The `Marshmallow`_ library and its `Marshmallow-SQLAlchemy`_ extension are
@@ -161,6 +227,9 @@ it might be helpful to compare **SQLAthanor** to some commonly-used alternatives
.. _Marshmallow: https://marshmallow.readthedocs.io/en/3.0/
.. _Marshmallow-SQLAlchemy: https://marshmallow-sqlalchemy.readthedocs.io/en/latest/
+.. _Pydantic: https://pydantic-docs.helpmanual.io/
+.. _FastAPI: https://fastapi.tiangolo.com/
.. _Colander: https://docs.pylonsproject.org/projects/colander/en/latest/
.. _ColanderAlchemy: https://colanderalchemy.readthedocs.io/en/latest/
.. _pandas: http://pandas.pydata.org/
+.. _SQLAlchemy: http://www.sqlalchemy.org
diff --git a/docs/api.rst b/docs/api.rst
index 146828b..bcc5b4b 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -79,6 +79,13 @@ generate_model_from_dict()
----------------------------------------
+generate_model_from_pydantic()
+-------------------------------------
+
+.. autofunction:: generate_model_from_pydantic
+
+--------------------------------------------------------
+
.. module:: sqlathanor.schema
Schema
@@ -197,6 +204,8 @@ Table
.. automethod:: Table.from_yaml
+ .. automethod:: Table.from_pydantic
+
----------------------------
.. module:: sqlathanor.attributes
@@ -220,6 +229,8 @@ AttributeConfiguration
.. automethod:: AttributeConfiguration.__init__
+ .. automethod:: from_pydantic_model
+
----------------------
validate_serialization_config()
diff --git a/docs/conf.py b/docs/conf.py
index c567ae7..c4447d4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -227,10 +227,11 @@
intersphinx_mapping = {
'python': ('https://docs.python.org/3.6', None),
'python27': ('https://docs.python.org/2.7', None),
- 'sqlalchemy': ('http://docs.sqlalchemy.org/en/latest/', None),
+ 'sqlalchemy': ('http://docs.sqlalchemy.org/en/13/', None),
'simplejson': ('http://simplejson.readthedocs.io/en/latest/', None),
'validator-collection': ('http://validator-collection.readthedocs.io/en/latest/', None),
'flask_sqlalchemy': ('http://flask-sqlalchemy.pocoo.org/2.3/', None),
+ #'pydantic': ('https://pydantic-docs.helpmanual.io/', None),
}
# -- Options for todo extension ----------------------------------------------
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 1aeeaaf..43de009 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -31,6 +31,22 @@ Glossary
with fields (columns) separated by a delimiter character (typically a comma
``,`` or pipe ``|``).
+ Configuration Set
+ A named set of attribute configurations which determine which :term:`model class`
+ attributes get :term:`serialized ` /
+ :term:`de-serialized ` under given circumstances (when indicating
+ the configuration set during the serialization / de-serialization operation).
+
+ .. tip::
+
+ Think of a configuration set as a set of "rules" that determine what gets
+ processed when serializing or de-serializing a :term:`model class`.
+
+ .. note::
+
+ It is possible for a single :term:`model class` to have many different configuration
+ sets, so long as each set has a unique name.
+
Declarative Configuration
A way of configuring :term:`serialization` and :term:`de-serialization`
for particular :term:`model attributes ` when defining
@@ -187,6 +203,27 @@ Glossary
module from the standard Python library, or an outside pickling library like
`dill `_.
+ Pydantic Model
+ A Pydantic Model is a representation of a class which contains data and some
+ attributes that inherits from the
+ :class:`pydantic.BaseModel ` class. This model is
+ used by the `Pydantic `_ library to either
+ validate the typing of data upon deserialization or to serialize data to appropriate
+ types when needed.
+
+ By definition, each Pydantic model is self-contained (though they may inherit across
+ models). Pydantic models are inherently reliant on Python's native typing support,
+ relying on type hints and annotations to provide the canonical instructions against
+ which to validate or based on which to serialize.
+
+ .. seealso::
+
+ * :doc:`SQLAthanor and Pydantic `
+ * :func:`generate_model_from_pydantic() `
+ * :meth:`Table.from_pydantic() `
+ * :meth:`AttributeConfiguration.from_pydantic_model() `
+
+
Relationship
A connection between two database tables or their corresponding
:term:`model classes ` defined using a foreign key constraint.
diff --git a/docs/index.rst b/docs/index.rst
index 854a24c..964cae1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -42,6 +42,7 @@ SQLAthanor
Home
Quickstart: Patterns and Best Practices
Using SQLAthanor
+ SQLAthanor and Pydantic
API Reference
Default Serialization Functions
Default De-serialization Functions
@@ -175,9 +176,9 @@ Key SQLAthanor Features
your needs.
* Works with :ref:`Declarative Reflection ` and the
:ref:`Automap Extension `.
-* Programmatically :ref:`generate Declarative Base Models from serialized data `.
+* Programmatically :ref:`generate Declarative Base Models from serialized data or Pydantic models `.
* Programmatically create :ref:`SQLAlchemy Table objects ` from
- serialized data.
+ serialized data or :term:`Pydantic models `.
|
diff --git a/docs/pydantic.rst b/docs/pydantic.rst
new file mode 100644
index 0000000..ed336cf
--- /dev/null
+++ b/docs/pydantic.rst
@@ -0,0 +1,532 @@
+.. sidebar:: Some Caveats
+
+ I'm a big fan of `Pydantic`_. Its authors have made different choices, but
+ that doesn't diminish my respect for their work, and that's one of the main reasons why
+ I have decided to extend **SQLAthanor** with built-in support for `Pydantic`_.
+
+ My interpretation of the priorities below is my subjective evaluation of the library and
+ its API semantics, and from my experience using the library in a number of web
+ application projects of varying complexity. These observations are not meant to be a
+ criticism: They are merely my thoughts on where the libraries have taken different
+ philosophical and stylistic paths.
+
+
+******************************************
+SQLAthanor and Pydantic
+******************************************
+
+.. |strong| raw:: html
+
+
+
+.. |/strong| raw:: html
+
+
+
+.. contents::
+ :local:
+ :depth: 3
+ :backlinks: entry
+
+----------
+
+.. versionadded:: 0.8.0
+
+SQLAthanor, Pydantic, and FastAPI
+=====================================
+
+**SQLAthanor** and `Pydantic`_ are both concerned with the serialization and
+deserialization of data. However, they approach the topic from different angles and have
+made a variety of (very different) architectural and stylistic choices.
+
+To be clear, neither set of choices is "better" or "worse", but they do reflect the
+authors' different priorities and stylistic preferences:
+
+.. list-table::
+ :widths: 50 50
+ :header-rows: 1
+
+ * - SQLAthanor Priorities
+ - Pydantic Priorities
+ * - Database/ORM compatibility with `SQLAlchemy`_
+ - Database/ORM agnosticism
+ * - | The maintenance of a single representation
+ | of your data model, tied to its database
+ | implementation
+ - | Multiple representations of your data model,
+ | each of which is tied to its usage in
+ | your code
+ * - | Explicit reference and conceptual
+ | documentation
+ - Documentation by example / in code
+ * - Explicit APIs for the data model's lifecycle
+ - | Implicit APIs relying on the Python standard
+ | library
+
+Both libraries have their place: in general terms, if I were working on a simple web
+application, on a microservice, or on a relatively simple data model I would consider
+`Pydantic`_ as a perfectly viable "quick-and-dirty" option. Its use of Python's native
+typing hints/annotation is a beautifully elegant solution.
+
+However, if I need to build a robust API with complex data model representations, tables
+with multiple relationships, or complicated business logic? Then I would prefer the
+robust and extensible capabilities afforded by the `SQLAlchemy`_ Delarative ORM and
+the **SQLAthanor** library.
+
+If that were it, I would consider `Pydantic`_ to be equivalent to `Marshmallow`_ and
+`Colander`_: an interesting tool for serialization/deserialization, and one that has its
+place, but not one that **SQLAthanor** need be concerned with.
+
+But there's one major difference: `FastAPI`_.
+
+`FastAPI`_ is an amazing microframework, and is rapidly rising in popularity across the
+Python ecosystem. That's for very good reason: It is blisteringly fast, its API is
+relatively simple, and it has the ability to automatically generate OpenAPI/Swagger
+schemas of your API endpoints. What's not to love?
+
+Well, its tight coupling with `Pydantic`_, for one thing. When building an application
+using the `FastAPI`_ framework, I am practically forced to use
+:term:`Pydantic models ` as my API inputs, outputs, and validators. If I
+choose not to use Pydantic models, then I lose many of the valuable features (besides
+performance) which make `FastAPI`_ so attractive for writing API applications.
+
+But using `FastAPI`_ and `Pydantic`_ in a complex API application may require a lot of
+"extra" code: the repetition of object models, the replication of business logic,
+the duplication of context, etc. All of these are concerns that **SQLAthanor** was
+explicitly designed to minimize.
+
+So what to do? Most patterns, documentation, and best practices found on the internet for
+authoring `FastAPI`_ applications explicitly suggest that you (manually, in your code):
+
+ * Create a `SQLAlchemy`_ :term:`model class` for the database interface for each data
+ model
+ * Create one `Pydantic`_ :term:`model class ` for *each* "version" of
+ your data model's output/input. So if you need one read version and a different write
+ version? You need two :term:`Pydantic models `.
+ * Use your :term:`Pydantic models ` as the validators for your API
+ endpoints, as needed.
+
+This is all fine and dandy, but now what happens if you need to add an attribute to your
+data model? You have to make a change to your `SQLAlchemy`_ model class, and to one or
+more `Pydantic`_ models, and possibly to your API endpoints. And let's not get started on
+changes to your data model's underlying business logic!
+
+There has to be a better way.
+
+Which is why I added `Pydantic`_ support to **SQLAthanor**. With this added support, you
+can effectively use your :term:`Pydantic models ` as the "canonical
+definition" of your data model. Think of the lifecycle this way:
+
+ * You define your data model in one or more :term:`Pydantic models `.
+ * You programmatically create a `SQLAlchemy`_ :term:`model class` whose columns are
+ *automatically* derived from the underlying :term:`Pydantic models `
+ and for whom each :term:`Pydantic Model` serves as a serialization/deserialization
+ :term:`configuration set`.
+
+Thus, you remove one of the (more complicated) steps in the process of writing your
+`FastAPI`_ application. Now all you have to do is create your `Pydantic`_ models, and then
+generate your **SQLAthanor** :term:`model classes `. Your `FastAPI`_ can
+still validate based on your `Pydantic`_ models, even if you choose to drive
+serialization/deserialization from your `SQLAlchemy`_ :term:`model classes `.
+
+In other words: It saves you code! And maintenance!
+
+Just look at the example below. Not only does it save you a couple of lines, but most
+importantly when in the future you need to modify your data model (and let's face it, that
+is one of the most common modifications in real applications) you make your changes in one
+place rather than two:
+
+.. tabs::
+
+ .. tab:: FastAPI with Pydantic only
+
+ .. code-block:: python
+ :linenos:
+
+ # THIS CODE SNIPPET HAS BEEN ADAPTED FROM THE OFFICIAL FASTAPI DOCUMENTATION:
+ # https://fastapi.tiangolo.com/tutorial/sql-databases/
+
+ # Assumes that there is a "database" module that defines your SQLAlchemy BaseModel.
+ from typing import List, Optional
+ from pydantic import BaseModel
+
+ from .database import Base
+
+ class User(Base):
+ __tablename__ = "users"
+
+ id = Column(Integer, primary_key=True, index=True)
+ email = Column(String, unique=True, index=True)
+ hashed_password = Column(String)
+ is_active = Column(Boolean, default=True)
+
+ items = relationship("Item", back_populates="owner")
+
+ class Item(Base):
+ __tablename__ = "items"
+
+ id = Column(Integer, primary_key=True, index=True)
+ title = Column(String, index=True)
+ description = Column(String, index=True)
+ owner_id = Column(Integer, ForeignKey("users.id"))
+
+ owner = relationship("User", back_populates="items")
+
+ class ItemBase(BaseModel):
+ title: str
+ description: Optional[str] = None
+
+ class ItemCreate(ItemBase):
+ pass
+
+ class Item(ItemBase):
+ id: int
+ owner_id: int
+
+ class Config:
+ orm_mode = True
+
+ class UserBase(BaseModel):
+ email: str
+
+ class UserCreate(UserBase):
+ password: str
+
+ class User(UserBase):
+ id: int
+ is_active: bool
+ items: List[Item] = []
+
+ class Config:
+ orm_mode = True
+
+ .. tab:: FastAPI with SQLAthanor/Pydantic
+
+ .. code-block:: python
+ :linenos:
+
+ from typing import List, Optional
+ from pydantic import BaseModel as PydanticBase
+
+ from sqlathanor import BaseModel, Column, generate_model_from_pydantic
+ from sqlalchemy.types import String
+
+ class ItemBase(PydanticBase):
+ title: str
+ description: Optional[str] = None
+
+ class ItemCreate(ItemBase):
+ pass
+
+ class ItemRead(ItemBase):
+ id: int
+ owner_id: int
+
+ class Config:
+ orm_mode = True
+
+ class UserBase(PydanticBase):
+ email: str
+
+ class UserCreate(UserBase):
+ password: str
+
+ class UserRead(UserBase):
+ id: int
+ is_active: bool
+ items: List[Item] = []
+
+ class Config:
+ orm_mode = True
+
+ User = generate_model_from_pydantic({ 'create': UserCreate
+ 'read': UserRead },
+ tablename = 'users',
+ primary_key = 'id')
+
+
+ Item = generate_model_from_pydantic({ 'create': ItemCreate,
+ 'read': ItemRead },
+ tablename = 'items',
+ primary_key = 'id')
+
+ Item.owner = relationship("User", back_populates="items")
+ User.items = relationship("Item", back_populates="owner")
+ User.hashed_password = Column(String,
+ supports_csv = False,
+ supports_json = False,
+ supports_yaml = False,
+ supports_dict = False)
+
+
+----------------
+
+.. _generating_and_configuring_model_classes_using_pydantic:
+
+Generating and Configuring Model Classes Using Pydantic
+==========================================================
+
+As **SQLAthanor** relies on the creation of :term:`model classes ` which
+both define your database representation and provide serialization/deserialization
+configuration instructions, the first step to using `Pydantic`_ with **SQLAthanor** is
+to generate your :term:`model classes ` based on your
+:term:`Pydantic models `.
+
+You can do this in **SQLAthanor** using the
+:func:`generate_model_from_pydantic() `
+function. This function takes your :term:`Pydantic models ` as an input,
+and creates a **SQLAthanor** :term:`model class` (which is a subclass of
+:class:`sqlathanor.declarative.BaseModel`).
+
+When generating your model classes from :term:`Pydantic models `, you can
+supply multiple models which will then get consolidated into a single **SQLAthanor**
+:class:`BaseModel `. For example:
+
+.. tabs::
+
+ .. tab:: 1 Model
+
+ This example shows how you would generate a single
+ :class:`sqlathanor.BaseModel ` from a single
+ :class:`pydantic.BaseModel`. Since it only has one model, it would have only one
+ serialization/deserialization :term:`configuration set` by default:
+
+ .. code-block:: python
+
+ from pydantic import BaseModel as PydanticBaseModel
+ from sqlathanor import generate_model_from_pydantic
+
+ class SinglePydanticModel(PydanticBaseModel):
+ id: int
+ username: str
+ email: str
+
+ SingleSQLAthanorModel = generate_model_from_pydantic(SinglePydanticModel,
+ tablename = 'my_tablename',
+ primary_key = 'id')
+
+ This code will generate a single **SQLAthanor** :term:`model class` named
+ ``SingleSQLAthanorModel``, which will contain three columns: ``id``, ``username``,
+ and ``email``. The column types will be set to correspond to the data types annotated
+ in the ``SinglePydanticModel`` class definition.
+
+ .. tab:: 2 Models (shared config set)
+
+ This example shows how you would combine multiple
+ :term:`Pydantic models ` into a single
+ :class:`sqlathanor.BaseModel `. A typical use case
+ would be if one :term:`Pydantic model` represents the output when
+ you are retrieving/viewing a user's data (which does not have a ``password`` field for
+ security reasons) and hte other :term:`Pydantic model` represents the input when
+ you are writing/creating a new user (which does need the password field).
+
+ .. note::
+
+ Because both :term:`Pydantic models ` are passed to the function in
+ a single :class:`list `, they will receive a single **SQLAthanor**
+ :term:`configuration set`.
+
+ .. code-block:: python
+
+ from pydantic import BaseModel as PydanticBaseModel
+ from sqlathanor import generate_model_from_pydantic
+
+ class ReadUserModel(PydanticBaseModel):
+ id: int
+ username: str
+ email: str
+
+ class WriteUserModel(ReadUserModel):
+ password: str
+
+ SingleSQLAthanorModel = generate_model_from_pydantic([ReadUserModel,
+ WriteUserModel],
+ tablename = 'my_tablename',
+ primary_key = 'id')
+
+ This code will generate a single **SQLAthanor** :term:`model class` named
+ ``SingleSQLAthanorModel`` with four columns (``id``, ``username``, ``email``, and
+ ``password``). However, because all models were passed in as a single list, the
+ columns will be consolidated with only *one* :term:`configuration set`.
+
+ .. caution::
+
+ In my experience, it is very rare that you would want to consolidate multiple
+ :term:`Pydantic models ` with only one :term:`configuration set`.
+ Most of the type, each :term:`Pydantic model` will actually represent its own
+ :term:`configuration set` as documented in the next example.
+
+ .. tab:: 2 Models (independent config sets)
+
+ This example shows how you would combine multiple
+ :term:`Pydantic models ` into a single
+ :class:`sqlathanor.BaseModel `, but configure
+ multiple serialization/deserialization
+ :term:`configuration sets ` based on those
+ :term:`Pydantic models `.
+
+ This is the most-common use case, and is fairly practical. To define multiple
+ :term:`configuration sets `, simply pass the
+ :term:`Pydantic models ` as key/value pairs in the first argument:
+
+ .. code-block:: python
+
+ from pydantic import BaseModel as PydanticBaseModel
+ from sqlathanor import generate_model_from_pydantic
+
+ class ReadUserModel(PydanticBaseModel):
+ id: int
+ username: str
+ email: str
+
+ class WriteUserModel(ReadUserModel):
+ password: str
+
+ SQLAthanorModel = generate_model_from_pydantic({ 'read': ReadUserModel,
+ 'write': WriteUserModel
+ },
+ tablename = 'my_tablename',
+ primary_key = 'id')
+
+ This code will generate a single **SQLAthanor** :term:`model class`
+ (``SQLAthanorModel``, with four columns - ``id``, ``username``, ``email``, and
+ ``password``), but that model class will have two configuration sets: ``read`` which
+ will serialize/de-serialize only three columns (``id``, ``username``, and ``email``) and
+ ``write`` which will serialize/de-serialize four columns (``id``, ``username``,
+ ``email``, and ``password``).
+
+ This ``SQLAthanorModel`` then becomes useful when serializing your
+ :term:`model instances ` to :class:`dict ` or de-serializing
+ them from :class:`dict ` using the context-appropriate
+ :term:`configuration set`:
+
+ .. code-block:: python
+
+ # Assumes that "as_dict" contains a string JSON representation with attributes as
+ # defined in your "WriteUserModel" Pydantic model.
+ model_instance = SQLAthanorModel.new_from_json(as_json, config_set = 'write')
+
+ # Produces a dict representation of the object with three attributes, corresponding
+ # to your "ReadUserModel" Pydantic model.
+ readable_as_dict = model_instance.to_dict(config_set = 'read')
+
+.. tip::
+
+ When generating your **SQLAthanor** :term:`model classes ` from your
+ :term:`Pydantic models `, it is important to remember that serialization
+ and de-serialization is disabled by default for security reasons. Therefore a best
+ practice is to
+ :ref:`enable/disable your serialization and de-serialization at runtime `.
+
+ .. seealso::
+
+ * :meth:`BaseModel.configure_serialization() `
+ * :meth:`BaseModel.set_attribute_serialization_config() `
+
+.. caution::
+
+ This functionality *does not* support more complex table structures, including
+ relationships, hybrid properties, or association proxies.
+
+-------------------------
+
+Generating Tables from Pydantic Models
+==========================================
+
+Just as you can
+:ref:`generate SQLAthanor model classes from Pydantic models `,
+you can also create :class:`Table ` objects from
+:term:`Pydantic models `, consolidating their attributes into standard
+SQL :class:`Column ` definitions.
+
+.. code-block:: python
+
+ from pydantic import BaseModel
+ from sqlathanor import Table
+
+ # Define Your Pydantic Models
+ class UserWriteModel(BaseModel):
+ id: int
+ username: str
+ email: str
+ password: str
+
+ class UserReadModel(BaseModel):
+ id: int
+ username: str
+ email: str
+
+ # Create Your Table
+ pydantic_table = Table.from_pydantic([UserWriteModel, UserReadModel],
+ tablename = 'my_tablename_goes_here',
+ primary_key = 'id')
+
+This code will generate a single :class:`Table ` instance
+(``pydantic_table``) which will have four columns: ``id``, ``username``, ``email``, and
+``password``. Their column types will correspond to the type hints defined in the Pydantic
+models.
+
+.. seealso::
+
+ * :class:`Table `
+ * :meth:`Table.from_pydantic() `
+
+----------------------
+
+.. _configuring_attributes_from_pydantic_models:
+
+Configuring Attributes from Pydantic Models
+===============================================
+
+There may be times when you wish to configure the serialization / de-serialization of
+:term:`model class` attributes based on a related :term:`Pydantic model`. You can
+programmatically create a new
+:class:`AttributeConfiguration ` instance
+from a :term:`Pydantic model` by calling the
+:meth:`AttributeConfiguration.from_pydantic_model() `
+class method:
+
+.. code-block:: python
+
+ from pydantic import BaseModel
+ from sqlathanor import Table
+
+ # Define Your Pydantic Models
+ class UserWriteModel(BaseModel):
+ id: int
+ username: str
+ email: str
+ password: str
+
+ class UserReadModel(BaseModel):
+ id: int
+ username: str
+ email: str
+
+ password_config = AttributeConfiguration.from_pydantic_model(UserWriteModel,
+ field = 'password',
+ supports_csv = (True, False),
+ supports_json = (True, False),
+ supports_yaml = (True, False),
+ supports_dict = (True, False),
+ on_deserialize = my_encryption_function)
+
+This code will produce a single
+:class:`AttributeConfiguration ` instance
+named ``password_config``. It will support the de-serialization of data, but will never be
+serialized (a typical pattern for password fields!). Furthermore, it will execute the
+``my_encryption_function`` during the de-serialization process.
+
+A very common use case is to configure the serialization/de-serialization profile for
+attributes that were programmatically derived from
+:term:`Pydantic models `.
+
+.. seealso::
+
+ * :meth:`AttributeConfiguration.from_pydantic_model() `
+
+.. _Pydantic: https://pydantic-docs.helpmanual.io/
+.. _FastAPI: https://fastapi.tiangolo.com/
+.. _SQLAlchemy: http://www.sqlalchemy.org
+.. _Marshmallow: https://marshmallow.readthedocs.io/en/3.0/
+.. _Colander: https://docs.pylonsproject.org/projects/colander/en/latest/
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index dde5a5d..8853363 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -347,7 +347,8 @@ Password De-serialization
Programmatically Generating Models
=====================================
-.. versionadded:: 0.3.0
+.. versionadded:: 0.3.0 generation from CSV, JSON, YAML, or :class:`dict `
+.. versionadded:: 0.8.0 generation from Pydantic models
.. seealso::
@@ -355,6 +356,7 @@ Programmatically Generating Models
* :func:`generate_model_from_json() `
* :func:`generate_model_from_yaml() `
* :func:`generate_model_from_dict() `
+ * :func:`generate_model_from_pydantic() `
.. tabs::
@@ -403,6 +405,17 @@ Programmatically Generating Models
tablename = 'my_table_name',
primary_key = 'id')
+ .. tab:: Pydantic
+
+ .. code-block:: python
+
+ from sqlathanor import generate_model_from_pydantic
+
+ # Assumes that "PydanticReadModel" and "PydanticWriteModel" contain the Pydantic
+ # models for the object/resource you are representing.
+ PydanticModel = generate_model_from_pydantic([PydanticReadModel, PydanticWriteModel],
+ tablename = 'my_table_name',
+ primary_key = 'id')
----------------------------
@@ -672,10 +685,11 @@ Using SQLAthanor with Flask-SQLAlchemy
----------------------------
-Generating SQLAlchemy Tables from Serialized Data
+Generating SQLAlchemy Tables Programmatically
====================================================
-.. versionadded:: 0.3.0
+.. versionadded:: 0.3.0 CSV, JSON, YAML, and :class:`dict ` support
+.. versionadded:: 0.8.0 Pydantic model support
.. seealso::
@@ -683,6 +697,7 @@ Generating SQLAlchemy Tables from Serialized Data
* :meth:`Table.from_json() `
* :meth:`Table.from_yaml() `
* :meth:`Table.from_dict() `
+ * :meth:`Table.from_pydantic() `
.. tabs::
@@ -749,3 +764,35 @@ Generating SQLAlchemy Tables from Serialized Data
skip_nested = True,
default_to_str = False,
type_mapping = None)
+
+ .. tab:: Pydantic
+
+ .. versionadded:: 0.8.0
+
+ .. code-block:: python
+
+ from pydantic import BaseModel
+ from sqlathanor import Table
+
+ # Define Your Pydantic Models
+ class UserWriteModel(BaseModel):
+ id: int
+ username: str
+ email: str
+ password: str
+
+ class UserReadModel(BaseModel):
+ id: int
+ username: str
+ email: str
+
+ # Create Your Table
+ pydantic_table = Table.from_pydantic([UserWriteModel, UserReadModel],
+ tablename = 'my_tablename_goes_here',
+ primary_key = 'id')
+
+ .. seealso::
+
+ * :class:`Table `
+ * :meth:`Table.from_pydantic() `
+ * :doc:`SQLAthanor and Pydantic `
diff --git a/docs/using.rst b/docs/using.rst
index a1e5c26..691e192 100644
--- a/docs/using.rst
+++ b/docs/using.rst
@@ -148,6 +148,8 @@ SQLAthanor Features
:doc:`SQLAlchemy ORM `.
* Maintain all of the existing APIs, methods, functions, and functionality of
:doc:`SQLAlchemy Declarative ORM `.
+* Drive the definition and configuration of your data model from your
+ :term:`Pydantic models `.
---------------
@@ -260,7 +262,7 @@ Dependencies
.. tabs::
- .. tab:: Standard Approach
+ .. tab:: Normally
Because **SQLAthanor** is a drop-in replacement for `SQLAlchemy`_ and its
:doc:`Declarative ORM `, you can
@@ -272,7 +274,36 @@ Dependencies
* :doc:`SQLAlchemy ORM Tutorial `
* :doc:`Flask-SQLAlchemy: Declaring Models `
- .. tab:: Declarative Reflection
+ .. tab:: from Pydantic
+
+ .. versionadded:: 0.8.0
+
+ If your application is using `Pydantic`_ as a parsing/validation library, then you
+ can programmatically generate a pre-configured
+ :doc:`SQLAlchemy Declarative `
+ :term:`model class` using **SQLAthanor** with the syntax
+ ``generate_model_from_pydantic()``.
+
+ This function can accept one or more :term:`Pydantic models `, and
+ will consolidate them into a single **SQLAthanor** :term:`model class`, with
+ each underlying :term:`Pydantic model` corresponding to a :term:`configuration set`
+ for easy serialization / deserialization:
+
+ .. code-block:: python
+
+ from sqlathanor import generate_model_from_pydantic
+
+ # Assuming that "PydanticBaseModel" is a
+ PydanticDBModel = generate_model_from_pydantic([PydanticReadModel, PydanticWriteModel],
+ tablename = 'my_table_name',
+ primary_key = 'id')
+
+ .. seealso::
+
+ * :func:`generate_model_from_pydantic() `
+ * :doc:`SQLAthanor and Pydantic Support `
+
+ .. tab:: via Reflection
`SQLAlchemy`_ supports the use of `reflection`_ with the
:doc:`SQLAlchemy Declarative ORM `.
@@ -293,7 +324,7 @@ Dependencies
* **SQLAlchemy**: :doc:`Reflecting Database Objects `
* **SQLAlchemy**: `Using Reflection with Declarative `_
- .. tab:: Using Automap
+ .. tab:: via Automap
.. versionadded:: 0.2.0
@@ -314,7 +345,7 @@ Dependencies
* :ref:`Using Automap with SQLAthanor `
* **SQLAlchemy**: :doc:`Automap Extension `
- .. tab:: Programmatically
+ .. tab:: from Serialized Data
.. versionadded:: 0.3.0
@@ -1054,6 +1085,8 @@ Why Two Configuration Approaches?
and "data scientists", I've tried to design an interface that will feel "natural"
to both communities.
+.. _configuring_at_runtime:
+
Configuring at Runtime
-------------------------------------
@@ -1632,20 +1665,22 @@ expect it in inbound data to de-serialize.
* **SQLAlchemy**: :doc:`Automap Extension `
* :ref:`Using Declarative Reflection with SQLAthanor `
-.. _SQLAlchemy: http://www.sqlalchemy.org
-.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/
.. _reflection: http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/table_config.html#using-reflection-with-declarative
----------------------------
.. _generating_tables:
-Generating SQLAlchemy Tables from Serialized Data
+Generating SQLAlchemy Tables...
====================================================
+...from Serialized Data
+-------------------------------
+
.. versionadded:: 0.3.0
-If you are **not** using `SQLAlchemy`_'s :doc:`Declarative ORM `
+If you are **not** using `SQLAlchemy`_'s
+:doc:`Declarative ORM `
but would like to generate SQLAlchemy :class:`Table `
objects programmatically based on serialized data, you can do so by importing the
**SQLAthanor** :class:`Table ` object and calling a
@@ -1724,3 +1759,42 @@ objects programmatically based on serialized data, you can do so by importing th
* :meth:`Table.from_json() `
* :meth:`Table.from_yaml() `
* :meth:`Table.from_dict() `
+
+... from Pydantic Models
+--------------------------------
+
+If you are **not** using `SQLAlchemy`_'s
+:doc:`Declarative ORM `
+but would like to generate SQLAlchemy :class:`Table `
+objects programmatically based on :term:`Pydantic models `, you can do so
+by importing the **SQLAthanor** :class:`Table ` class and calling
+the :meth:`from_pydantic() ` class
+method:
+
+.. code-block:: python
+
+ from pydantic import BaseModel
+ from sqlathanor import Table
+
+ # Define Your Pydantic Models
+ class PydanticWriteModel(BaseModel):
+ id: int
+ username: str
+ email: str
+ password: str
+
+ class PydanticReadModel(BaseModel):
+ id: int
+ username: str
+ email: str
+
+ # Create Your Table
+ pydantic_table = Table.from_pydantic([PydanticWriteModel, PydanticReadModel],
+ tablename = 'my_tablename_goes_here',
+ primary_key = 'id')
+
+.. seealso::
+
+ * :class:`Table `
+ * :meth:`Table.from_pydantic() `
+ * :doc:`SQLAthanor and Pydantic `
diff --git a/requirements.txt b/requirements.txt
index dfd980a..99b4f8d 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/setup.py b/setup.py
index a297eb4..3d410d3 100644
--- a/setup.py
+++ b/setup.py
@@ -120,7 +120,9 @@
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: 3.8'
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10'
],
zip_safe = False,
@@ -172,14 +174,16 @@
'sphinx-tabs',
'readme-renderer',
'restview',
- 'Flask-SQLAlchemy'],
+ 'Flask-SQLAlchemy',
+ 'pydantic;python_version>"3.6"'],
'test': ['coverage',
'pytest',
'pytest-benchmark',
'pytest-cov',
'tox',
'codecov',
- 'Flask-SQLAlchemy'],
+ 'Flask-SQLAlchemy',
+ 'pydantic;python_version>="3.6"'],
},
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4',
diff --git a/sqlathanor/__version__.py b/sqlathanor/__version__.py
index 28b1b2f..690cefb 100644
--- a/sqlathanor/__version__.py
+++ b/sqlathanor/__version__.py
@@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
-__version__ = '0.7.0'
+__version__ = '0.8.0'
diff --git a/sqlathanor/attributes.py b/sqlathanor/attributes.py
index 04f8f4d..8cc7035 100644
--- a/sqlathanor/attributes.py
+++ b/sqlathanor/attributes.py
@@ -12,6 +12,7 @@
from sqlathanor._serialization_support import SerializationMixin
from sqlathanor.utilities import bool_to_tuple, callable_to_dict
+from sqlathanor import errors
BLANK_ON_SERIALIZE = {
@@ -172,11 +173,26 @@ def __init__(self,
provided in ``name``. Defaults to :obj:`None `
:type display_name: :class:`str ` / :obj:`None `
+ :param pydantic_field: An optional Pydantic
+ :class:`ModelField ` which can be used to
+ validate the attribute on serialization. Defaults to :obj:`None `.
+
+ .. note::
+
+ If present, values will be validated against the Pydantic field *after* any
+ ``on_deserialize`` function is executed.
+
+ :type pydantic_field: :class:`pydantic.fields.ModelField `
+ / :class:`pydantic.fields.FieldInfo `
+ / :obj:`None `
+
"""
object.__setattr__(self, '_dict_proxy', {})
self._current = -1
+ self._pydantic_field = None
self._name = None
self.name = kwargs.pop('name', None)
+ self.pydantic_field = kwargs.pop('pydantic_field', None)
attribute = kwargs.pop('attribute', None)
super(AttributeConfiguration, self).__init__(*args, **kwargs)
@@ -401,6 +417,27 @@ def name(self, value):
value = validators.string(value, allow_empty = True)
self._name = value
+ @property
+ def pydantic_field(self):
+ """A Pydantic :class:`ModelField` object that can be used to validate the
+ attribute. Defaults to :obj:`None `.
+
+ :rtype: :class:`pydantic.fields.ModelField ` /
+ :class:`pydantic.fields.FieldInfo ` /
+ :obj:`None `
+ """
+ return self._pydantic_field
+
+ @pydantic_field.setter
+ def pydantic_field(self, value):
+ if not value:
+ value = None
+ elif not checkers.is_type(value, ('ModelField', 'FieldInfo')):
+ raise ValueError('value must be a Pydantic ModelField or FieldInfo object. '
+ 'Was: %s' % type(value))
+
+ self._pydantic_field = value
+
@classmethod
def from_attribute(cls, attribute):
"""Return an instance of :class:`AttributeConfiguration` configured for a
@@ -423,9 +460,365 @@ def copy(self):
return new_instance
+ @classmethod
+ def from_pydantic_model(cls,
+ model,
+ name,
+ **kwargs):
+ """Return a new :class:`AttributeConfiguration` instance produced from a
+ Pydantic model's field definition for ``name``.
+
+ :param model: The :term:`Pydantic model` whose field should be used.
+ :type model: Pydantic :class:`ModelMetaclass `
+
+ :param name: The name of the field to convert to convert to an
+ :class:`AttributeConfiguration`
+ :type name: :class:`str `
+
+ :param supports_csv: Determines whether the column can be serialized to or
+ de-serialized from CSV format.
+
+ If ``True``, can be serialized to CSV and de-serialized from CSV. If
+ ``False``, will not be included when serialized to CSV and will be ignored
+ if present in a de-serialized CSV.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to CSV
+ or de-serialized from CSV.
+
+ :type supports_csv: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param supports_json: Determines whether the column can be serialized to or
+ de-serialized from JSON format.
+
+ If ``True``, can be serialized to JSON and de-serialized from JSON.
+ If ``False``, will not be included when serialized to JSON and will be
+ ignored if present in a de-serialized JSON.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to JSON
+ or de-serialized from JSON.
+
+ :type supports_json: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param supports_yaml: Determines whether the column can be serialized to or
+ de-serialized from YAML format.
+
+ If ``True``, can be serialized to YAML and de-serialized from YAML.
+ If ``False``, will not be included when serialized to YAML and will be
+ ignored if present in a de-serialized YAML.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to YAML
+ or de-serialized from YAML.
+
+ :type supports_yaml: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param supports_dict: Determines whether the column can be serialized to or
+ de-serialized to a Python :class:`dict `.
+
+ If ``True``, can be serialized to :class:`dict ` and de-serialized
+ from a :class:`dict `. If ``False``, will not be included
+ when serialized to :class:`dict ` and will be ignored if
+ present in a de-serialized :class:`dict `.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to a
+ :class:`dict ` or de-serialized from a :class:`dict `.
+
+ :type supports_dict: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param on_deserialize: A function that will be called when attempting to
+ assign a de-serialized value to the column. This is intended to either coerce
+ the value being assigned to a form that is acceptable by the column, or
+ raise an exception if it cannot be coerced. If :obj:`None `, the data
+ type's default ``on_deserialize`` function will be called instead.
+
+ .. tip::
+
+ If you need to execute different ``on_deserialize`` functions for
+ different formats, you can also supply a :class:`dict `:
+
+ .. code-block:: python
+
+ on_deserialize = {
+ 'csv': csv_on_deserialize_callable,
+ 'json': json_on_deserialize_callable,
+ 'yaml': yaml_on_deserialize_callable,
+ 'dict': dict_on_deserialize_callable
+ }
+
+ Defaults to :obj:`None `.
+
+ :type on_deserialize: callable / :class:`dict ` with formats
+ as keys and values as callables
+
+ :param on_serialize: A function that will be called when attempting to
+ serialize a value from the column. If :obj:`None `, the data
+ type's default ``on_serialize`` function will be called instead.
+
+ .. tip::
+
+ If you need to execute different ``on_serialize`` functions for
+ different formats, you can also supply a :class:`dict `:
+
+ .. code-block:: python
+
+ on_serialize = {
+ 'csv': csv_on_serialize_callable,
+ 'json': json_on_serialize_callable,
+ 'yaml': yaml_on_serialize_callable,
+ 'dict': dict_on_serialize_callable
+ }
+
+ Defaults to :obj:`None `.
+
+ :type on_serialize: callable / :class:`dict ` with formats
+ as keys and values as callables
+
+ :param csv_sequence: Indicates the numbered position that the column should be in
+ in a valid CSV-version of the object. Defaults to :obj:`None `.
+
+ .. note::
+
+ If not specified, the column will go after any columns that *do* have a
+ ``csv_sequence`` assigned, sorted alphabetically.
+
+ If two columns have the same ``csv_sequence``, they will be sorted
+ alphabetically.
+
+ :type csv_sequence: :class:`int ` / :obj:`None `
+
+ :returns: An :class:`AttributeConfiguration` for attribute ``name`` derived from
+ the Pydantic model.
+ :rtype: :class:`AttributeConfiguration`
+
+ :raises ValueError: if ``model`` is not a Pydantic
+ :class:`ModelMetaclass `
+
+ :raises validator_collection.errors.InvalidVariableName: if ``name`` is not a
+ valid variable name
+ :raises FieldNotFoundError: if a field with ``name`` is not found within ``model``
+
+ """
+ if not checkers.is_type(model, ('BaseModel', 'ModelMetaclass')):
+ raise ValueError('model must be a Pydantic Model. Was: %s' % type(model))
+
+ name = validators.variable_name(name, allow_empty = False)
+ if name not in model.__fields__:
+ raise errors.FieldNotFoundError(
+ 'name ("%s") not found in the Pydantic model' % name
+ )
+
+ field = model.__fields__[name]
+
+ if not kwargs:
+ kwargs = {}
+ kwargs['name'] = name
+ kwargs['pydantic_field'] = field
+
+ return_value = cls(**kwargs)
+
+ return return_value
+
+
+def convert_pydantic_model(model,
+ **kwargs):
+ """Convert a Pydantic model to a collection of :class:`AttributeConfiguration` objects.
+
+ :param model: The Pydantic
+ :class:`ModelMetaclass ` to
+ convert.
+
+ .. caution::
+
+ This parameter is expected to be a **class** object, not an **instance** object.
+ In a Pydantic context, it is the class that you define which inherits from
+ Pydantic ``BaseModel``.
+
+ Thus, if your Pydantic model definition looks like this:
+
+ .. code-block:: python
+
+ from pydantic import BaseModel
+
+ class User(BaseModel):
+ id: int
+ username: str
+ email: str
+
+ user = User(id = 123, username = 'test_username', email = 'email@domain.dev')
+
+ you would pass convert ``User`` and not ``user``:
+
+ .. code-block:: python
+
+ attribute_configurations = convert_pydantic_model(User)
+
+ :type model: :class:`pydantic.main.ModelMetaclass `
+
+ :param supports_csv: Determines whether the column can be serialized to or
+ de-serialized from CSV format.
+
+ If ``True``, can be serialized to CSV and de-serialized from CSV. If
+ ``False``, will not be included when serialized to CSV and will be ignored
+ if present in a de-serialized CSV.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to CSV
+ or de-serialized from CSV.
+
+ :type supports_csv: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param supports_json: Determines whether the column can be serialized to or
+ de-serialized from JSON format.
+
+ If ``True``, can be serialized to JSON and de-serialized from JSON.
+ If ``False``, will not be included when serialized to JSON and will be
+ ignored if present in a de-serialized JSON.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to JSON
+ or de-serialized from JSON.
+
+ :type supports_json: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param supports_yaml: Determines whether the column can be serialized to or
+ de-serialized from YAML format.
+
+ If ``True``, can be serialized to YAML and de-serialized from YAML.
+ If ``False``, will not be included when serialized to YAML and will be
+ ignored if present in a de-serialized YAML.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to YAML
+ or de-serialized from YAML.
+
+ :type supports_yaml: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param supports_dict: Determines whether the column can be serialized to or
+ de-serialized to a Python :class:`dict `.
+
+ If ``True``, can be serialized to :class:`dict ` and de-serialized
+ from a :class:`dict `. If ``False``, will not be included
+ when serialized to :class:`dict ` and will be ignored if
+ present in a de-serialized :class:`dict `.
+
+ Can also accept a 2-member :class:`tuple ` (inbound / outbound)
+ which determines de-serialization and serialization support respectively.
+
+ Defaults to ``False``, which means the column will not be serialized to a
+ :class:`dict ` or de-serialized from a :class:`dict `.
+
+ :type supports_dict: :class:`bool ` / :class:`tuple ` of
+ form (inbound: :class:`bool `, outbound: :class:`bool `)
+
+ :param on_deserialize: A function that will be called when attempting to
+ assign a de-serialized value to the column. This is intended to either coerce
+ the value being assigned to a form that is acceptable by the column, or
+ raise an exception if it cannot be coerced. If :obj:`None `, the data
+ type's default ``on_deserialize`` function will be called instead.
+
+ .. tip::
+
+ If you need to execute different ``on_deserialize`` functions for
+ different formats, you can also supply a :class:`dict `:
+
+ .. code-block:: python
+
+ on_deserialize = {
+ 'csv': csv_on_deserialize_callable,
+ 'json': json_on_deserialize_callable,
+ 'yaml': yaml_on_deserialize_callable,
+ 'dict': dict_on_deserialize_callable
+ }
+
+ Defaults to :obj:`None `.
+
+ :type on_deserialize: callable / :class:`dict ` with formats
+ as keys and values as callables
+
+ :param on_serialize: A function that will be called when attempting to
+ serialize a value from the column. If :obj:`None `, the data
+ type's default ``on_serialize`` function will be called instead.
+
+ .. tip::
+
+ If you need to execute different ``on_serialize`` functions for
+ different formats, you can also supply a :class:`dict `:
+
+ .. code-block:: python
+
+ on_serialize = {
+ 'csv': csv_on_serialize_callable,
+ 'json': json_on_serialize_callable,
+ 'yaml': yaml_on_serialize_callable,
+ 'dict': dict_on_serialize_callable
+ }
+
+ Defaults to :obj:`None `.
+
+ :type on_serialize: callable / :class:`dict ` with formats
+ as keys and values as callables
+
+ :param csv_sequence: Indicates the numbered position that the column should be in
+ in a valid CSV-version of the object. Defaults to :obj:`None `.
+
+ .. note::
+
+ If not specified, the column will go after any columns that *do* have a
+ ``csv_sequence`` assigned, sorted alphabetically.
+
+ If two columns have the same ``csv_sequence``, they will be sorted
+ alphabetically.
+
+ :type csv_sequence: :class:`int ` / :obj:`None `
+
+ :returns: A collection of :class:`AttributeConfiguration` objects.
+ :rtype: :class:`list ` of :class:`AttributeConfiguration` objects
+
+ :raises ValueError: if ``model`` is not a Pydantic
+ :class:`BaseModel `
+
+ """
+ if not checkers.is_type(model, ('ModelMetaclass')):
+ raise ValueError('model must be a Pydantic ModelMetaclass object. '
+ 'Was: %s' % model.__class__.__name__)
+
+ attribute_names = [x for x in model.__fields__]
+
+ return_value = [AttributeConfiguration.from_pydantic_model(model,
+ name = x,
+ **kwargs)
+ for x in attribute_names]
+
+ return return_value
+
def validate_serialization_config(config):
- """Validate that ``config`` contains :class:`AttributeConfiguration` objects.
+ """Validate that ``config`` contains or can be converted to
+ :class:`AttributeConfiguration` objects.
:param config: Object or iterable of objects that represent
:class:`AttributeConfigurations `
@@ -433,6 +826,8 @@ def validate_serialization_config(config):
:class:`dict ` objects corresponding to a
:class:`AttributeConfiguration` / :class:`AttributeConfiguration` /
:class:`dict ` object corresponding to a :class:`AttributeConfiguration`
+ / :class:`pydantic.main.ModelMetaclass `
+ object whose fields correspond to :class:`AttributeConfiguration` objects
:rtype: :class:`list ` of :class:`AttributeConfiguration` objects
"""
@@ -446,6 +841,9 @@ def validate_serialization_config(config):
if not config:
return []
+ if checkers.is_type(config[0], ('BaseModel', 'ModelMetaclass')):
+ config = convert_pydantic_model(config[0])
+
return_value = []
for item in config:
if isinstance(item, AttributeConfiguration) and item not in return_value:
diff --git a/sqlathanor/declarative/__init__.py b/sqlathanor/declarative/__init__.py
index 2f92f54..fca3d06 100644
--- a/sqlathanor/declarative/__init__.py
+++ b/sqlathanor/declarative/__init__.py
@@ -8,7 +8,8 @@
from sqlathanor.declarative.base_model import BaseModel
from sqlathanor.declarative.declarative_base import declarative_base, as_declarative
from sqlathanor.declarative.generate_model import generate_model_from_csv, \
- generate_model_from_json, generate_model_from_yaml, generate_model_from_dict
+ generate_model_from_json, generate_model_from_yaml, generate_model_from_dict, \
+ generate_model_from_pydantic
__all__ = [
@@ -18,5 +19,6 @@
'generate_model_from_csv',
'generate_model_from_json',
'generate_model_from_yaml',
- 'generate_model_from_dict'
+ 'generate_model_from_dict',
+ 'generate_model_from_pydantic'
]
diff --git a/sqlathanor/declarative/_base_configuration_mixin.py b/sqlathanor/declarative/_base_configuration_mixin.py
index a57a0fd..b70e582 100644
--- a/sqlathanor/declarative/_base_configuration_mixin.py
+++ b/sqlathanor/declarative/_base_configuration_mixin.py
@@ -568,8 +568,8 @@ def get_serialization_config(cls,
:param config_set: If not :obj:`None `, will return those
:class:`AttributeConfiguration `
- objects that are contained within the named set. Defaults to
- :obj:`None `.
+ objects that are contained within the named
+ :term:`configuration set`. Defaults to :obj:`None `.
:type config_set: :class:`str ` / :obj:`None `
:returns: List of attribute configurations.
@@ -648,8 +648,8 @@ def get_attribute_serialization_config(cls,
:param config_set: If not :obj:`None `, will return the
:class:`AttributeConfiguration `
- object for ``attribute`` that is contained within the named set. Defaults to
- :obj:`None `.
+ object for ``attribute`` that is contained within the named
+ :term:`configuration set`. Defaults to :obj:`None `.
:type config_set: :class:`str ` / :obj:`None `
:returns: The
@@ -698,7 +698,8 @@ def set_attribute_serialization_config(cls,
:class:`AttributeConfiguration