pax_global_header00006660000000000000000000000064135343356100014515gustar00rootroot0000000000000052 comment=abf13441214e0ccd13c93cbcac493c065042cace marshmallow-sqlalchemy-0.19.0/000077500000000000000000000000001353433561000162725ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/.gitignore000066400000000000000000000006411353433561000202630ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml .pytest_cache # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Complexity output/*.html output/*/index.html # Sphinx docs/_build README.html # konch .konchrc marshmallow-sqlalchemy-0.19.0/.pre-commit-config.yaml000066400000000000000000000006731353433561000225610ustar00rootroot00000000000000repos: - repo: https://github.com/asottile/pyupgrade rev: v1.23.0 hooks: - id: pyupgrade args: ["--py36-plus"] - repo: https://github.com/python/black rev: 19.3b0 hooks: - id: black language_version: python3 - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.8 hooks: - id: flake8 - repo: https://github.com/asottile/blacken-docs rev: v1.3.0 hooks: - id: blacken-docs additional_dependencies: [black==19.3b0] marshmallow-sqlalchemy-0.19.0/.readthedocs.yml000066400000000000000000000002511353433561000213560ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py formats: all python: version: 3.7 install: - method: pip path: . extra_requirements: - docs marshmallow-sqlalchemy-0.19.0/AUTHORS.rst000066400000000000000000000025761353433561000201630ustar00rootroot00000000000000******* Authors ******* Leads ===== - Steven Loria `@sloria `_ Contributors ============ - Rob MacKinnon `@rmackinnon `_ - Josh Carp `@jmcarp `_ - Jason Mitchell `@mitchej123 `_ - Douglas Russell `@dpwrussell `_ - Rudá Porto Filgueiras `@rudaporto `_ - Sean Harrington `@seanharr11 `_ - Eric Wittle `@ewittle `_ - Alex Rothberg `@cancan101 `_ - Vlad Frolov `@frol `_ - Kelvin Hammond `@kelvinhammond `_ - Yuri Heupa `@YuriHeupa `_ - Jeremy Muhlich `@jmuhlich `_ - Ilya Chistyakov `@ilya-chistyakov `_ - Victor Gavro `@vgavro `_ - Maciej Barański `@gtxm `_ - Jared Deckard `@deckar01 `_ - AbdealiJK `@AbdealiJK `_ - jean-philippe serafin `@jeanphix `_ - Jack Smith `@jacksmith15 `_ - Kazantcev Andrey `@heckad `_ - Samuel Searles-Bryant `@samueljsb `_ marshmallow-sqlalchemy-0.19.0/CHANGELOG.rst000066400000000000000000000242321353433561000203160ustar00rootroot00000000000000Changelog --------- 0.19.0 (2019-09-05) +++++++++++++++++++ * Drop support for Python 2.7 and 3.5 (:issue:`241`). * Drop support for marshmallow<2.15.2. * Only support sqlalchemy>=1.2.0. 0.18.0 (2019-09-05) +++++++++++++++++++ Features: * ``marshmallow_sqlalchemy.fields.Nested`` propagates the value of ``transient`` on the call to ``load`` (:issue:`177`, :issue:`206`). Thanks :user:`leonidumanskiy` for reporting. Note: This is the last release to support Python 2.7 and 3.5. 0.17.2 (2019-08-31) +++++++++++++++++++ Bug fixes: * Fix error handling when passing an invalid type to ``Related`` (:issue:`223`). Thanks :user:`heckad` for reporting. * Address ``DeprecationWarning`` raised when using ``Related`` with marshmallow 3 (:pr:`243`). 0.17.1 (2019-08-31) +++++++++++++++++++ Bug fixes: * Add ``marshmallow_sqlalchemy.fields.Nested`` field that inherits its session from its schema. This fixes a bug where an exception was raised when using ``Nested`` within a ``ModelSchema`` (:issue:`67`). Thanks :user:`nickw444` for reporting and thanks :user:`samueljsb` for the PR. User code should be updated to use marshmallow-sqlalchemy's ``Nested`` instead of ``marshmallow.fields.Nested``. .. code-block:: python # Before from marshmallow import fields from marshmallow_sqlalchemy import ModelSchema class ArtistSchema(ModelSchema): class Meta: model = models.Artist class AlbumSchema(ModelSchema): class Meta: model = models.Album artist = fields.Nested(ArtistSchema) # After from marshmallow import fields from marshmallow_sqlalchemy import ModelSchema from marshmallow_sqlalchemy.fields import Nested class ArtistSchema(ModelSchema): class Meta: model = models.Artist class AlbumSchema(ModelSchema): class Meta: model = models.Album artist = Nested(ArtistSchema) 0.17.0 (2019-06-22) +++++++++++++++++++ Features: * Add support for ``postgresql.MONEY`` type (:issue:`218`). Thanks :user:`heckad` for the PR. 0.16.4 (2019-06-15) +++++++++++++++++++ Bug fixes: * Compatibility with marshmallow 3.0.0rc7. Thanks :user:`heckad` for the catch and patch. 0.16.3 (2019-05-05) +++++++++++++++++++ Bug fixes: * Compatibility with marshmallow 3.0.0rc6. 0.16.2 (2019-04-10) +++++++++++++++++++ Bug fixes: * Prevent ValueError when using the ``exclude`` class Meta option with ``TableSchema`` (:pr:`202`). 0.16.1 (2019-03-11) +++++++++++++++++++ Bug fixes: * Fix compatibility with SQLAlchemy 1.3 (:issue:`185`). 0.16.0 (2019-02-03) +++++++++++++++++++ Features: * Add support for deserializing transient objects (:issue:`62`). Thanks :user:`jacksmith15` for the PR. 0.15.0 (2018-11-05) +++++++++++++++++++ Features: * Add ``ModelConverter._should_exclude_field`` hook (:pr:`139`). Thanks :user:`jeanphix` for the PR. * Allow field ``kwargs`` to be overriden by passing ``info['marshmallow']`` to column properties (:issue:`21`). Thanks :user:`dpwrussell` for the suggestion and PR. Thanks :user:`jeanphix` for the final implementation. 0.14.2 (2018-11-03) +++++++++++++++++++ Bug fixes: - Fix behavior of ``Related`` field (:issue:`150`). Thanks :user:`zezic` for reporting and thanks :user:`AbdealiJK` for the PR. - ``Related`` now works with ``AssociationProxy`` fields (:issue:`151`). Thanks :user:`AbdealiJK` for the catch and patch. Other changes: - Test against Python 3.7. - Bring development environment in line with marshmallow. 0.14.1 (2018-07-19) +++++++++++++++++++ Bug fixes: - Fix behavior of ``exclude`` with marshmallow 3.0 (:issue:`131`). Thanks :user:`yaheath` for reporting and thanks :user:`deckar01` for the fix. 0.14.0 (2018-05-28) +++++++++++++++++++ Features: - Make ``ModelSchema.session`` a property, which allows session to be retrieved from ``context`` (:issue:`129`). Thanks :user:`gtxm`. Other changes: - Drop official support for Python 3.4. Python>=3.5 and Python 2.7 are supported. 0.13.2 (2017-10-23) +++++++++++++++++++ Bug fixes: - Unset ``instance`` attribute when an error occurs during a ``load`` call (:issue:`114`). Thanks :user:`vgavro` for the catch and patch. 0.13.1 (2017-04-06) +++++++++++++++++++ Bug fixes: - Prevent unnecessary queries when using the `fields.Related` (:issue:`106`). Thanks :user:`xarg` for reporting and thanks :user:`jmuhlich` for the PR. 0.13.0 (2017-03-12) +++++++++++++++++++ Features: - Invalid inputs for compound primary keys raise a ``ValidationError`` when deserializing a scalar value (:issue:`103`). Thanks :user:`YuriHeupa` for the PR. Bug fixes: - Fix compatibility with marshmallow>=3.x. 0.12.1 (2017-01-05) +++++++++++++++++++ Bug fixes: - Reset ``ModelSchema.instance`` after each ``load`` call, allowing schema instances to be reused (:issue:`78`). Thanks :user:`georgexsh` for reporting. Other changes: - Test against Python 3.6. 0.12.0 (2016-10-08) +++++++++++++++++++ Features: - Add support for TypeDecorator-based types (:issue:`83`). Thanks :user:`frol`. Bug fixes: - Fix bug that caused a validation errors for custom column types that have the ``python_type`` of ``uuid.UUID`` (:issue:`54`). Thanks :user:`wkevina` and thanks :user:`kelvinhammond` for the fix. Other changes: - Drop official support for Python 3.3. Python>=3.4 and Python 2.7 are supported. 0.11.0 (2016-10-01) +++++++++++++++++++ Features: - Allow overriding field class returned by ``field_for`` by adding the ``field_class`` param (:issue:`81`). Thanks :user:`cancan101`. 0.10.0 (2016-08-14) +++++++++++++++++++ Features: - Support for SQLAlchemy JSON type (in SQLAlchemy>=1.1) (:issue:`74`). Thanks :user:`ewittle` for the PR. 0.9.0 (2016-07-02) ++++++++++++++++++ Features: - Enable deserialization of many-to-one nested objects that do not exist in the database (:issue:`69`). Thanks :user:`seanharr11` for the PR. Bug fixes: - Depend on SQLAlchemy>=0.9.7, since marshmallow-sqlalchemy uses ``sqlalchemy.dialects.postgresql.JSONB`` (:issue:`65`). Thanks :user:`alejom99` for reporting. 0.8.1 (2016-02-21) ++++++++++++++++++ Bug fixes: - ``ModelSchema`` and ``TableSchema`` respect field order if the ``ordered=True`` class Meta option is set (:issue:`52`). Thanks :user:`jeffwidman` for reporting and :user:`jmcarp` for the patch. - Declared fields are not introspected in order to support, e.g. ``column_property`` (:issue:`57`). Thanks :user:`jmcarp`. 0.8.0 (2015-12-28) ++++++++++++++++++ Features: - ``ModelSchema`` and ``TableSchema`` will respect the ``TYPE_MAPPING`` class variable of Schema subclasses when converting ``Columns`` to ``Fields`` (:issue:`42`). Thanks :user:`dwieeb` for the suggestion. 0.7.1 (2015-12-13) ++++++++++++++++++ Bug fixes: - Don't make marshmallow fields required for non-nullable columns if a column has a default value or autoincrements (:issue:`47`). Thanks :user:`jmcarp` for the fix. Thanks :user:`AdrielVelazquez` for reporting. 0.7.0 (2015-12-07) ++++++++++++++++++ Features: - Add ``include_fk`` class Meta option (:issue:`36`). Thanks :user:`jmcarp`. - Non-nullable columns will generated required marshmallow Fields (:issue:`40`). Thanks :user:`jmcarp`. - Improve support for MySQL BIT field (:issue:`41`). Thanks :user:`rudaporto`. - *Backwards-incompatible*: Remove ``fields.get_primary_columns`` in favor of ``fields.get_primary_keys``. - *Backwards-incompatible*: Remove ``Related.related_columns`` in favor of ``fields.related_keys``. Bug fixes: - Fix serializing relationships when using non-default column names (:issue:`44`). Thanks :user:`jmcarp` for the fix. Thanks :user:`repole` for the bug report. 0.6.0 (2015-09-29) ++++++++++++++++++ Features: - Support for compound primary keys. Thanks :user:`jmcarp`. Other changes: - Supports marshmallow>=2.0.0. 0.5.0 (2015-09-27) ++++++++++++++++++ - Add ``instance`` argument to ``ModelSchema`` constructor and ``ModelSchema.load`` which allows for updating existing DB rows (:issue:`26`). Thanks :user:`sssilver` for reporting and :user:`jmcarp` for the patch. - Don't autogenerate fields that are in ``Meta.exclude`` (:issue:`27`). Thanks :user:`jmcarp`. - Raise ``ModelConversionError`` if converting properties whose column don't define a ``python_type``. Thanks :user:`jmcarp`. - *Backwards-incompatible*: ``ModelSchema.make_object`` is removed in favor of decorated ``make_instance`` method for compatibility with marshmallow>=2.0.0rc2. 0.4.1 (2015-09-13) ++++++++++++++++++ Bug fixes: - Now compatible with marshmallow>=2.0.0rc1. - Correctly pass keyword arguments from ``field_for`` to generated ``List`` fields (:issue:`25`). Thanks :user:`sssilver` for reporting. 0.4.0 (2015-09-03) ++++++++++++++++++ Features: - Add ``TableSchema`` for generating ``Schemas`` from tables (:issue:`4`). Thanks :user:`jmcarp`. Bug fixes: - Allow ``session`` to be passed to ``ModelSchema.validate``, since it requires it. Thanks :user:`dpwrussell`. - When serializing, don't skip overriden fields that are part of a polymorphic hierarchy (:issue:`18`). Thanks again :user:`dpwrussell`. Support: - Docs: Add new recipe for automatic generation of schemas. Thanks :user:`dpwrussell`. 0.3.0 (2015-08-27) ++++++++++++++++++ Features: - *Backwards-incompatible*: Relationships are (de)serialized by a new, more efficient ``Related`` column (:issue:`7`). Thanks :user:`jmcarp`. - Improve support for MySQL types (:issue:`1`). Thanks :user:`rmackinnon`. - Improve support for Postgres ARRAY types (:issue:`6`). Thanks :user:`jmcarp`. - ``ModelSchema`` no longer requires the ``sqla_session`` class Meta option. A ``Session`` can be passed to the constructor or to the ``ModelSchema.load`` method (:issue:`11`). Thanks :user:`dtheodor` for the suggestion. Bug fixes: - Null foreign keys are serialized correctly as ``None`` (:issue:`8`). Thanks :user:`mitchej123`. - Properly handle a relationship specifies ``uselist=False`` (:issue:`#17`). Thanks :user:`dpwrussell`. 0.2.0 (2015-05-03) ++++++++++++++++++ Features: - Add ``field_for`` function for generating marshmallow Fields from SQLAlchemy mapped class properties. Support: - Docs: Add "Overriding generated fields" section to "Recipes". 0.1.1 (2015-05-02) ++++++++++++++++++ Bug fixes: - Fix ``keygetter`` class Meta option. 0.1.0 (2015-04-28) ++++++++++++++++++ - First release. marshmallow-sqlalchemy-0.19.0/CONTRIBUTING.rst000066400000000000000000000070751353433561000207440ustar00rootroot00000000000000Contributing Guidelines ======================= In General ---------- - `PEP 8`_, when sensible. - Test ruthlessly. Write docs for new features. - Even more important than Test-Driven Development--*Human-Driven Development*. .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ In Particular ------------- Questions, Feature Requests, Bug Reports, and Feedback. . . +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ . . .should all be reported on the `Github Issue Tracker`_ . .. _`Github Issue Tracker`: https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues?state=open Setting Up for Local Development ++++++++++++++++++++++++++++++++ 1. Fork marshmallow-sqlalchemy_ on Github. :: $ git clone https://github.com/marshmallow-code/marshmallow-sqlalchemy.git $ cd marshmallow-sqlalchemy 2. Install development requirements. **It is highly recommended that you use a virtualenv.** Use the following command to install an editable version of marshmallow-sqlalchemy along with its development requirements. :: # After activating your virtualenv $ pip install -e '.[dev]' 3. Install the pre-commit hooks, which will format and lint your git staged files. :: # The pre-commit CLI was installed above $ pre-commit install --allow-missing-config Git Branch Structure ++++++++++++++++++++ marshmallow-sqlalchemy abides by the following branching model: ``dev`` Current development branch. **New features should branch off here**. ``X.Y-line`` Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.**. The maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes. **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes. Pull Requests ++++++++++++++ 1. Create a new local branch. :: # For a new feature $ git checkout -b name-of-feature dev # For a bugfix $ git checkout -b fix-something 1.2-line 2. Commit your changes. Write `good commit messages `_. :: $ git commit -m "Detailed commit message" $ git push origin name-of-feature 3. Before submitting a pull request, check the following: - If the pull request adds functionality, it is tested and the docs are updated. - You've added yourself to ``AUTHORS.rst``. 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `CI `_ build must be passing before your pull request is merged. Running Tests +++++++++++++ To run all To run all tests: :: $ pytest To run syntax checks: :: $ tox -e lint (Optional) To run tests on Python 2.7, 3.5, 3.6, and 3.7 virtual environments (must have each interpreter installed): :: $ tox Documentation +++++++++++++ Contributions to the documentation are welcome. Documentation is written in `reStructured Text`_ (rST). A quick rST reference can be found `here `_. Builds are powered by Sphinx_. To build the docs in "watch" mode: :: $ tox -e watch-docs Changes in the `docs/` directory will automatically trigger a rebuild. .. _Sphinx: http://sphinx.pocoo.org/ .. _`reStructured Text`: http://docutils.sourceforge.net/rst.html .. _`marshmallow-sqlalchemy`: https://github.com/marshmallow-code/marshmallow-sqlalchemy marshmallow-sqlalchemy-0.19.0/LICENSE000066400000000000000000000020621353433561000172770ustar00rootroot00000000000000Copyright 2015-2019 Steven Loria and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. marshmallow-sqlalchemy-0.19.0/MANIFEST.in000066400000000000000000000003211353433561000200240ustar00rootroot00000000000000include *.rst LICENSE recursive-include tests * recursive-include docs * recursive-exclude docs *.pyc recursive-exclude docs *.pyo recursive-exclude tests *.pyc recursive-exclude tests *.pyo prune docs/_build marshmallow-sqlalchemy-0.19.0/README.rst000066400000000000000000000072551353433561000177720ustar00rootroot00000000000000********************** marshmallow-sqlalchemy ********************** |pypi-package| |build-status| |docs| |marshmallow23| |black| Homepage: https://marshmallow-sqlalchemy.readthedocs.io/ `SQLAlchemy `_ integration with the `marshmallow `_ (de)serialization library. Declare your models =================== .. code-block:: python import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref engine = sa.create_engine("sqlite:///:memory:") session = scoped_session(sessionmaker(bind=engine)) Base = declarative_base() class Author(Base): __tablename__ = "authors" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String) def __repr__(self): return "".format(self=self) class Book(Base): __tablename__ = "books" id = sa.Column(sa.Integer, primary_key=True) title = sa.Column(sa.String) author_id = sa.Column(sa.Integer, sa.ForeignKey("authors.id")) author = relationship("Author", backref=backref("books")) Base.metadata.create_all(engine) Generate marshmallow schemas ============================ .. code-block:: python from marshmallow_sqlalchemy import ModelSchema class AuthorSchema(ModelSchema): class Meta: model = Author class BookSchema(ModelSchema): class Meta: model = Book # optionally attach a Session # to use for deserialization sqla_session = session author_schema = AuthorSchema() (De)serialize your data ======================= .. code-block:: python author = Author(name="Chuck Paluhniuk") author_schema = AuthorSchema() book = Book(title="Fight Club", author=author) session.add(author) session.add(book) session.commit() dump_data = author_schema.dump(author) # {'books': [123], 'id': 321, 'name': 'Chuck Paluhniuk'} author_schema.load(dump_data, session=session) # Get it now ========== :: pip install -U marshmallow-sqlalchemy Documentation ============= Documentation is available at https://marshmallow-sqlalchemy.readthedocs.io/ . Project Links ============= - Docs: https://marshmallow-sqlalchemy.readthedocs.io/ - Changelog: https://marshmallow-sqlalchemy.readthedocs.io/en/latest/changelog.html - PyPI: https://pypi.python.org/pypi/marshmallow-sqlalchemy - Issues: https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues License ======= MIT licensed. See the bundled `LICENSE `_ file for more details. .. |pypi-package| image:: https://badgen.net/pypi/v/marshmallow-sqlalchemy :target: https://pypi.org/project/marshmallow-sqlalchemy/ :alt: Latest version .. |build-status| image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.marshmallow-sqlalchemy?branchName=dev :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=10&branchName=dev :alt: Build status .. |docs| image:: https://readthedocs.org/projects/marshmallow-sqlalchemy/badge/ :target: http://marshmallow-sqlalchemy.readthedocs.io/ :alt: Documentation .. |marshmallow23| image:: https://badgen.net/badge/marshmallow/2,3?list=1 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html :alt: marshmallow 3 compatible .. |black| image:: https://badgen.net/badge/code%20style/black/000 :target: https://github.com/ambv/black :alt: code style: black marshmallow-sqlalchemy-0.19.0/azure-pipelines.yml000066400000000000000000000014441353433561000221340ustar00rootroot00000000000000trigger: branches: include: [dev, test-me-*] tags: include: ['*'] # Run builds nightly to catch incompatibilities with new marshmallow releases schedules: - cron: "0 0 * * *" displayName: Daily midnight build branches: include: - dev always: "true" resources: repositories: - repository: sloria type: github endpoint: github name: sloria/azure-pipeline-templates ref: refs/heads/sloria jobs: - template: job--python-tox.yml@sloria parameters: toxenvs: - lint - py36-lowest - py36-marshmallow2 - py36-marshmallow3 - py37-marshmallow2 - py37-marshmallow3 - py37-marshmallowdev - docs os: linux - template: job--pypi-release.yml@sloria parameters: dependsOn: - tox_linux marshmallow-sqlalchemy-0.19.0/docs/000077500000000000000000000000001353433561000172225ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/docs/Makefile000066400000000000000000000151721353433561000206700ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." marshmallow-sqlalchemy-0.19.0/docs/_static/000077500000000000000000000000001353433561000206505ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/docs/_static/marshmallow-sqlalchemy-logo.png000066400000000000000000000306031353433561000270040ustar00rootroot00000000000000PNG  IHDR,n.sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATx{|Tյ3$B$$ bb)D(bk E,bZk+^Z5yȕZbQ4\* 0 $!$Lℜ{̙}Μ@p.'W-!x `YW F(~qN`>) \8r* 8/(`psp8qZݿUS eS^[ca|h-)@]goNNGn!,h Ga98TQX' rpp8ipINz:= DvD@"`o7M9y945?" > Hh~;>Y0D;<YbB uSӀшp7dCtD=oSJa?(K\HS8`548`jxo  e\D~w O8lA٘s` RN߁_+;| 7xiZXiF`6cS xbUמ 6q%GMg?E } ˀr+ L!*f"CENЃ͈pfDerKlwNFg{/ˆQc ;,~{dth|1Ӂ $Oفs |gP#:/Q?[+ |~Jbt~`!, hN6I fH5݃謹b3s crQ?!r gt BV.6"G2Hr.-PG썈7Ύܗ,A$N|/b ]sT sہ"CS-bܪRRc.M[0s܈{'b:eL;^E(u{jڮ1f#*yɆS;m#n,9a](dZ *p=aUcbo*F(jv-|k؋hoBǯ-u\lq쀜W7 x6+  ہ o@6xA6"'BXbV홈MJm)5Ľ&5ǍB^}Tڎzxxda[OGdE5?j"S3NoԦa:¼oE J}܈ߧi!W&N1lq%Y׊*V{a;>/-R mb; PYP6F":~;IcflH<"EWsjzbUcTD7lMm?b`F/]o4k10a<n=tݧllmS p-i皈x9l1} -qam #|"zfB %E(e=C˛ϝO[әku|/fJ86, yѥ]Qa a]|HEO# -J.0|g5=#^I)+@XK>TEN |\V\k|b34{N0ՎXc68$*4־#ߎXO;D'3z<*Y!$Ow1ͲZEz9ӊe%TQbJ Jj Yal{{WxV*G_⸣$ ffL lNwD]Cz8>Zl;Xa4è|TH>b굄PE/6ȃ* ?P|RTF RCxzc8/|VIO{O2P .=ڿR>_3 +_UzHoGdT"l#ē2#X+?J@Tǘ[ | ,lo;}ELB \ǻd0OltDf#.t( Q[nh<}n)ae EW 5#Vb}c837|>۱@|I"ʊ< h?F7qF^aME;Hju@/}zT+VxN\$č~ [1]4 :@& [c]&i)34m&bEyt/L i_ƀhF"te6Ѕ8D]w[ }pBXTz!RfOT1,7ҕoİ__=i"5lxra;֖5IO1]Wj8+r;VcvMMiﮢ+t8!BГ|GMLKD}*('| S+"h5Ŷzx%8'>o G!#^ǻhxtw:-p *A^\x|pWX0>%%`U)+8q:n"r02:'{p"ӯ^݋HQ(.1Nc~oz_ks$At#ǜ ˅HhǕ_x(oLUNfKx*GLo2:H"r }v!zNv1]1jCTD|Fj2gԬ^dVXHO2h*JDm']acE8 臽jz}IcDD默0LlںɬT['rld}=+)U*& vzn2|~X*xo/RFqZ"\vVf7;E@?}V|gLy$#bs{::_U}.ǺJ#{<6w+x1ExdVmz#ݍ(t7$h]5Fxc@s-#+8k"rZ_VV3*)mu.0D=%=b= |"}F <5?Le [9cTR3{BOLqO`zEx9DR\H|3/׻pKU .A+4uM,?r D K#Y*dԓw}WX>빍@,*ϯUB޾ICtm&QU}*M(!h\ՍCeW5}1b_ ӄX}e#DD@DM#ɪtY(cb\EL. |^%d3D]Q2DMB˕D<]/GLSGD뙂H9uog"Z(B, R "]9UmJW'Usdo] D!-F&dɖ5k= WT``gPHOAԮ=[+A+#~ ]cҕ&6UuIXy{+5 t=?g*WTOGDә]9cGev29?~t5 ` o6Uo=1gOUx0\v.bo^>>ϛW֟LUoš}Jcn; q7/xX4=bs;11ªzw[@s7BCe۩V+X)8D P;SZmُx3:pb4 Ct#J$!,r #x}dW<ă.a}#RBKJBMM fg#MWOX|xp^hz*D 2 Go*MޣVEEkN55PYYINNٜwy*Dyy9[no&999fY{wVV5\c㡠2L6=^WV =;!.\5l^o͡Y$ɽ/$2xȐ/LLLCѣE*W׭r[ufqɥt R]]͜9s8HNNfȑQ m˖-FCEaa!ͳkF}lG蓧}Kvmз-~3ԨHQٙg`ȑzhSQ񐛛LORz*))і.]j[ATWW+yu:5g͖-[0vXڢz677kr-^[nŋ3r0 k .9ӤKO}'h^x!n`kǂ CBf6(éAVEEfy䑘KOAA7lJaڑfb¹ SYYIMMMp kZe\\fo*cizn)6hݵfT K.[=,"U=ᕰs3F1[YaaoZǧ0pL Q΃^7`.Ԯ\u^ĐIDZʷk\MW}{P:W ~ou#M%t|Y;3皎Y+_}M;mi>%qmU\\:I%#U8cxl^]fg7T֕x=$⾱ƊtnZG'h9=|z|o"͚!k)߮U43o2=4c U狫,q-۵/= u\k:2rm),׫x^49b^7zU\r,c7nܨTҹ. bh񮊊 *D@2bkMJC;2Snnاtۗ]73 h.~Zx71";^2s匨U$屛_ 땐~p|9AE_/ g8WU#Liui n#Ld. bu%'$$Pry}2XL1ֶO?5Xt; lI'Wf̘Annnp;USR'0+\˖-TZ/o?Ҹ.!)#33 L{ϽYY+kuKf *+g>v'om6V[?sS%'nz)>o*}m3ݍ6x_.T%_fvpLδ]ss=sY ]RQZZJkkٸarUOڵkYzuvD6g0}"Cwm(xŌ\MR547aaR*o[7tLiTGןμJboI߯}qB̝4Ik >\G[ɸ~Ky6;obϞ=|}0?ezA~/ Luy26)H-@xi:qn֭[Yf w Ԑh!*zZ -۵}^>ߙ\z>|Ϭ,h 6N 1,!V ]#x3Bq_e9mmmFD/im3+_PCj._c{jX oj7 Q{WI_{Əo7߶/l:2י/-L{l>$m)cZ_ SV['hw;&]^Y'\t-HǻJ6cJXYi鍬%~k: 芬[; 9}3;( i}NHsJJWˑAK`padpRI`6"0dOM>DYU޶g$z2S S/sYYi{M9ƌs Sv7s4?"aKa X=Քk QVB -š _YY,B9ѾOuVn:V\}Ӑa (qS]ՙO8mgrVvgZ Eɸz PV:2hOOٱ`VdV sRh<3(l3sÎ" VMʲTr/2,(mt)3e B&}o)))!~ ooy")-[)))H3}l++ ~֧ʀ(6ly:T)))e˖ifNj*++u{( {h?PȼEIDAT˯FjW.qtI{*'^koZ?y?X`ϸ#s? d^O{z1&\H}z[(!õWM%)#3X@;2X/L?޲Kii)999svؾB}ĉ*9uvRwmipD}VfVV`jqYmqS]~_kkkcdOӮlfe:2Dl䩚!wd' jy۠?`﹓St]X馝Sz LBӃV\! kС.zeeLّQVVL`@fo߯EUWWSZZb`f^'`baKIʴHߛVֱZ*>&)K I[ya(hu|YXBI\~-m/p`1::2hLf}FMxkǂi,e*iĖKRPPzg"T*33" :%KϘ!yjkk-#r{6heeeo)8Ҥ5dW Ymk EWk&*u,詾3s_|0ս CvQ'؏9G}իW+hfJu-}SƊ͋XSN['Mp˖-}>g13V"Rwm~NLmJoZaOU2m)}m;5i8k++g9jaHEm$o^sfggSSSøyA,˜pz*3k׮&cGi%V頌x<|>0vδ 2'׹o_VjZ4az|ĔC-JO Tze ҶGm`/܈vrg2>X)=HPikk :̙Ô)S&]wi؛oVP"3fˁ&,i`oPRR֦쬇t}ѹkEMab(=}?-cBqg,wY)#$S4fje23 teа^%%vDj|Xz_v-EEEݵ3*ez֒S C~[2VWW#zj>c@̲J9sohwh5+G^Xae+aE_;,㲲W/jЧ -喁h}{_3Z(I.0VYݫZQ~DʘyU\Fu4 UXܬ_LZ]]}_ߤI&K%_R2-D^^ 0dQTT0[SXQ17F*!J"l*.]Vʡkb/`h$i4ng0p2.1izпW1m|e3bTIcY#+vc.}0/ #oEFTSMX>wY)oӆ% !72FqߪYv-k׮ *e˖iٜ20xU}-c!> sivVζcǬkrz@z{cj}.TB}5_ݭk;hشNnןnQe% .#9ne&&O~:p6,ҙ߇^[kZȼi) .6=nz*92BZu7Xx />iJ:tEѨzm +X, Vq.׫XƂ^Y1Z[7l@LkxP*8=Xqף/0ux( zw}zr~R7A찝i4//do*ʃŽR=yѽp -.KKE*v߰i@m][l* \Y)oJYEO~ EA+Q;@~6nt͘_j|NRe=x=JHz'*3XY/E@r$=*+;Z٦{!:T)ztI2am$m^\ejՙA[*Y~iݫAk YaTh䫪b˖-Q)kTQFbD %j7MP!뀛]Kp&WNfOXѡ7.:1ϻ-Xu ++-}C\9-9 [m*sŴ{Yi]HrgTu+u}ϙX\0ʟyRLэQoAnnx0"#GVm K%W@v܉&X*Z~}|~_bҤIlٲED V[vۊx<+;-!!IQ˶>X d@S\+nDc6jm_ø%ަSGճe1[*r j*wEV+fr /?P|ȄmrmQf9-|ٳ&_obֿlUB&|>8 ֣ 4 ,2)Mmk @#V[]ko%{+3!> mL> p7A;rU܋LW1A雊rP8ZG,(RTIOOgt7-?`ܾÃU[}E9PKDҵENZ?y!*sT¢mE0KJ-侧 ɦ3*xM7˶`;9JCmf /06r/meDC(JU#;Zܱ$Z_hHNN"\줯HelTxK"ߊjoF%UPY[X-?Mu/䵙 T|ѹޝ@+N+VDUXXHAA\pA`bKÉN0BFp?s41>*Kɨ޸yxqazcDJ9[x?~PqAlD{G3gӧOﳕGƨ P&|NAD[~(t *,,Dd?K-!п6⒰@uweNJb"W >e9s;v,J VH|>_RU"|x$ "j985ME-FDW=QbU tN45kXdMiim- :Q;8esO٩vTdpn0jBIq),,4]Yead"!4t#ךi2ԃ۸U*}]2)zCnne)5vrݻw[+ш\Y3f ޽}9s搓#޲NdTyytϡipZG|ߣD?0wc_3%3&Bl2c [K_(cLNf+w\|u0M*1!!+w>X"HF۩vJ),=ɨ͛g@G|p/1y&sₐH" fjq@Mq8{8`6)^qEBVMÉFAo0l7m.@eNƉ3"mS)cdOAA@Q985C,鴵żR];m\V[@YE3Nd}BlͱkdGY9J+IZkMo'>m02Ļ6ԜFGdwé=ѷ/ZKPXQRnj3^l|4xIENDB`marshmallow-sqlalchemy-0.19.0/docs/_templates/000077500000000000000000000000001353433561000213575ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/docs/_templates/_templates/000077500000000000000000000000001353433561000235145ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/docs/_templates/_templates/useful-links.html000066400000000000000000000002241353433561000270210ustar00rootroot00000000000000

Useful Links

    {% for text, uri in theme_extra_nav_links.items() %}
  • {{text}}
  • {% endfor %}
marshmallow-sqlalchemy-0.19.0/docs/_templates/useful-links.html000066400000000000000000000002241353433561000246640ustar00rootroot00000000000000

Useful Links

    {% for text, uri in theme_extra_nav_links.items() %}
  • {{text}}
  • {% endfor %}
marshmallow-sqlalchemy-0.19.0/docs/api_reference.rst000066400000000000000000000010661353433561000225460ustar00rootroot00000000000000.. _api: ************* API Reference ************* Core ==== .. automodule:: marshmallow_sqlalchemy :members: .. autodata:: marshmallow_sqlalchemy.fields_for_model :annotation: =func(...) .. autodata:: marshmallow_sqlalchemy.property2field :annotation: =func(...) .. autodata:: marshmallow_sqlalchemy.column2field :annotation: =func(...) .. autodata:: marshmallow_sqlalchemy.field_for :annotation: =func(...) Fields ====== .. automodule:: marshmallow_sqlalchemy.fields :members: :private-members: marshmallow-sqlalchemy-0.19.0/docs/authors.rst000066400000000000000000000000341353433561000214360ustar00rootroot00000000000000.. include:: ../AUTHORS.rst marshmallow-sqlalchemy-0.19.0/docs/changelog.rst000066400000000000000000000000561353433561000217040ustar00rootroot00000000000000.. _changelog: .. include:: ../CHANGELOG.rst marshmallow-sqlalchemy-0.19.0/docs/conf.py000077500000000000000000000043771353433561000205370ustar00rootroot00000000000000from collections import OrderedDict import datetime as dt import os import sys import alabaster sys.path.insert(0, os.path.abspath(os.path.join("..", "src"))) import marshmallow_sqlalchemy # noqa: E402 extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_issues", ] primary_domain = "py" default_role = "py:obj" intersphinx_mapping = { "python": ("https://python.readthedocs.io/en/latest/", None), "marshmallow": ("https://marshmallow.readthedocs.io/en/latest/", None), "sqlalchemy": ("http://www.sqlalchemy.org/docs/", None), } issues_github_path = "marshmallow-code/marshmallow-sqlalchemy" source_suffix = ".rst" master_doc = "index" project = "marshmallow-sqlalchemy" copyright = "Steven Loria and contributors {:%Y}".format(dt.datetime.utcnow()) version = release = marshmallow_sqlalchemy.__version__ exclude_patterns = ["_build"] html_theme_path = [alabaster.get_path()] html_theme = "alabaster" html_static_path = ["_static"] templates_path = ["_templates"] html_show_sourcelink = False html_theme_options = { "logo": "marshmallow-sqlalchemy-logo.png", "description": "SQLAlchemy integration with the marshmallow (de)serialization library", "description_font_style": "italic", "github_user": "marshmallow-code", "github_repo": "marshmallow-sqlalchemy", "github_banner": True, "github_button": False, "code_font_size": "0.85em", "warn_bg": "#FFC", "warn_border": "#EEE", # Used to populate the useful-links.html template "extra_nav_links": OrderedDict( [ ( "marshmallow-sqlalchemy @ PyPI", "http://pypi.python.org/pypi/marshmallow-sqlalchemy", ), ( "marshmallow-sqlalchemy @ GitHub", "http://github.com/marshmallow-code/marshmallow-sqlalchemy", ), ( "Issue Tracker", "http://github.com/marshmallow-code/marshmallow-sqlalchemy/issues", ), ] ), } html_sidebars = { "index": ["about.html", "useful-links.html", "searchbox.html"], "**": [ "about.html", "useful-links.html", "localtoc.html", "relations.html", "searchbox.html", ], } marshmallow-sqlalchemy-0.19.0/docs/contributing.rst000066400000000000000000000000411353433561000224560ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst marshmallow-sqlalchemy-0.19.0/docs/index.rst000066400000000000000000000052071353433561000210670ustar00rootroot00000000000000********************** marshmallow-sqlalchemy ********************** Release v\ |version| (:ref:`Changelog `) `SQLAlchemy `_ integration with the `marshmallow `_ (de)serialization library. Declare your models =================== .. code-block:: python import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref engine = sa.create_engine("sqlite:///:memory:") session = scoped_session(sessionmaker(bind=engine)) Base = declarative_base() class Author(Base): __tablename__ = "authors" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String) def __repr__(self): return "".format(self=self) class Book(Base): __tablename__ = "books" id = sa.Column(sa.Integer, primary_key=True) title = sa.Column(sa.String) author_id = sa.Column(sa.Integer, sa.ForeignKey("authors.id")) author = relationship("Author", backref=backref("books")) Base.metadata.create_all(engine) Generate marshmallow schemas ============================ .. code-block:: python from marshmallow_sqlalchemy import ModelSchema class AuthorSchema(ModelSchema): class Meta: model = Author class BookSchema(ModelSchema): class Meta: model = Book # optionally attach a Session # to use for deserialization sqla_session = session author_schema = AuthorSchema() Make sure to declare `Models` before instantiating `Schemas`. Otherwise `sqlalchemy.orm.configure_mappers() `_ will run too soon and fail. (De)serialize your data ======================= .. code-block:: python author = Author(name="Chuck Paluhniuk") author_schema = AuthorSchema() book = Book(title="Fight Club", author=author) session.add(author) session.add(book) session.commit() dump_data = author_schema.dump(author) print(dump_data) # {'books': [123], 'id': 321, 'name': 'Chuck Paluhniuk'} load_data = author_schema.load(dump_data, session=session) print(load_data) # Get it now ========== :: pip install -U marshmallow-sqlalchemy Learn ===== .. toctree:: :maxdepth: 2 recipes API === .. toctree:: :maxdepth: 2 api_reference Project Info ============ .. toctree:: :maxdepth: 1 changelog contributing authors license marshmallow-sqlalchemy-0.19.0/docs/license.rst000066400000000000000000000000701353433561000213730ustar00rootroot00000000000000******* License ******* .. literalinclude:: ../LICENSE marshmallow-sqlalchemy-0.19.0/docs/make.bat000066400000000000000000000145031353433561000206320ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end marshmallow-sqlalchemy-0.19.0/docs/recipes.rst000066400000000000000000000223121353433561000214060ustar00rootroot00000000000000.. _recipes: ******* Recipes ******* Base Schema I ============= A common pattern with `marshmallow` is to define a base `Schema ` class which has common configuration and behavior for your application's `Schemas`. You may want to define a common session object, e.g. a `scoped_session ` to use for all `Schemas `. .. code-block:: python # myproject/db.py import sqlalchemy as sa from sqlalchemy import orm Session = orm.scoped_session(orm.sessionmaker()) Session.configure(bind=engine) .. code-block:: python # myproject/schemas.py from marshmallow_sqlalchemy import ModelSchema from .db import Session class BaseSchema(ModelSchema): class Meta: sqla_session = Session .. code-block:: python :emphasize-lines: 9 # myproject/users/schemas.py from ..schemas import BaseSchema from .models import User class UserSchema(BaseSchema): # Inherit BaseSchema's options class Meta(BaseSchema.Meta): model = User Base Schema II ============== Here is an alternative way to define a BaseSchema class with a common ``Session`` object. .. code-block:: python # myproject/schemas.py from marshmallow_sqlalchemy import ModelSchemaOpts from .db import Session class BaseOpts(ModelSchemaOpts): def __init__(self, meta, ordered=False): if not hasattr(meta, "sqla_session"): meta.sqla_session = Session super(BaseOpts, self).__init__(meta, ordered=ordered) class BaseSchema(ModelSchema): OPTIONS_CLASS = BaseOpts This allows you to define class Meta options without having to subclass ``BaseSchema.Meta``. .. code-block:: python :emphasize-lines: 8 # myproject/users/schemas.py from ..schemas import BaseSchema from .models import User class UserSchema(BaseSchema): class Meta: model = User Introspecting Generated Fields ============================== It is often useful to introspect what fields are generated for a `ModelSchema `. Generated fields are added to a `Schema's` ``_declared_fields`` attribute. .. code-block:: python AuthorSchema._declared_fields["books"] # , ...> You can also use `marshmallow_sqlalchemy's` conversion functions directly. .. code-block:: python from marshmallow_sqlalchemy import property2field id_prop = Author.__mapper__.get_property("id") property2field(id_prop) # , ...> Overriding Generated Fields =========================== Any field generated by a `ModelSchema ` can be overridden. .. code-block:: python from marshmallow import fields from marshmallow_sqlalchemy import ModelSchema from marshmallow_sqlalchemy.fields import Nested class AuthorSchema(ModelSchema): # Override books field to use a nested representation rather than pks books = Nested(BookSchema, many=True, exclude=("author",)) class Meta: model = Author sqla_session = Session You can use the `field_for ` function to generate a marshmallow `Field ` based on single model property. This is useful for passing additional keyword arguments to the generated field. .. code-block:: python from marshmallow_sqlalchemy import ModelSchema, field_for class AuthorSchema(ModelSchema): # Generate a field, passing in an additional dump_only argument date_created = field_for(Author, "date_created", dump_only=True) class Meta: model = Author sqla_session = Session You can customize the keyword arguments passed to a column property's corresponding marshmallow field by passing the ``info`` argument to the `Column`. .. code-block:: python class Book(Model): # ... abstract = Column(Text(), info=dict(marshmallow=dict(required=True))) Automatically Generating Schemas For SQLAlchemy Models ====================================================== It can be tedious to implement a large number of schemas if not overriding any of the generated fields as detailed above. SQLAlchemy has a hook that can be used to trigger the creation of the schemas, assigning them to the SQLAlchemy model property ``Model.__marshmallow__``. .. code-block:: python from marshmallow_sqlalchemy import ModelConversionError, ModelSchema def setup_schema(Base, session): # Create a function which incorporates the Base and session information def setup_schema_fn(): for class_ in Base._decl_class_registry.values(): if hasattr(class_, "__tablename__"): if class_.__name__.endswith("Schema"): raise ModelConversionError( "For safety, setup_schema can not be used when a" "Model class ends with 'Schema'" ) class Meta(object): model = class_ sqla_session = session schema_class_name = "%sSchema" % class_.__name__ schema_class = type(schema_class_name, (ModelSchema,), {"Meta": Meta}) setattr(class_, "__marshmallow__", schema_class) return setup_schema_fn An example of then using this: .. code-block:: python import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy import event from sqlalchemy.orm import mapper # Either import or declare setup_schema here engine = sa.create_engine("sqlite:///:memory:") session = scoped_session(sessionmaker(bind=engine)) Base = declarative_base() class Author(Base): __tablename__ = "authors" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String) def __repr__(self): return "".format(self=self) # Listen for the SQLAlchemy event and run setup_schema. # Note: This has to be done after Base and session are setup event.listen(mapper, "after_configured", setup_schema(Base, session)) Base.metadata.create_all(engine) author = Author(name="Chuck Paluhniuk") session.add(author) session.commit() # Model.__marshmallow__ returns the Class not an instance of the schema # so remember to instantiate it author_schema = Author.__marshmallow__() print(author_schema.dump(author)) This is inspired by functionality from ColanderAlchemy. Smart Nested Field ================== To serialize nested attributes to primary keys unless they are already loaded, you can use this custom field. .. code-block:: python from marshmallow_sqlalchemy.fields import Nested class SmartNested(Nested): def serialize(self, attr, obj, accessor=None): if attr not in obj.__dict__: return {"id": int(getattr(obj, attr + "_id"))} return super(SmartNested, self).serialize(attr, obj, accessor) An example of then using this: .. code-block:: python from marshmallow_sqlalchemy import ModelSchema class BookSchema(ModelSchema): author = SmartNested(AuthorSchema) class Meta: model = Book sqla_session = Session book = Book(id=1) book.author = Author(name="Chuck Paluhniuk") session.add(author) session.commit() book = Book.query.get(1) print(BookSchema().dump(book)["author"]) # {'id': 1} book = Book.query.options(joinedload("author")).get(1) print(BookSchema().dump(book)["author"]) # {'id': 1, 'name': 'Chuck Paluhniuk'} Transient Object Creation ========================= Sometimes it might be desirable to deserialize instances that are transient (not attached to a session). In these cases you can specify the `transient` option in the `Meta ` class of a `ModelSchema `. .. code-block:: python from marshmallow_sqlalchemy import ModelSchema class AuthorSchema(ModelSchema): class Meta: model = Author transient = True dump_data = {"id": 1, "name": "John Steinbeck"} print(AuthorSchema().load(dump_data)) # You may also explicitly specify an override by passing the same argument to `load `. .. code-block:: python from marshmallow_sqlalchemy import ModelSchema class AuthorSchema(ModelSchema): class Meta: model = Author sqla_session = session dump_data = {"id": 1, "name": "John Steinbeck"} print(AuthorSchema().load(dump_data, transient=True)) # Note that transience propagates to relationships (i.e. auto-generated schemas for nested items will also be transient). .. seealso:: See `State Management `_ to understand session state management. marshmallow-sqlalchemy-0.19.0/pytest.ini000066400000000000000000000002041353433561000203170ustar00rootroot00000000000000[pytest] filterwarnings = ignore:.*Binary.*:sqlalchemy.exc.SADeprecationWarning ignore:.*Decimal.*:sqlalchemy.exc.SAWarning marshmallow-sqlalchemy-0.19.0/setup.cfg000066400000000000000000000005511353433561000201140ustar00rootroot00000000000000[bdist_wheel] # This flag says that the code is written to work on both Python 2 and Python # 3. If at all possible, it is good practice to do this. If you cannot, you # will need to generate wheels for each Python version that you support. universal=1 [flake8] ignore = E203, E266, E501, W503 max-line-length = 80 max-complexity = 18 select = B,C,E,F,W,T4,B9 marshmallow-sqlalchemy-0.19.0/setup.py000066400000000000000000000042451353433561000200110ustar00rootroot00000000000000import re from setuptools import setup, find_packages INSTALL_REQUIRES = ("marshmallow>=2.15.2", "SQLAlchemy>=1.2.0") EXTRAS_REQUIRE = { "tests": ["pytest", "mock"], "lint": ["flake8==3.7.8", "flake8-bugbear==19.8.0", "pre-commit~=1.18"], "docs": ["sphinx==2.2.0", "alabaster==0.7.12", "sphinx-issues==1.2.0"], } EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] def find_version(fname): """Attempts to find the version number in the file names fname. Raises RuntimeError if not found. """ version = "" with open(fname, "r") as fp: reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') for line in fp: m = reg.match(line) if m: version = m.group(1) break if not version: raise RuntimeError("Cannot find version information") return version def read(fname): with open(fname) as fp: content = fp.read() return content setup( name="marshmallow-sqlalchemy", version=find_version("src/marshmallow_sqlalchemy/__init__.py"), description="SQLAlchemy integration with the marshmallow (de)serialization library", long_description=read("README.rst"), author="Steven Loria", author_email="sloria1@gmail.com", url="https://github.com/marshmallow-code/marshmallow-sqlalchemy", packages=find_packages("src"), package_dir={"": "src"}, include_package_data=True, install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, license="MIT", zip_safe=False, keywords="sqlalchemy marshmallow", classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", ], test_suite="tests", project_urls={ "Changelog": "https://marshmallow-sqlalchemy.readthedocs.io/en/latest/changelog.html", "Issues": "https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues", "Funding": "https://opencollective.com/marshmallow", }, ) marshmallow-sqlalchemy-0.19.0/src/000077500000000000000000000000001353433561000170615ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/src/marshmallow_sqlalchemy/000077500000000000000000000000001353433561000236315ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/src/marshmallow_sqlalchemy/__init__.py000066400000000000000000000007641353433561000257510ustar00rootroot00000000000000from .schema import TableSchemaOpts, ModelSchemaOpts, TableSchema, ModelSchema from .convert import ( ModelConverter, fields_for_model, property2field, column2field, field_for, ) from .exceptions import ModelConversionError __version__ = "0.19.0" __all__ = [ "TableSchema", "ModelSchema", "TableSchemaOpts", "ModelSchemaOpts", "ModelConverter", "fields_for_model", "property2field", "column2field", "ModelConversionError", "field_for", ] marshmallow-sqlalchemy-0.19.0/src/marshmallow_sqlalchemy/convert.py000066400000000000000000000257111353433561000256710ustar00rootroot00000000000000import inspect import functools import uuid import marshmallow as ma from marshmallow import validate, fields from sqlalchemy.dialects import postgresql, mysql, mssql import sqlalchemy as sa from .exceptions import ModelConversionError from .fields import Related, RelatedList def _is_field(value): return isinstance(value, type) and issubclass(value, fields.Field) def _has_default(column): return ( column.default is not None or column.server_default is not None or _is_auto_increment(column) ) def _is_auto_increment(column): return column.table is not None and column is column.table._autoincrement_column def _postgres_array_factory(converter, data_type): return functools.partial( fields.List, converter._get_field_class_for_data_type(data_type.item_type) ) class ModelConverter: """Class that converts a SQLAlchemy model into a dictionary of corresponding marshmallow `Fields `. """ SQLA_TYPE_MAPPING = { sa.Enum: fields.Field, sa.JSON: fields.Raw, postgresql.BIT: fields.Integer, postgresql.UUID: fields.UUID, postgresql.MACADDR: fields.String, postgresql.INET: fields.String, postgresql.JSON: fields.Raw, postgresql.JSONB: fields.Raw, postgresql.HSTORE: fields.Raw, postgresql.ARRAY: _postgres_array_factory, postgresql.MONEY: fields.Decimal, mysql.BIT: fields.Integer, mysql.YEAR: fields.Integer, mysql.SET: fields.List, mysql.ENUM: fields.Field, mssql.BIT: fields.Integer, } DIRECTION_MAPPING = {"MANYTOONE": False, "MANYTOMANY": True, "ONETOMANY": True} def __init__(self, schema_cls=None): self.schema_cls = schema_cls @property def type_mapping(self): if self.schema_cls: return self.schema_cls.TYPE_MAPPING else: return ma.Schema.TYPE_MAPPING def fields_for_model( self, model, *, include_fk=False, fields=None, exclude=None, base_fields=None, dict_cls=dict, ): result = dict_cls() base_fields = base_fields or {} for prop in model.__mapper__.iterate_properties: if self._should_exclude_field(prop, fields=fields, exclude=exclude): # Allow marshmallow to validate and exclude the field key. result[prop.key] = None continue if hasattr(prop, "columns"): if not include_fk: # Only skip a column if there is no overriden column # which does not have a Foreign Key. for column in prop.columns: if not column.foreign_keys: break else: continue field = base_fields.get(prop.key) or self.property2field(prop) if field: result[prop.key] = field return result def fields_for_table( self, table, *, include_fk=False, fields=None, exclude=None, base_fields=None, dict_cls=dict, ): result = dict_cls() base_fields = base_fields or {} for column in table.columns: if self._should_exclude_field(column, fields=fields, exclude=exclude): # Allow marshmallow to validate and exclude the field key. result[column.key] = None continue if not include_fk and column.foreign_keys: continue field = base_fields.get(column.key) or self.column2field(column) if field: result[column.key] = field return result def property2field(self, prop, *, instance=True, field_class=None, **kwargs): field_class = field_class or self._get_field_class_for_property(prop) if not instance: return field_class field_kwargs = self._get_field_kwargs_for_property(prop) field_kwargs.update(kwargs) ret = field_class(**field_kwargs) if ( hasattr(prop, "direction") and self.DIRECTION_MAPPING[prop.direction.name] and prop.uselist is True ): ret = RelatedList(ret, **kwargs) return ret def column2field(self, column, *, instance=True, **kwargs): field_class = self._get_field_class_for_column(column) if not instance: return field_class field_kwargs = self.get_base_kwargs() self._add_column_kwargs(field_kwargs, column) field_kwargs.update(kwargs) return field_class(**field_kwargs) def field_for(self, model, property_name, **kwargs): prop = model.__mapper__.get_property(property_name) return self.property2field(prop, **kwargs) def _get_field_class_for_column(self, column): return self._get_field_class_for_data_type(column.type) def _get_field_class_for_data_type(self, data_type): field_cls = None types = inspect.getmro(type(data_type)) # First search for a field class from self.SQLA_TYPE_MAPPING for col_type in types: if col_type in self.SQLA_TYPE_MAPPING: field_cls = self.SQLA_TYPE_MAPPING[col_type] if callable(field_cls) and not _is_field(field_cls): field_cls = field_cls(self, data_type) break else: # Try to find a field class based on the column's python_type try: python_type = data_type.python_type except NotImplementedError: python_type = None if python_type in self.type_mapping: field_cls = self.type_mapping[python_type] else: if hasattr(data_type, "impl"): return self._get_field_class_for_data_type(data_type.impl) raise ModelConversionError( "Could not find field column of type {}.".format(types[0]) ) return field_cls def _get_field_class_for_property(self, prop): if hasattr(prop, "direction"): field_cls = Related else: column = prop.columns[0] field_cls = self._get_field_class_for_column(column) return field_cls def _merge_validators(self, defaults, new): new_classes = [validator.__class__ for validator in new] return [ validator for validator in defaults if validator.__class__ not in new_classes ] + new def _get_field_kwargs_for_property(self, prop): kwargs = self.get_base_kwargs() if hasattr(prop, "columns"): column = prop.columns[0] self._add_column_kwargs(kwargs, column) prop = column if hasattr(prop, "direction"): # Relationship property self._add_relationship_kwargs(kwargs, prop) if getattr(prop, "doc", None): # Useful for documentation generation kwargs["description"] = prop.doc info = getattr(prop, "info", dict()) overrides = info.get("marshmallow") if overrides is not None: validate = overrides.pop("validate", []) kwargs["validate"] = self._merge_validators( kwargs["validate"], validate ) # Ensure we do not override the generated validators. kwargs.update(overrides) # Override other kwargs. return kwargs def _add_column_kwargs(self, kwargs, column): """Add keyword arguments to kwargs (in-place) based on the passed in `Column `. """ if column.nullable: kwargs["allow_none"] = True kwargs["required"] = not column.nullable and not _has_default(column) if hasattr(column.type, "enums"): kwargs["validate"].append(validate.OneOf(choices=column.type.enums)) # Add a length validator if a max length is set on the column # Skip UUID columns # (see https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/54) if hasattr(column.type, "length"): try: python_type = column.type.python_type except (AttributeError, NotImplementedError): python_type = None if not python_type or not issubclass(python_type, uuid.UUID): kwargs["validate"].append(validate.Length(max=column.type.length)) if hasattr(column.type, "scale"): kwargs["places"] = getattr(column.type, "scale", None) def _add_relationship_kwargs(self, kwargs, prop): """Add keyword arguments to kwargs (in-place) based on the passed in relationship `Property`. """ nullable = True for pair in prop.local_remote_pairs: if not pair[0].nullable: if prop.uselist is True: nullable = False break kwargs.update({"allow_none": nullable, "required": not nullable}) def _should_exclude_field(self, column, fields=None, exclude=None): if fields and column.key not in fields: return True if exclude and column.key in exclude: return True return False def get_base_kwargs(self): return {"validate": []} default_converter = ModelConverter() fields_for_model = default_converter.fields_for_model """Generate a dict of field_name: `marshmallow.fields.Field` pairs for the given model. :param model: The SQLAlchemy model :param bool include_fk: Whether to include foreign key fields in the output. :return: dict of field_name: Field instance pairs """ property2field = default_converter.property2field """Convert a SQLAlchemy `Property` to a field instance or class. :param Property prop: SQLAlchemy Property. :param bool instance: If `True`, return `Field` instance, computing relevant kwargs from the given property. If `False`, return the `Field` class. :param kwargs: Additional keyword arguments to pass to the field constructor. :return: A `marshmallow.fields.Field` class or instance. """ column2field = default_converter.column2field """Convert a SQLAlchemy `Column ` to a field instance or class. :param sqlalchemy.schema.Column column: SQLAlchemy Column. :param bool instance: If `True`, return `Field` instance, computing relevant kwargs from the given property. If `False`, return the `Field` class. :return: A `marshmallow.fields.Field` class or instance. """ field_for = default_converter.field_for """Convert a property for a mapped SQLAlchemy class to a marshmallow `Field`. Example: :: date_created = field_for(Author, 'date_created', dump_only=True) author = field_for(Book, 'author') :param type model: A SQLAlchemy mapped class. :param str property_name: The name of the property to convert. :param kwargs: Extra keyword arguments to pass to `property2field` :return: A `marshmallow.fields.Field` class or instance. """ marshmallow-sqlalchemy-0.19.0/src/marshmallow_sqlalchemy/exceptions.py000066400000000000000000000005011353433561000263600ustar00rootroot00000000000000class MarshmallowSQLAlchemyError(Exception): """Base exception class from which all exceptions related to marshmallow-sqlalchemy inherit. """ class ModelConversionError(MarshmallowSQLAlchemyError): """Raised when an error occurs in converting a SQLAlchemy construct to a marshmallow object. """ marshmallow-sqlalchemy-0.19.0/src/marshmallow_sqlalchemy/fields.py000066400000000000000000000121601353433561000254510ustar00rootroot00000000000000from marshmallow import fields from marshmallow.utils import is_iterable_but_not_string from sqlalchemy.orm.exc import NoResultFound def get_primary_keys(model): """Get primary key properties for a SQLAlchemy model. :param model: SQLAlchemy model class """ mapper = model.__mapper__ return [mapper.get_property_by_column(column) for column in mapper.primary_key] def ensure_list(value): return value if is_iterable_but_not_string(value) else [value] class RelatedList(fields.List): def get_value(self, obj, attr, accessor=None): # Do not call `fields.List`'s get_value as it calls the container's # `get_value` if the container has `attribute`. # Instead call the `get_value` from the parent of `fields.List` # so the special handling is avoided. return super(fields.List, self).get_value(obj, attr, accessor=accessor) class Related(fields.Field): """Related data represented by a SQLAlchemy `relationship`. Must be attached to a :class:`Schema` class whose options includes a SQLAlchemy `model`, such as :class:`ModelSchema`. :param list columns: Optional column names on related model. If not provided, the primary key(s) of the related model will be used. """ default_error_messages = { "invalid": "Could not deserialize related value {value!r}; " "expected a dictionary with keys {keys!r}" } def __init__(self, column=None, **kwargs): super().__init__(**kwargs) self.columns = ensure_list(column or []) @property def model(self): return self.root.opts.model @property def related_model(self): model_attr = getattr(self.model, self.attribute or self.name) if hasattr(model_attr, "remote_attr"): # handle association proxies model_attr = model_attr.remote_attr return model_attr.property.mapper.class_ @property def related_keys(self): if self.columns: return [ self.related_model.__mapper__.columns[column] for column in self.columns ] return get_primary_keys(self.related_model) @property def session(self): return self.root.session @property def transient(self): return self.root.transient def _serialize(self, value, attr, obj): ret = {prop.key: getattr(value, prop.key, None) for prop in self.related_keys} return ret if len(ret) > 1 else list(ret.values())[0] def _deserialize(self, value, *args, **kwargs): """Deserialize a serialized value to a model instance. If the parent schema is transient, create a new (transient) instance. Otherwise, attempt to find an existing instance in the database. :param value: The value to deserialize. """ if not isinstance(value, dict): if len(self.related_keys) != 1: keys = [prop.key for prop in self.related_keys] if hasattr(self, "make_error"): raise self.make_error("invalid", value=value, keys=keys) else: # marshmallow 2 self.fail("invalid", value=value, keys=keys) value = {self.related_keys[0].key: value} if self.transient: return self.related_model(**value) try: result = self._get_existing_instance( self.session.query(self.related_model), value ) except NoResultFound: # The related-object DNE in the DB, but we still want to deserialize it # ...perhaps we want to add it to the DB later return self.related_model(**value) return result def _get_existing_instance(self, query, value): """Retrieve the related object from an existing instance in the DB. :param query: A SQLAlchemy `Query ` object. :param value: The serialized value to mapto an existing instance. :raises NoResultFound: if there is no matching record. """ if self.columns: result = query.filter_by( **{prop.key: value.get(prop.key) for prop in self.related_keys} ).one() else: # Use a faster path if the related key is the primary key. lookup_values = [value.get(prop.key) for prop in self.related_keys] try: result = query.get(lookup_values) except TypeError: keys = [prop.key for prop in self.related_keys] if hasattr(self, "make_error"): raise self.make_error("invalid", value=value, keys=keys) else: # marshmallow 2 self.fail("invalid", value=value, keys=keys) if result is None: raise NoResultFound return result class Nested(fields.Nested): """Nested field that inherits the session from its parent.""" def _deserialize(self, *args, **kwargs): if hasattr(self.schema, "session"): self.schema.session = self.root.session self.schema.transient = self.root.transient return super()._deserialize(*args, **kwargs) marshmallow-sqlalchemy-0.19.0/src/marshmallow_sqlalchemy/schema.py000066400000000000000000000211111353433561000254370ustar00rootroot00000000000000import marshmallow as ma from .convert import ModelConverter from .fields import get_primary_keys class TableSchemaOpts(ma.SchemaOpts): """Options class for `TableSchema`. Adds the following options: - ``table``: The SQLAlchemy table to generate the `Schema` from (required). - ``model_converter``: `ModelConverter` class to use for converting the SQLAlchemy table to marshmallow fields. - ``include_fk``: Whether to include foreign fields; defaults to `False`. """ def __init__(self, meta, *args, **kwargs): super().__init__(meta, *args, **kwargs) self.table = getattr(meta, "table", None) self.model_converter = getattr(meta, "model_converter", ModelConverter) self.include_fk = getattr(meta, "include_fk", False) class ModelSchemaOpts(ma.SchemaOpts): """Options class for `ModelSchema`. Adds the following options: - ``model``: The SQLAlchemy model to generate the `Schema` from (required). - ``sqla_session``: SQLAlchemy session to be used for deserialization. This is optional; you can also pass a session to the Schema's `load` method. - ``model_converter``: `ModelConverter` class to use for converting the SQLAlchemy model to marshmallow fields. - ``include_fk``: Whether to include foreign fields; defaults to `False`. - ``transient``: Whether load model instances in a transient state (effectively ignoring the session). """ def __init__(self, meta, *args, **kwargs): super().__init__(meta, *args, **kwargs) self.model = getattr(meta, "model", None) self.sqla_session = getattr(meta, "sqla_session", None) self.model_converter = getattr(meta, "model_converter", ModelConverter) self.include_fk = getattr(meta, "include_fk", False) self.transient = getattr(meta, "transient", False) class SchemaMeta(ma.schema.SchemaMeta): """Metaclass for `ModelSchema`.""" # override SchemaMeta @classmethod def get_declared_fields(mcs, klass, cls_fields, inherited_fields, dict_cls): """Updates declared fields with fields converted from the SQLAlchemy model passed as the `model` class Meta option. """ opts = klass.opts Converter = opts.model_converter converter = Converter(schema_cls=klass) declared_fields = super().get_declared_fields( klass, cls_fields, inherited_fields, dict_cls ) fields = mcs.get_fields(converter, opts, declared_fields, dict_cls) fields.update(declared_fields) return fields @classmethod def get_fields(mcs, converter, base_fields, opts): pass class TableSchemaMeta(SchemaMeta): @classmethod def get_fields(mcs, converter, opts, base_fields, dict_cls): if opts.table is not None: return converter.fields_for_table( opts.table, fields=opts.fields, exclude=opts.exclude, include_fk=opts.include_fk, base_fields=base_fields, dict_cls=dict_cls, ) return dict_cls() class ModelSchemaMeta(SchemaMeta): @classmethod def get_fields(mcs, converter, opts, base_fields, dict_cls): if opts.model is not None: return converter.fields_for_model( opts.model, fields=opts.fields, exclude=opts.exclude, include_fk=opts.include_fk, base_fields=base_fields, dict_cls=dict_cls, ) return dict_cls() class TableSchema(ma.Schema, metaclass=TableSchemaMeta): """Base class for SQLAlchemy model-based Schemas. Example: :: from marshmallow_sqlalchemy import TableSchema from mymodels import engine, users class UserSchema(TableSchema): class Meta: table = users schema = UserSchema() select = users.select().limit(1) user = engine.execute(select).fetchone() serialized = schema.dump(user) """ OPTIONS_CLASS = TableSchemaOpts class ModelSchema(ma.Schema, metaclass=ModelSchemaMeta): """Base class for SQLAlchemy model-based Schemas. Example: :: from marshmallow_sqlalchemy import ModelSchema from mymodels import User, session class UserSchema(ModelSchema): class Meta: model = User schema = UserSchema() user = schema.load({'name': 'Bill'}, session=session) existing_user = schema.load({'name': 'Bill'}, instance=User.query.first()) :param session: Optional SQLAlchemy session; may be overridden in `load.` :param instance: Optional existing instance to modify; may be overridden in `load`. """ OPTIONS_CLASS = ModelSchemaOpts @property def session(self): return self._session or self.opts.sqla_session @session.setter def session(self, session): self._session = session @property def transient(self): return self._transient or self.opts.transient @transient.setter def transient(self, transient): self._transient = transient def __init__(self, *args, **kwargs): self._session = kwargs.pop("session", None) self.instance = kwargs.pop("instance", None) self._transient = kwargs.pop("transient", None) super().__init__(*args, **kwargs) def get_instance(self, data): """Retrieve an existing record by primary key(s). If the schema instance is transient, return None. :param data: Serialized data to inform lookup. """ if self.transient: return None props = get_primary_keys(self.opts.model) filters = {prop.key: data.get(prop.key) for prop in props} if None not in filters.values(): return self.session.query(self.opts.model).filter_by(**filters).first() return None @ma.post_load def make_instance(self, data, **kwargs): """Deserialize data to an instance of the model. Update an existing row if specified in `self.instance` or loaded by primary key(s) in the data; else create a new row. :param data: Data to deserialize. """ instance = self.instance or self.get_instance(data) if instance is not None: for key, value in data.items(): setattr(instance, key, value) return instance kwargs, association_attrs = self._split_model_kwargs_association(data) instance = self.opts.model(**kwargs) for attr, value in association_attrs.items(): setattr(instance, attr, value) return instance def load(self, data, *, session=None, instance=None, transient=False, **kwargs): """Deserialize data to internal representation. :param session: Optional SQLAlchemy session. :param instance: Optional existing instance to modify. :param transient: Optional switch to allow transient instantiation. """ self._session = session or self._session self._transient = transient or self._transient if not (self.transient or self.session): raise ValueError("Deserialization requires a session") self.instance = instance or self.instance try: return super().load(data, **kwargs) finally: self.instance = None def validate(self, data, *, session=None, **kwargs): self._session = session or self._session if not (self.transient or self.session): raise ValueError("Validation requires a session") return super().validate(data, **kwargs) def _split_model_kwargs_association(self, data): """Split serialized attrs to ensure association proxies are passed separately. This is necessary for Python < 3.6.0, as the order in which kwargs are passed is non-deterministic, and associations must be parsed by sqlalchemy after their intermediate relationship, unless their `creator` has been set. Ignore invalid keys at this point - behaviour for unknowns should be handled elsewhere. :param data: serialized dictionary of attrs to split on association_proxy. """ association_attrs = { key: value for key, value in data.items() # association proxy if hasattr(getattr(self.opts.model, key, None), "remote_attr") } kwargs = { key: value for key, value in data.items() if (hasattr(self.opts.model, key) and key not in association_attrs) } return kwargs, association_attrs marshmallow-sqlalchemy-0.19.0/tests/000077500000000000000000000000001353433561000174345ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/tests/__init__.py000066400000000000000000000000001353433561000215330ustar00rootroot00000000000000marshmallow-sqlalchemy-0.19.0/tests/test_marshmallow_sqlalchemy.py000066400000000000000000001363451353433561000256310ustar00rootroot00000000000000import uuid import datetime as dt import decimal from collections import OrderedDict from types import SimpleNamespace import sqlalchemy as sa from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship, backref, column_property from sqlalchemy.dialects import postgresql import marshmallow from marshmallow import Schema, fields, validate, post_load, ValidationError import pytest from marshmallow_sqlalchemy import ( fields_for_model, TableSchema, ModelSchema, ModelConverter, property2field, column2field, field_for, ModelConversionError, ) from marshmallow_sqlalchemy.fields import Related, RelatedList, Nested MARSHMALLOW_VERSION_INFO = tuple( [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] ) def unpack(return_value): return return_value.data if MARSHMALLOW_VERSION_INFO[0] < 3 else return_value def contains_validator(field, v_type): for v in field.validators: if isinstance(v, v_type): return v return False class AnotherInteger(sa.Integer): """Use me to test if MRO works like we want""" pass class AnotherText(sa.types.TypeDecorator): """Use me to test if MRO and `impl` virtual type works like we want""" impl = sa.UnicodeText @pytest.fixture() def Base(): return declarative_base() @pytest.fixture() def engine(): return sa.create_engine("sqlite:///:memory:", echo=False) @pytest.fixture() def session(Base, models, engine): Session = sessionmaker(bind=engine) Base.metadata.create_all(bind=engine) return Session() @pytest.fixture() def models(Base): # models adapted from https://github.com/wtforms/wtforms-sqlalchemy/blob/master/tests/tests.py student_course = sa.Table( "student_course", Base.metadata, sa.Column("student_id", sa.Integer, sa.ForeignKey("student.id")), sa.Column("course_id", sa.Integer, sa.ForeignKey("course.id")), ) class Course(Base): __tablename__ = "course" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String(255), nullable=False) # These are for better model form testing cost = sa.Column(sa.Numeric(5, 2), nullable=False) description = sa.Column( sa.Text, nullable=True, info=dict( marshmallow=dict(validate=[validate.Length(max=1000)], required=True) ), ) level = sa.Column(sa.Enum("Primary", "Secondary")) has_prereqs = sa.Column(sa.Boolean, nullable=False) started = sa.Column(sa.DateTime, nullable=False) grade = sa.Column(AnotherInteger, nullable=False) transcription = sa.Column(AnotherText, nullable=False) @property def url(self): return f"/courses/{self.id}" class School(Base): __tablename__ = "school" id = sa.Column("school_id", sa.Integer, primary_key=True) name = sa.Column(sa.String(255), nullable=False) @property def url(self): return f"/schools/{self.id}" class Student(Base): __tablename__ = "student" id = sa.Column(sa.Integer, primary_key=True) full_name = sa.Column(sa.String(255), nullable=False, unique=True) dob = sa.Column(sa.Date(), nullable=True) date_created = sa.Column( sa.DateTime, default=dt.datetime.utcnow, doc="date the student was created" ) current_school_id = sa.Column( sa.Integer, sa.ForeignKey(School.id), nullable=False ) current_school = relationship(School, backref=backref("students")) possible_teachers = association_proxy("current_school", "teachers") courses = relationship( "Course", secondary=student_course, backref=backref("students", lazy="dynamic"), ) @property def url(self): return f"/students/{self.id}" class Teacher(Base): __tablename__ = "teacher" id = sa.Column(sa.Integer, primary_key=True) full_name = sa.Column( sa.String(255), nullable=False, unique=True, default="Mr. Noname" ) current_school_id = sa.Column( sa.Integer, sa.ForeignKey(School.id), nullable=True ) current_school = relationship(School, backref=backref("teachers")) substitute = relationship("SubstituteTeacher", uselist=False, backref="teacher") class SubstituteTeacher(Base): __tablename__ = "substituteteacher" id = sa.Column(sa.Integer, sa.ForeignKey("teacher.id"), primary_key=True) class Paper(Base): __tablename__ = "paper" satype = sa.Column(sa.String(50)) __mapper_args__ = {"polymorphic_identity": "paper", "polymorphic_on": satype} id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.String, nullable=False, unique=True) class GradedPaper(Paper): __tablename__ = "gradedpaper" __mapper_args__ = {"polymorphic_identity": "gradedpaper"} id = sa.Column(sa.Integer, sa.ForeignKey("paper.id"), primary_key=True) marks_available = sa.Column(sa.Integer) class Seminar(Base): __tablename__ = "seminar" title = sa.Column(sa.String, primary_key=True) semester = sa.Column(sa.String, primary_key=True) label = column_property(title + ": " + semester) lecturekeywords_table = sa.Table( "lecturekeywords", Base.metadata, sa.Column("keyword_id", sa.Integer, sa.ForeignKey("keyword.id")), sa.Column("lecture_id", sa.Integer, sa.ForeignKey("lecture.id")), ) class Keyword(Base): __tablename__ = "keyword" id = sa.Column(sa.Integer, primary_key=True) keyword = sa.Column(sa.String) class Lecture(Base): __tablename__ = "lecture" __table_args__ = ( sa.ForeignKeyConstraint( ["seminar_title", "seminar_semester"], ["seminar.title", "seminar.semester"], ), ) id = sa.Column(sa.Integer, primary_key=True) topic = sa.Column(sa.String) seminar_title = sa.Column(sa.String, sa.ForeignKey(Seminar.title)) seminar_semester = sa.Column(sa.String, sa.ForeignKey(Seminar.semester)) seminar = relationship( Seminar, foreign_keys=[seminar_title, seminar_semester], backref="lectures" ) kw = relationship("Keyword", secondary=lecturekeywords_table) keywords = association_proxy( "kw", "keyword", creator=lambda kw: Keyword(keyword=kw) ) return SimpleNamespace( Course=Course, School=School, Student=Student, Teacher=Teacher, SubstituteTeacher=SubstituteTeacher, Paper=Paper, GradedPaper=GradedPaper, Seminar=Seminar, Lecture=Lecture, Keyword=Keyword, ) class MyDateField(fields.Date): pass @pytest.fixture() def schemas(models, session): class CourseSchema(ModelSchema): class Meta: model = models.Course sqla_session = session strict = True # for testing marshmallow 2 class SchoolSchema(ModelSchema): class Meta: model = models.School sqla_session = session strict = True # for testing marshmallow 2 class StudentSchema(ModelSchema): class Meta: model = models.Student sqla_session = session strict = True # for testing marshmallow 2 class StudentSchemaWithCustomTypeMapping(ModelSchema): TYPE_MAPPING = Schema.TYPE_MAPPING.copy() TYPE_MAPPING.update({dt.date: MyDateField}) class Meta: model = models.Student sqla_session = session strict = True # for testing marshmallow 2 class TeacherSchema(ModelSchema): class Meta: model = models.Teacher sqla_session = session strict = True # for testing marshmallow 2 class SubstituteTeacherSchema(ModelSchema): class Meta: model = models.SubstituteTeacher strict = True # for testing marshmallow 2 class PaperSchema(ModelSchema): class Meta: model = models.Paper sqla_session = session strict = True # for testing marshmallow 2 class GradedPaperSchema(ModelSchema): class Meta: model = models.GradedPaper sqla_session = session strict = True # for testing marshmallow 2 class HyperlinkStudentSchema(ModelSchema): class Meta: model = models.Student sqla_session = session strict = True # for testing marshmallow 2 class SeminarSchema(ModelSchema): class Meta: model = models.Seminar sqla_session = session strict = True # for testing marshmallow 2 label = fields.Str() class LectureSchema(ModelSchema): class Meta: model = models.Lecture sqla_session = session strict = True # for testing marshmallow 2 return SimpleNamespace( CourseSchema=CourseSchema, SchoolSchema=SchoolSchema, StudentSchema=StudentSchema, StudentSchemaWithCustomTypeMapping=StudentSchemaWithCustomTypeMapping, TeacherSchema=TeacherSchema, SubstituteTeacherSchema=SubstituteTeacherSchema, PaperSchema=PaperSchema, GradedPaperSchema=GradedPaperSchema, HyperlinkStudentSchema=HyperlinkStudentSchema, SeminarSchema=SeminarSchema, LectureSchema=LectureSchema, ) class TestModelFieldConversion: def test_fields_for_model_types(self, models): fields_ = fields_for_model(models.Student, include_fk=True) assert type(fields_["id"]) is fields.Int assert type(fields_["full_name"]) is fields.Str assert type(fields_["dob"]) is fields.Date assert type(fields_["current_school_id"]) is fields.Int assert type(fields_["date_created"]) is fields.DateTime def test_fields_for_model_handles_exclude(self, models): fields_ = fields_for_model(models.Student, exclude=("dob",)) assert type(fields_["id"]) is fields.Int assert type(fields_["full_name"]) is fields.Str assert fields_["dob"] is None def test_fields_for_model_handles_custom_types(self, models): fields_ = fields_for_model(models.Course, include_fk=True) assert type(fields_["grade"]) is fields.Int assert type(fields_["transcription"]) is fields.Str def test_fields_for_model_saves_doc(self, models): fields_ = fields_for_model(models.Student, include_fk=True) assert ( fields_["date_created"].metadata["description"] == "date the student was created" ) def test_length_validator_set(self, models): fields_ = fields_for_model(models.Student) validator = contains_validator(fields_["full_name"], validate.Length) assert validator assert validator.max == 255 def test_sets_allow_none_for_nullable_fields(self, models): fields_ = fields_for_model(models.Student) assert fields_["dob"].allow_none is True def test_sets_enum_choices(self, models): fields_ = fields_for_model(models.Course) validator = contains_validator(fields_["level"], validate.OneOf) assert validator assert list(validator.choices) == ["Primary", "Secondary"] def test_many_to_many_relationship(self, models): student_fields = fields_for_model(models.Student) assert type(student_fields["courses"]) is RelatedList course_fields = fields_for_model(models.Course) assert type(course_fields["students"]) is RelatedList def test_many_to_one_relationship(self, models): student_fields = fields_for_model(models.Student) assert type(student_fields["current_school"]) is Related school_fields = fields_for_model(models.School) assert type(school_fields["students"]) is RelatedList def test_include_fk(self, models): student_fields = fields_for_model(models.Student, include_fk=False) assert "current_school_id" not in student_fields student_fields2 = fields_for_model(models.Student, include_fk=True) assert "current_school_id" in student_fields2 def test_overridden_with_fk(self, models): graded_paper_fields = fields_for_model(models.GradedPaper, include_fk=False) assert "id" in graded_paper_fields def test_info_overrides(self, models): fields_ = fields_for_model(models.Course) field = fields_["description"] validator = contains_validator(field, validate.Length) assert validator.max == 1000 assert field.required def make_property(*column_args, **column_kwargs): return column_property(sa.Column(*column_args, **column_kwargs)) class TestPropertyFieldConversion: @pytest.fixture() def converter(self): return ModelConverter() def test_convert_custom_type_mapping_on_schema(self): class MyDateTimeField(fields.DateTime): pass class MySchema(Schema): TYPE_MAPPING = Schema.TYPE_MAPPING.copy() TYPE_MAPPING.update({dt.datetime: MyDateTimeField}) converter = ModelConverter(schema_cls=MySchema) prop = make_property(sa.DateTime()) field = converter.property2field(prop) assert type(field) == MyDateTimeField @pytest.mark.parametrize( ("sa_type", "field_type"), ( (sa.String, fields.Str), (sa.Unicode, fields.Str), (sa.Binary, fields.Str), (sa.LargeBinary, fields.Str), (sa.Text, fields.Str), (sa.Date, fields.Date), (sa.DateTime, fields.DateTime), (sa.Boolean, fields.Bool), (sa.Boolean, fields.Bool), (sa.Float, fields.Float), (sa.SmallInteger, fields.Int), (postgresql.UUID, fields.UUID), (postgresql.MACADDR, fields.Str), (postgresql.INET, fields.Str), ), ) def test_convert_types(self, converter, sa_type, field_type): prop = make_property(sa_type()) field = converter.property2field(prop) assert type(field) == field_type def test_convert_Numeric(self, converter): prop = make_property(sa.Numeric(scale=2)) field = converter.property2field(prop) assert type(field) == fields.Decimal assert field.places == decimal.Decimal((0, (1,), -2)) def test_convert_ARRAY_String(self, converter): prop = make_property(postgresql.ARRAY(sa.String())) field = converter.property2field(prop) assert type(field) == fields.List inner_field = getattr(field, "inner", getattr(field, "container", None)) assert type(inner_field) == fields.Str def test_convert_ARRAY_Integer(self, converter): prop = make_property(postgresql.ARRAY(sa.Integer)) field = converter.property2field(prop) assert type(field) == fields.List inner_field = getattr(field, "inner", getattr(field, "container", None)) assert type(inner_field) == fields.Int def test_convert_TSVECTOR(self, converter): prop = make_property(postgresql.TSVECTOR) with pytest.raises(ModelConversionError): converter.property2field(prop) def test_convert_default(self, converter): prop = make_property(sa.String, default="ack") field = converter.property2field(prop) assert field.required is False def test_convert_server_default(self, converter): prop = make_property(sa.String, server_default=sa.text("sysdate")) field = converter.property2field(prop) assert field.required is False def test_convert_autoincrement(self, models, converter): prop = models.Course.__mapper__.get_property("id") field = converter.property2field(prop) assert field.required is False class TestPropToFieldClass: def test_property2field(self): prop = make_property(sa.Integer()) field = property2field(prop, instance=True) assert type(field) == fields.Int field_cls = property2field(prop, instance=False) assert field_cls == fields.Int def test_can_pass_extra_kwargs(self): prop = make_property(sa.String()) field = property2field(prop, instance=True, description="just a string") assert field.metadata["description"] == "just a string" class TestColumnToFieldClass: def test_column2field(self): column = sa.Column(sa.String(255)) field = column2field(column, instance=True) assert type(field) == fields.String field_cls = column2field(column, instance=False) assert field_cls == fields.String def test_can_pass_extra_kwargs(self): column = sa.Column(sa.String(255)) field = column2field(column, instance=True, description="just a string") assert field.metadata["description"] == "just a string" def test_uuid_column2field(self): class UUIDType(sa.types.TypeDecorator): python_type = uuid.UUID impl = sa.BINARY(16) column = sa.Column(UUIDType) assert issubclass(column.type.python_type, uuid.UUID) # Test against test check assert hasattr(column.type, "length") # Test against test check assert column.type.length == 16 # Test against test field = column2field(column, instance=True) uuid_val = uuid.uuid4() assert field.deserialize(str(uuid_val)) == uuid_val class TestFieldFor: def test_field_for(self, models, session): field = field_for(models.Student, "full_name") assert type(field) == fields.Str field = field_for(models.Student, "current_school", session=session) assert type(field) == Related field = field_for(models.Student, "full_name", field_class=fields.Date) assert type(field) == fields.Date def test_field_for_can_override_validators(self, models, session): field = field_for( models.Student, "full_name", validate=[validate.Length(max=20)] ) assert len(field.validators) == 1 assert field.validators[0].max == 20 field = field_for(models.Student, "full_name", validate=[]) assert field.validators == [] class TestTableSchema: @pytest.fixture def school(self, models, session): table = models.School.__table__ insert = table.insert().values(name="Univ. of Whales") with session.connection() as conn: conn.execute(insert) select = table.select().limit(1) return conn.execute(select).fetchone() def test_dump_row(self, models, school): class SchoolSchema(TableSchema): class Meta: table = models.School.__table__ schema = SchoolSchema() data = unpack(schema.dump(school)) assert data == {"name": "Univ. of Whales", "school_id": 1} def test_exclude(self, models, school): class SchoolSchema(TableSchema): class Meta: table = models.School.__table__ exclude = ("name",) schema = SchoolSchema() data = unpack(schema.dump(school)) assert "name" not in data class TestModelSchema: @pytest.fixture() def school(self, models, session): school_ = models.School(name="Univ. Of Whales") session.add(school_) session.flush() return school_ @pytest.fixture() def school_with_teachers(self, models, session): school_with_teachers_ = models.School(name="School of Hard Knocks") school_with_teachers_.teachers = [ models.Teacher(full_name="Teachy McTeachFace"), models.Teacher(full_name="Another Teacher"), ] session.add(school_with_teachers_) session.flush() return school_with_teachers_ @pytest.fixture() def student(self, models, school, session): student_ = models.Student(full_name="Monty Python", current_school=school) session.add(student_) session.flush() return student_ @pytest.fixture() def student_with_teachers(self, models, school_with_teachers, session): student_ = models.Student( full_name="Monty Python", current_school=school_with_teachers ) session.add(student_) session.flush() return student_ @pytest.fixture() def teacher(self, models, school, session): teacher_ = models.Teacher(full_name="The Substitute Teacher") session.add(teacher_) session.flush() return teacher_ @pytest.fixture() def subteacher(self, models, school, teacher, session): sub_ = models.SubstituteTeacher(teacher=teacher) session.add(sub_) session.flush() return sub_ @pytest.fixture() def seminar(self, models, session): seminar_ = models.Seminar(title="physics", semester="spring") session.add(seminar_) session.flush() return seminar_ @pytest.fixture() def lecture(self, models, session, seminar): lecture_ = models.Lecture(topic="force", seminar=seminar) lecture_.keywords.extend(["Newton's Laws", "Friction"]) session.add(lecture_) session.flush() return lecture_ def test_model_schema_with_custom_type_mapping(self, schemas): schema = schemas.StudentSchemaWithCustomTypeMapping() assert type(schema.fields["dob"]) is MyDateField def test_model_schema_field_inheritance(self, schemas): class CourseSchemaSub(schemas.CourseSchema): additional = fields.Int() parent_schema = schemas.CourseSchema() child_schema = CourseSchemaSub() parent_schema_fields = set(parent_schema.declared_fields) child_schema_fields = set(child_schema.declared_fields) assert parent_schema_fields.issubset(child_schema_fields) assert "additional" in child_schema_fields def test_model_schema_class_meta_inheritance(self, models, session): class BaseCourseSchema(ModelSchema): class Meta: model = models.Course sqla_session = session class CourseSchema(BaseCourseSchema): pass schema = CourseSchema() field_names = schema.declared_fields assert "id" in field_names assert "name" in field_names assert "cost" in field_names def test_model_schema_ordered(self, models): class SchoolSchema(ModelSchema): class Meta: model = models.School ordered = True schema = SchoolSchema() assert isinstance(schema.fields, OrderedDict) fields = [prop.key for prop in models.School.__mapper__.iterate_properties] assert list(schema.fields.keys()) == fields def test_model_schema_dumping(self, schemas, student): schema = schemas.StudentSchema() data = unpack(schema.dump(student)) # fk excluded by default assert "current_school_id" not in data # related field dumps to pk assert data["current_school"] == student.current_school.id def test_model_schema_loading(self, models, schemas, student, session): schema_kwargs = ( {"unknown": marshmallow.INCLUDE} if MARSHMALLOW_VERSION_INFO[0] >= 3 else {} ) schema = schemas.StudentSchema(**schema_kwargs) dump_data = unpack(schema.dump(student)) load_data = unpack(schema.load(dump_data)) assert load_data is student assert load_data.current_school == student.current_school def test_model_schema_loading_invalid_related_type( self, models, schemas, student, session ): schema = schemas.StudentSchema() dump_data = unpack(schema.dump(student)) dump_data["current_school"] = [1] with pytest.raises( ValidationError, match="Could not deserialize related value" ): schema.load(dump_data) def test_model_schema_loading_missing_field( self, models, schemas, student, session ): schema = schemas.StudentSchema() dump_data = unpack(schema.dump(student)) dump_data.pop("full_name") with pytest.raises(ValidationError) as excinfo: schema.load(dump_data) err = excinfo.value assert err.messages["full_name"] == ["Missing data for required field."] def test_model_schema_loading_custom_instance( self, models, schemas, student, session ): schema = schemas.StudentSchema(instance=student) dump_data = unpack(schema.dump(student)) dump_data["full_name"] = "Terry Gilliam" load_data = unpack(schema.load(dump_data)) assert load_data is student assert load_data.current_school == student.current_school # Regression test for https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/78 def test_model_schema_loading_resets_instance(self, models, schemas, student): schema = schemas.StudentSchema() data1 = unpack(schema.load({"full_name": "new name"}, instance=student)) assert data1.id == student.id assert data1.full_name == student.full_name data2 = unpack(schema.load({"full_name": "new name2"})) assert isinstance(data2, models.Student) # loaded data is different from first instance (student) assert data2 != student assert data2.full_name == "new name2" def test_model_schema_loading_no_instance_or_pk( self, models, schemas, student, session ): schema = schemas.StudentSchema() dump_data = {"full_name": "Terry Gilliam"} load_data = unpack(schema.load(dump_data)) assert load_data is not student def test_model_schema_compound_key(self, schemas, seminar): schema = schemas.SeminarSchema() dump_data = unpack(schema.dump(seminar)) load_data = unpack(schema.load(dump_data)) assert load_data is seminar def test_model_schema_compound_key_relationship(self, schemas, lecture): schema = schemas.LectureSchema() dump_data = unpack(schema.dump(lecture)) assert dump_data["seminar"] == { "title": lecture.seminar_title, "semester": lecture.seminar_semester, } load_data = unpack(schema.load(dump_data)) assert load_data is lecture def test_model_schema_compound_key_relationship_invalid_key(self, schemas, lecture): schema = schemas.LectureSchema() dump_data = unpack(schema.dump(lecture)) dump_data["seminar"] = "scalar" with pytest.raises(ValidationError) as excinfo: schema.load(dump_data) err = excinfo.value assert "seminar" in err.messages def test_model_schema_loading_passing_session_to_load( self, models, schemas, student, session ): class StudentSchemaNoSession(ModelSchema): class Meta: model = models.Student schema = StudentSchemaNoSession() dump_data = unpack(schema.dump(student)) load_data = unpack(schema.load(dump_data, session=session)) assert type(load_data) == models.Student assert load_data.current_school == student.current_school def test_model_schema_validation_passing_session_to_validate( self, models, schemas, student, session ): class StudentSchemaNoSession(ModelSchema): class Meta: model = models.Student schema = StudentSchemaNoSession() dump_data = unpack(schema.dump(student)) assert type(schema.validate(dump_data, session=session)) is dict def test_model_schema_loading_passing_session_to_constructor( self, models, schemas, student, session ): class StudentSchemaNoSession(ModelSchema): class Meta: model = models.Student schema = StudentSchemaNoSession(session=session) dump_data = unpack(schema.dump(student)) load_data = unpack(schema.load(dump_data)) assert type(load_data) == models.Student assert load_data.current_school == student.current_school def test_model_schema_validation_passing_session_to_constructor( self, models, schemas, student, session ): class StudentSchemaNoSession(ModelSchema): class Meta: model = models.Student schema = StudentSchemaNoSession(session=session) dump_data = unpack(schema.dump(student)) assert type(schema.validate(dump_data)) is dict def test_model_schema_loading_and_validation_with_no_session_raises_error( self, models, schemas, student, session ): class StudentSchemaNoSession(ModelSchema): class Meta: model = models.Student schema = StudentSchemaNoSession() dump_data = unpack(schema.dump(student)) with pytest.raises(ValueError) as excinfo: schema.load(dump_data) assert excinfo.value.args[0] == "Deserialization requires a session" with pytest.raises(ValueError) as excinfo: schema.validate(dump_data) assert excinfo.value.args[0] == "Validation requires a session" def test_model_schema_custom_related_column( self, models, schemas, student, session ): class StudentSchema(ModelSchema): class Meta: model = models.Student sqla_session = session current_school = Related(column="name") schema = StudentSchema() dump_data = unpack(schema.dump(student)) load_data = unpack(schema.load(dump_data)) assert type(load_data) == models.Student assert load_data.current_school == student.current_school def test_model_schema_with_attribute( self, models, schemas, school, student, session ): class StudentSchema(Schema): id = fields.Int() class SchoolSchema(ModelSchema): class Meta: model = models.School sqla_session = session students = RelatedList(Related(attribute="students"), attribute="students") class SchoolSchema2(ModelSchema): class Meta: model = models.School sqla_session = session students = field_for(models.School, "students", attribute="students") class SchoolSchema3(ModelSchema): class Meta: model = models.School sqla_session = session students = field_for( models.School, "students", attribute="students", column="full_name" ) schema = SchoolSchema() schema2 = SchoolSchema2() schema3 = SchoolSchema3() dump_data = unpack(schema.dump(school)) dump_data2 = unpack(schema2.dump(school)) dump_data3 = unpack(schema3.dump(school)) load_data = unpack(schema.load(dump_data)) assert dump_data["students"] == dump_data["students"] assert dump_data["students"] == dump_data2["students"] assert [i.full_name for i in school.students] == dump_data3["students"] assert type(load_data) == models.School assert load_data.students == school.students def test_dump_many_to_one_relationship(self, models, schemas, school, student): schema = schemas.SchoolSchema() dump_data = unpack(schema.dump(school)) assert dump_data["students"] == [student.id] def test_load_many_to_one_relationship(self, models, schemas, school, student): schema = schemas.SchoolSchema() dump_data = unpack(schema.dump(school)) load_data = unpack(schema.load(dump_data)) assert type(load_data.students[0]) is models.Student assert load_data.students[0] == student def test_fields_option(self, student, models, session): class StudentSchema(ModelSchema): class Meta: model = models.Student sqla_session = session fields = ("full_name", "date_created") session.commit() schema = StudentSchema() data = unpack(schema.dump(student)) assert "full_name" in data assert "date_created" in data assert "dob" not in data assert len(data.keys()) == 2 def test_exclude_option(self, student, models, session): class StudentSchema(ModelSchema): class Meta: model = models.Student sqla_session = session exclude = ("date_created",) session.commit() schema = StudentSchema() data = unpack(schema.dump(student)) assert "full_name" in data assert "date_created" not in data def test_include_fk_option(self, models, schemas): class StudentSchema(ModelSchema): class Meta: model = models.Student include_fk = True assert "current_school_id" in StudentSchema().fields assert "current_school_id" not in schemas.StudentSchema().fields def test_additional_option(self, student, models, session): class StudentSchema(ModelSchema): uppername = fields.Function(lambda x: x.full_name.upper()) class Meta: model = models.Student sqla_session = session additional = ("date_created",) session.commit() schema = StudentSchema() data = unpack(schema.dump(student)) assert "full_name" in data assert "uppername" in data assert data["uppername"] == student.full_name.upper() def test_field_override(self, student, models, session): class MyString(fields.Str): def _serialize(self, val, attr, obj): return val.upper() class StudentSchema(ModelSchema): full_name = MyString() class Meta: model = models.Student sqla_session = session session.commit() schema = StudentSchema() data = unpack(schema.dump(student)) assert "full_name" in data assert data["full_name"] == student.full_name.upper() def test_a_teacher_who_is_a_substitute( self, models, schemas, teacher, subteacher, session ): session.commit() schema = schemas.TeacherSchema() data = unpack(schema.dump(teacher)) load_data = unpack(schema.load(data)) assert type(load_data) is models.Teacher assert "substitute" in data assert data["substitute"] == subteacher.id def test_dump_only_relationship(self, models, session, school, student): class SchoolSchema2(ModelSchema): class Meta: model = models.School students = field_for(models.School, "students", dump_only=True) # override for easier testing @post_load def make_instance(self, data, **kwargs): return data sch = SchoolSchema2() students_field = sch.fields["students"] assert students_field.dump_only is True dump_data = unpack(sch.dump(school)) if MARSHMALLOW_VERSION_INFO[0] < 3: load_data = unpack(sch.load(dump_data, session=session)) assert "students" not in load_data else: with pytest.raises(ValidationError) as excinfo: sch.load(dump_data, session=session) err = excinfo.value assert err.messages == {"students": ["Unknown field."]} def test_transient_schema(self, models, school): class SchoolSchemaTransient(ModelSchema): class Meta: model = models.School transient = True sch = SchoolSchemaTransient() dump_data = unpack(sch.dump(school)) load_data = unpack(sch.load(dump_data)) assert isinstance(load_data, models.School) state = sa.inspect(load_data) assert state.transient def test_transient_load(self, models, session, school): class SchoolSchemaTransient(ModelSchema): class Meta: model = models.School sqla_session = session sch = SchoolSchemaTransient() dump_data = unpack(sch.dump(school)) load_data = unpack(sch.load(dump_data, transient=True)) assert isinstance(load_data, models.School) state = sa.inspect(load_data) assert state.transient @pytest.mark.skipif( MARSHMALLOW_VERSION_INFO[0] < 3, reason="`unknown` was added in marshmallow 3" ) def test_transient_load_with_unknown_include(self, models, session, school): class SchoolSchemaTransient(ModelSchema): class Meta: model = models.School sqla_session = session unknown = marshmallow.INCLUDE sch = SchoolSchemaTransient() dump_data = unpack(sch.dump(school)) dump_data["foo"] = "bar" load_data = unpack(sch.load(dump_data, transient=True)) assert isinstance(load_data, models.School) state = sa.inspect(load_data) assert state.transient def test_transient_schema_with_relationship( self, models, student_with_teachers, session ): class StudentSchemaTransient(ModelSchema): class Meta: model = models.Student transient = True sch = StudentSchemaTransient() dump_data = unpack(sch.dump(student_with_teachers)) load_data = unpack(sch.load(dump_data)) assert isinstance(load_data, models.Student) state = sa.inspect(load_data) assert state.transient school = load_data.current_school assert isinstance(school, models.School) school_state = sa.inspect(school) assert school_state.transient def test_transient_schema_with_association(self, models, student_with_teachers): class StudentSchemaTransient(ModelSchema): class Meta: model = models.Student transient = True possible_teachers = field_for( models.School, "teachers", attribute="possible_teachers" ) sch = StudentSchemaTransient() dump_data = unpack(sch.dump(student_with_teachers)) load_data = unpack(sch.load(dump_data)) assert isinstance(load_data, models.Student) state = sa.inspect(load_data) assert state.transient school = load_data.current_school assert isinstance(school, models.School) school_state = sa.inspect(school) assert school_state.transient possible_teachers = load_data.possible_teachers assert possible_teachers assert school.teachers == load_data.possible_teachers assert sa.inspect(possible_teachers[0]).transient def test_transient_schema_with_implicit_association(self, models, lecture): """Test for transient implicit creation of associated objects.""" class LectureSchemaTransient(ModelSchema): class Meta: model = models.Lecture transient = True keywords = field_for( models.Keyword, "keyword", attribute="keywords", field_class=fields.List, cls_or_instance=fields.Str, ) sch = LectureSchemaTransient() dump_data = unpack(sch.dump(lecture)) del dump_data["kw"] load_data = unpack(sch.load(dump_data)) assert isinstance(load_data, models.Lecture) state = sa.inspect(load_data) assert state.transient kw_objects = load_data.kw assert kw_objects for keyword in kw_objects: assert isinstance(keyword, models.Keyword) assert sa.inspect(keyword).transient keywords = {kw.keyword for kw in kw_objects} assert keywords == set(load_data.keywords) def test_exclude(self, models, school): class SchoolSchema(ModelSchema): class Meta: model = models.School exclude = ("name",) schema = SchoolSchema() data = unpack(schema.dump(school)) assert "name" not in data class TestNullForeignKey: @pytest.fixture() def school(self, models, session): school_ = models.School(name="The Teacherless School") session.add(school_) session.flush() return school_ @pytest.fixture() def teacher(self, models, school, session): teacher_ = models.Teacher(full_name="The Schoolless Teacher") session.add(teacher_) session.flush() return teacher_ def test_a_teacher_with_no_school(self, models, schemas, teacher, session): session.commit() schema = schemas.TeacherSchema() dump_data = unpack(schema.dump(teacher)) load_data = unpack(schema.load(dump_data)) assert type(load_data) == models.Teacher assert load_data.current_school is None def test_a_teacher_who_is_not_a_substitute(self, models, schemas, teacher, session): session.commit() schema = schemas.TeacherSchema() data = unpack(schema.dump(teacher)) load_data = unpack(schema.load(data)) assert type(load_data) is models.Teacher assert "substitute" in data assert data["substitute"] is None class TestDeserializeObjectThatDNE: def test_deserialization_of_seminar_with_many_lectures_that_DNE( self, models, schemas, session ): seminar_schema = schemas.SeminarSchema() seminar_dict = { "title": "Novice Training", "semester": "First", "lectures": [ { "topic": "Intro to Ter'Angreal", "seminar_title": "Novice Training", "seminar_semester": "First", }, { "topic": "History of the Ajahs", "seminar_title": "Novice Training", "seminar_semester": "First", }, ], } deserialized_seminar_object = unpack( seminar_schema.load(seminar_dict, session=session) ) # Ensure both nested lecture objects weren't forgotten... assert len(deserialized_seminar_object.lectures) == 2 # Assume that if 1 lecture has a field with the correct String value, all strings # were successfully deserialized in nested objects assert deserialized_seminar_object.lectures[0].topic == "Intro to Ter'Angreal" class TestMarshmallowContext: def test_getting_session_from_marshmallow_context(self, session, models): class SchoolSchema(ModelSchema): @property def session(self): return ( self.context.get("session", None) or self._session or self.opts.sqla_session ) class Meta: model = models.School fields = ("name",) class SchoolWrapperSchema(Schema): school = fields.Nested(SchoolSchema) schools = fields.Nested(SchoolSchema, many=True) schema = SchoolWrapperSchema(context=dict(session=session)) data = { "school": {"name": "one school"}, "schools": [{"name": "another school"}, {"name": "yet, another school"}], } result = unpack(schema.load(data)) session.add(result["school"]) session.add_all(result["schools"]) session.flush() assert isinstance(result["school"], models.School) assert isinstance(result["school"].id, int) for school in result["schools"]: assert isinstance(school, models.School) assert isinstance(school.id, int) dump_result = unpack(schema.dump(result)) assert dump_result == data class TestNestedFieldSession: """Test the session can be passed to nested fields.""" @pytest.fixture def association_table(self, Base): return sa.Table( "association", Base.metadata, sa.Column("left_id", sa.Integer, sa.ForeignKey("left.id")), sa.Column("right_id", sa.Integer, sa.ForeignKey("right.id")), ) @pytest.fixture def parent_model(self, Base, association_table): class Parent(Base): __tablename__ = "left" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Text) children = relationship( "Child", secondary=association_table, back_populates="parents" ) return Parent @pytest.fixture def child_model(self, Base, parent_model, association_table): class Child(Base): __tablename__ = "right" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Text) parents = relationship( "Parent", secondary=association_table, back_populates="children" ) return Child @pytest.fixture def child_schema(self, child_model): class ChildSchema(ModelSchema): class Meta: model = child_model return ChildSchema def test_session_is_passed_to_nested_field( self, child_schema, parent_model, session ): class ParentSchema(ModelSchema): children = Nested(child_schema, many=True) class Meta: model = parent_model data = {"name": "Parent1", "children": [{"name": "Child1"}]} ParentSchema().load(data, session=session) def test_session_is_passed_to_nested_field_in_list_field( self, parent_model, child_model, child_schema, session ): class ParentSchema(ModelSchema): children = fields.List(Nested(child_schema)) class Meta: model = parent_model data = {"name": "Jorge", "children": [{"name": "Jose"}]} ParentSchema().load(data, session=session) # regression test for https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/177 def test_transient_is_propgated_to_nested_field(self, child_schema, parent_model): class ParentSchema(ModelSchema): children = Nested(child_schema, many=True) class Meta: model = parent_model data = {"name": "Parent1", "children": [{"name": "Child1"}]} load_data = unpack(ParentSchema().load(data, transient=True)) child = load_data.children[0] state = sa.inspect(child) assert state.transient def _repr_validator_list(validators): return sorted([repr(validator) for validator in validators]) @pytest.mark.parametrize( "defaults,new,expected", [ ([validate.Length()], [], [validate.Length()]), ( [validate.Range(max=100), validate.Length(min=3)], [validate.Range(max=1000)], [validate.Range(max=1000), validate.Length(min=3)], ), ( [validate.Range(max=1000)], [validate.Length(min=3)], [validate.Range(max=1000), validate.Length(min=3)], ), ([], [validate.Length(min=3)], [validate.Length(min=3)]), ], ) def test_merge_validators(defaults, new, expected): converter = ModelConverter() validators = converter._merge_validators(defaults, new) assert _repr_validator_list(validators) == _repr_validator_list(expected) marshmallow-sqlalchemy-0.19.0/tox.ini000066400000000000000000000016271353433561000176130ustar00rootroot00000000000000[tox] envlist= lint py{36,37}-marshmallow2 py{36,37}-marshmallow3 py37-marshmallowdev py36-lowest docs [testenv] extras = tests deps = marshmallow2: marshmallow>=2.15.2,<3.0.0 marshmallow3: marshmallow>=3.0.0,<4.0.0 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz lowest: marshmallow==2.15.2 lowest: sqlalchemy==1.2.0 commands = pytest {posargs} [testenv:lint] deps = pre-commit~=1.18 skip_install = true commands = pre-commit run --all-files [testenv:docs] extras = docs commands = sphinx-build docs/ docs/_build {posargs} ; Below tasks are for development only (not run in CI) [testenv:watch-docs] deps = sphinx-autobuild extras = docs commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/marshmallow_sqlalchemy -s 2 [testenv:watch-readme] deps = restview skip_install = true commands = restview README.rst