pax_global_header00006660000000000000000000000064145562356020014522gustar00rootroot0000000000000052 comment=dc88552ae220d9810df76d40bbf9d6f513fc3504 python-marshmallow-sqlalchemy-1.0.0/000077500000000000000000000000001455623560200175255ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/.github/000077500000000000000000000000001455623560200210655ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/.github/dependabot.yml000066400000000000000000000003301455623560200237110ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" python-marshmallow-sqlalchemy-1.0.0/.github/workflows/000077500000000000000000000000001455623560200231225ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/.github/workflows/build-release.yml000066400000000000000000000044641455623560200263720ustar00rootroot00000000000000name: build on: push: branches: ["dev"] tags: ["*"] pull_request: jobs: tests: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - { name: "3.8", python: "3.8", tox: py38-marshmallow3 } - { name: "3.12", python: "3.12", tox: py312-marshmallow3 } - { name: "lowest", python: "3.8", tox: py38-lowest } - { name: "dev", python: "3.12", tox: py312-marshmallowdev } steps: - uses: actions/checkout@v4.0.0 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: python -m pip install tox - run: python -m tox -e${{ matrix.tox }} build: name: Build package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install pypa/build run: python -m pip install build - name: Build a binary wheel and a source tarball run: python -m build - name: Install twine run: python -m pip install twine - name: Check build run: python -m twine check --strict dist/* - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ # this duplicates pre-commit.ci, so only run it on tags # it guarantees that linting is passing prior to a release lint-pre-release: if: startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.0.0 - uses: actions/setup-python@v5 with: python-version: "3.11" - run: python -m pip install tox - run: python -m tox -e lint publish-to-pypi: name: PyPI release if: startsWith(github.ref, 'refs/tags/') needs: [build, tests, lint-pre-release] runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/marshmallow-sqlalchemy permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 python-marshmallow-sqlalchemy-1.0.0/.gitignore000066400000000000000000000006411455623560200215160ustar00rootroot00000000000000*.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 python-marshmallow-sqlalchemy-1.0.0/.pre-commit-config.yaml000066400000000000000000000006261455623560200240120ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.14 hooks: - id: ruff - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.27.3 hooks: - id: check-github-workflows - id: check-readthedocs - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: - id: blacken-docs additional_dependencies: [black==23.12.1] python-marshmallow-sqlalchemy-1.0.0/.readthedocs.yml000066400000000000000000000003241455623560200226120ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py formats: - pdf build: os: ubuntu-22.04 tools: python: "3.11" python: install: - method: pip path: . extra_requirements: - docs python-marshmallow-sqlalchemy-1.0.0/AUTHORS.rst000066400000000000000000000043471455623560200214140ustar00rootroot00000000000000******* 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 `_ - Michaela Ockova `@evelyn9191 `_ - Pierre Verkest `@petrus-v `_ - Erik Cederstrand `@ecederstrand `_ - Daven Quinn `@davenquinn `_ - Peter Schutt `@peterschutt `_ - Arash Fatahzade `@ArashFatahzade `_ - David Malakh `@Unix-Code `_ - Martijn Pieters `@mjpieters `_ - Bruce Adams `@bruceadams `_ - Justin Crown `@mrname `_ - Jeppe Fihl-Pearson `@Tenzer `_ - Indivar `@indiVar0508 `_ - David Doyon `@ddoyon92 `_ - Hippolyte Henry `@zippolyte `_ python-marshmallow-sqlalchemy-1.0.0/CHANGELOG.rst000066400000000000000000000440201455623560200215460ustar00rootroot00000000000000Changelog --------- 1.0.0 (2024-01-30) +++++++++++++++++++ * Remove ``__version__`` attribute. Use feature detection or ``importlib.metadata.version("marshmallow-sqlalchemy")`` instead (:pr:`568`). * Support marshmallow>=3.10.0 (:pr:`566`). * Passing `info={"marshmallow": ...}` to SQLAlchemy columns is removed, as it is redundant with the ``auto_field`` functionality (:pr:`567`). * Remove ``packaging`` as a dependency (:pr:`566`). * Support Python 3.12. 0.30.0 (2024-01-07) +++++++++++++++++++ Features: * Use ``Session.get()`` load instances to improve deserialization performance (:pr:`548`). Thanks :user:`zippolyte` for the PR. Other changes: * Drop support for Python 3.7, which is EOL (:pr:`540`). 0.29.0 (2023-02-27) +++++++++++++++++++ Features: * Support SQLAlchemy 2.0 (:pr:`494`). Thanks :user:`dependabot` for the PR. * Enable (in tests) and fix SQLAlchemy 2.0 compatibility warnings (:pr:`493`). Bug fixes: * Use mapper ``.attrs`` rather than ``.get_property`` and ``.iterate_properties`` to ensure ``registry.configure`` is called (call removed in SQLAlchemy 2.0.2) (:issue:`487`). Thanks :user:`ddoyon92` for the PR. Other changes: * Drop support for SQLAlchemy 1.3, which is EOL (:pr:`493`). 0.28.2 (2023-02-23) +++++++++++++++++++ Bug fixes: * Use .scalar_subquery() for SQLAlchemy>1.4 to suppress a warning (:issue:`459`). Thanks :user:`indiVar0508` for the PR. Other changes: * Lock SQLAlchemy<2.0 in setup.py. SQLAlchemy 2.x is not supported (:pr:`486`). * Test against Python 3.11 (:pr:`486`). 0.28.1 (2022-07-18) +++++++++++++++++++ Bug fixes: * Address ``DeprecationWarning`` re: usage of ``distutils`` (:pr:`435`). Thanks :user:`Tenzer` for the PR. 0.28.0 (2022-03-09) +++++++++++++++++++ Features: * Add support for generating fields from `column_property` (:issue:`97`). Thanks :user:`mrname` for the PR. Other changes: * Drop support for Python 3.6, which is EOL. * Drop support for SQLAlchemy 1.2, which is EOL. 0.27.0 (2021-12-18) +++++++++++++++++++ Features: * Distribute type information per `PEP 561 `_ (:pr:`420`). Thanks :user:`bruceadams` for the PR. Other changes: * Test against Python 3.10 (:pr:`421`). 0.26.1 (2021-06-05) +++++++++++++++++++ Bug fixes: * Fix generating fields for ``postgreql.ARRAY`` columns (:issue:`392`). Thanks :user:`mjpieters` for the catch and patch. 0.26.0 (2021-05-26) +++++++++++++++++++ Bug fixes: * Unwrap proxied columns to handle models for subqueries (:issue:`383`). Thanks :user:`mjpieters` for the catch and patch * Fix setting ``transient`` on a per-instance basis when the ``transient`` Meta option is set (:issue:`388`). Thanks again :user:`mjpieters`. Other changes: * *Backwards-incompatible*: Remove deprecated ``ModelSchema`` and ``TableSchema`` classes. 0.25.0 (2021-05-02) +++++++++++++++++++ * Add ``load_instance`` as a parameter to `SQLAlchemySchema` and `SQLAlchemyAutoSchema` (:pr:`380`). Thanks :user:`mjpieters` for the PR. 0.24.3 (2021-04-26) +++++++++++++++++++ * Fix deprecation warnings from marshmallow 3.10 and SQLAlchemy 1.4 (:pr:`369`). Thanks :user:`peterschutt` for the PR. 0.24.2 (2021-02-07) +++++++++++++++++++ * ``auto_field`` supports ``association_proxy`` fields with local multiplicity (``uselist=True``) (:issue:`364`). Thanks :user:`Unix-Code` for the catch and patch. 0.24.1 (2020-11-20) +++++++++++++++++++ * ``auto_field`` works with ``association_proxy`` (:issue:`338`). Thanks :user:`AbdealiJK`. 0.24.0 (2020-10-20) +++++++++++++++++++ * *Backwards-incompatible*: Drop support for marshmallow 2.x, which is now EOL. * Test against Python 3.9. 0.23.1 (2020-05-30) +++++++++++++++++++ Bug fixes: * Don't add no-op `Length` validator (:pr:`315`). Thanks :user:`taion` for the PR. 0.23.0 (2020-04-26) +++++++++++++++++++ Bug fixes: * Fix data keys when using ``Related`` with a ``Column`` that is named differently from its attribute (:issue:`299`). Thanks :user:`peterschutt` for the catch and patch. * Fix bug that raised an exception when using the `ordered = True` option on a schema that has an `auto_field` (:issue:`306`). Thanks :user:`KwonL` for reporting and thanks :user:`peterschutt` for the PR. 0.22.3 (2020-03-01) +++++++++++++++++++ Bug fixes: * Fix ``DeprecationWarning`` getting raised even when user code does not use ``TableSchema`` or ``ModelSchema`` (:issue:`289`). Thanks :user:`5uper5hoot` for reporting. 0.22.2 (2020-02-09) +++++++++++++++++++ Bug fixes: * Avoid error when using ``SQLAlchemyAutoSchema``, ``ModelSchema``, or ``fields_for_model`` with a model that has a ``SynonymProperty`` (:issue:`190`). Thanks :user:`TrilceAC` for reporting. * ``auto_field`` and ``field_for`` work with ``SynonymProperty`` (:pr:`280`). Other changes: * Add hook in ``ModelConverter`` for changing field names based on SQLA columns and properties (:issue:`276`). Thanks :user:`davenquinn` for the suggestion and the PR. 0.22.1 (2020-02-09) +++++++++++++++++++ Bug fixes: * Fix behavior when passing ``table`` to ``auto_field`` (:pr:`277`). 0.22.0 (2020-02-09) +++++++++++++++++++ Features: * Add ``SQLAlchemySchema`` and ``SQLAlchemyAutoSchema``, which have an improved API for generating marshmallow fields and overriding their arguments via ``auto_field`` (:issue:`240`). Thanks :user:`taion` for the idea and original implementation. .. code-block:: python # Before from marshmallow_sqlalchemy import ModelSchema, field_for from . import models class ArtistSchema(ModelSchema): class Meta: model = models.Artist id = field_for(models.Artist, "id", dump_only=True) created_at = field_for(models.Artist, "created_at", dump_only=True) # After from marshmallow_sqlalchemy import SQLAlchemySchema, auto_field from . import models class ArtistSchema(SQLAlchemyAutoSchema): class Meta: model = models.Artist id = auto_field(dump_only=True) created_at = auto_field(dump_only=True) * Add ``load_instance`` option to configure deserialization to model instances (:issue:`193`, :issue:`270`). * Add ``include_relationships`` option to configure generation of marshmallow fields for relationship properties (:issue:`98`). Thanks :user:`dusktreader` for the suggestion. Deprecations: * ``ModelSchema`` and ``TableSchema`` are deprecated, since ``SQLAlchemyAutoSchema`` has equivalent functionality. .. code-block:: python # Before from marshmallow_sqlalchemy import ModelSchema, TableSchema from . import models class ArtistSchema(ModelSchema): class Meta: model = models.Artist class AlbumSchema(TableSchema): class Meta: table = models.Album.__table__ # After from marshmallow_sqlalchemy import SQLAlchemyAutoSchema from . import models class ArtistSchema(SQLAlchemyAutoSchema): class Meta: model = models.Artist include_relationships = True load_instance = True class AlbumSchema(SQLAlchemyAutoSchema): class Meta: table = models.Album.__table__ * Passing `info={"marshmallow": ...}` to SQLAlchemy columns is deprecated, as it is redundant with the ``auto_field`` functionality. Other changes: * *Backwards-incompatible*: ``fields_for_model`` does not include relationships by default. Use ``fields_for_model(..., include_relationships=True)`` to preserve the old behavior. 0.21.0 (2019-12-04) +++++++++++++++++++ * Add support for ``postgresql.OID`` type (:pr:`262`). Thanks :user:`petrus-v` for the PR. * Remove imprecise Python 3 classifier from PyPI metadata (:pr:`255`). Thanks :user:`ecederstrand`. 0.20.0 (2019-12-01) +++++++++++++++++++ * Add support for ``mysql.DATETIME`` and ``mysql.INTEGER`` type (:issue:`204`). * Add support for ``postgresql.CIDR`` type (:issue:`183`). * Add support for ``postgresql.DATE`` and ``postgresql.TIME`` type. Thanks :user:`evelyn9191` for the PR. 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. python-marshmallow-sqlalchemy-1.0.0/CONTRIBUTING.rst000066400000000000000000000065211455623560200221720ustar00rootroot00000000000000Contributing Guidelines ======================= 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 formatting and syntax checks: :: $ tox -e lint (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): :: $ tox Documentation ------------- Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (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: https://www.sphinx-doc.org/ .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html .. _`marshmallow-sqlalchemy`: https://github.com/marshmallow-code/marshmallow-sqlalchemy python-marshmallow-sqlalchemy-1.0.0/LICENSE000066400000000000000000000020501455623560200205270ustar00rootroot00000000000000Copyright 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. python-marshmallow-sqlalchemy-1.0.0/README.rst000066400000000000000000000114541455623560200212210ustar00rootroot00000000000000********************** marshmallow-sqlalchemy ********************** |pypi-package| |build-status| |docs| |marshmallow3| |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, nullable=False) 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 SQLAlchemySchema, auto_field class AuthorSchema(SQLAlchemySchema): class Meta: model = Author load_instance = True # Optional: deserialize to model instances id = auto_field() name = auto_field() books = auto_field() class BookSchema(SQLAlchemySchema): class Meta: model = Book load_instance = True id = auto_field() title = auto_field() author_id = auto_field() You can automatically generate fields for a model's columns using `SQLAlchemyAutoSchema`. The following schema classes are equivalent to the above. .. code-block:: python from marshmallow_sqlalchemy import SQLAlchemyAutoSchema class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author include_relationships = True load_instance = True class BookSchema(SQLAlchemyAutoSchema): class Meta: model = Book include_fk = True load_instance = True 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) # {'id': 1, 'name': 'Chuck Paluhniuk', 'books': [1]} load_data = author_schema.load(dump_data, session=session) print(load_data) # Get it now ========== :: pip install -U marshmallow-sqlalchemy Requires Python >= 3.8, marshmallow >= 3.0.0, and SQLAlchemy >= 1.4.40. 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 - Contributing Guidelines: https://marshmallow-sqlalchemy.readthedocs.io/en/latest/contributing.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://github.com/marshmallow-code/marshmallow-sqlalchemy/actions/workflows/build-release.yml/badge.svg :target: https://github.com/marshmallow-code/marshmallow-sqlalchemy/actions/workflows/build-release.yml :alt: Build status .. |docs| image:: https://readthedocs.org/projects/marshmallow-sqlalchemy/badge/ :target: http://marshmallow-sqlalchemy.readthedocs.io/ :alt: Documentation .. |marshmallow3| image:: https://badgen.net/badge/marshmallow/3 :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 python-marshmallow-sqlalchemy-1.0.0/RELEASING.md000066400000000000000000000004231455623560200213570ustar00rootroot00000000000000# Releasing 1. Bump version in `pyproject.toml` and update the changelog with today's date. 2. Commit: `git commit -m "Bump version and update changelog"` 3. Tag the commit: `git tag x.y.z` 4. Push: `git push --tags origin dev`. CI will take care of the PyPI release. python-marshmallow-sqlalchemy-1.0.0/SECURITY.md000066400000000000000000000003001455623560200213070ustar00rootroot00000000000000# Security Contact Information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. python-marshmallow-sqlalchemy-1.0.0/docs/000077500000000000000000000000001455623560200204555ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/docs/Makefile000066400000000000000000000151721455623560200221230ustar00rootroot00000000000000# 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." python-marshmallow-sqlalchemy-1.0.0/docs/_static/000077500000000000000000000000001455623560200221035ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/docs/_static/marshmallow-sqlalchemy-logo.png000066400000000000000000000306031455623560200302370ustar00rootroot00000000000000PNG  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`python-marshmallow-sqlalchemy-1.0.0/docs/_templates/000077500000000000000000000000001455623560200226125ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/docs/_templates/_templates/000077500000000000000000000000001455623560200247475ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/docs/_templates/_templates/useful-links.html000066400000000000000000000002241455623560200302540ustar00rootroot00000000000000

Useful Links

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

Useful Links

    {% for text, uri in theme_extra_nav_links.items() %}
  • {{text}}
  • {% endfor %}
python-marshmallow-sqlalchemy-1.0.0/docs/api_reference.rst000066400000000000000000000010661455623560200240010ustar00rootroot00000000000000.. _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: python-marshmallow-sqlalchemy-1.0.0/docs/authors.rst000066400000000000000000000000341455623560200226710ustar00rootroot00000000000000.. include:: ../AUTHORS.rst python-marshmallow-sqlalchemy-1.0.0/docs/changelog.rst000066400000000000000000000000561455623560200231370ustar00rootroot00000000000000.. _changelog: .. include:: ../CHANGELOG.rst python-marshmallow-sqlalchemy-1.0.0/docs/conf.py000077500000000000000000000041611455623560200217610ustar00rootroot00000000000000import importlib.metadata from collections import OrderedDict import alabaster 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" version = release = importlib.metadata.version("marshmallow-sqlalchemy") 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", ], } python-marshmallow-sqlalchemy-1.0.0/docs/contributing.rst000066400000000000000000000000411455623560200237110ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst python-marshmallow-sqlalchemy-1.0.0/docs/index.rst000066400000000000000000000073671455623560200223330ustar00rootroot00000000000000********************** 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, nullable=False) 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 SQLAlchemySchema, auto_field class AuthorSchema(SQLAlchemySchema): class Meta: model = Author load_instance = True # Optional: deserialize to model instances id = auto_field() name = auto_field() books = auto_field() class BookSchema(SQLAlchemySchema): class Meta: model = Book load_instance = True id = auto_field() title = auto_field() author_id = auto_field() You can automatically generate fields for a model's columns using `SQLAlchemyAutoSchema `. The following schema classes are equivalent to the above. .. code-block:: python from marshmallow_sqlalchemy import SQLAlchemyAutoSchema class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author include_relationships = True load_instance = True class BookSchema(SQLAlchemyAutoSchema): class Meta: model = Book include_fk = True load_instance = True Make sure to declare `Models` before instantiating `Schemas`. Otherwise `sqlalchemy.orm.configure_mappers() `_ will run too soon and fail. .. note:: Any `column_property` on the model that does not derive directly from `Column` (such as a mapped expression), will be detected and marked as `dump_only`. `hybrid_property` is not automatically handled at all, and would need to be explicitly declared as a field. (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) # {'id': 1, 'name': 'Chuck Paluhniuk', 'books': [1]} load_data = author_schema.load(dump_data, session=session) print(load_data) # Get it now ========== :: pip install -U marshmallow-sqlalchemy Requires Python >= 3.8, marshmallow >= 3.0.0, and SQLAlchemy >= 1.4.40. Learn ===== .. toctree:: :maxdepth: 2 recipes API === .. toctree:: :maxdepth: 2 api_reference Project Info ============ .. toctree:: :maxdepth: 1 changelog contributing authors license python-marshmallow-sqlalchemy-1.0.0/docs/license.rst000066400000000000000000000000701455623560200226260ustar00rootroot00000000000000******* License ******* .. literalinclude:: ../LICENSE python-marshmallow-sqlalchemy-1.0.0/docs/make.bat000066400000000000000000000145031455623560200220650ustar00rootroot00000000000000@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 python-marshmallow-sqlalchemy-1.0.0/docs/recipes.rst000066400000000000000000000246561455623560200226560ustar00rootroot00000000000000.. _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 SQLAlchemySchema from .db import Session class BaseSchema(SQLAlchemySchema): 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 SQLAlchemySchemaOpts, SQLAlchemySchema from .db import Session class BaseOpts(SQLAlchemySchemaOpts): 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(SQLAlchemySchema): 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 `SQLAlchemyAutoSchema `. 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__.attrs.get("id") property2field(id_prop) # , ...> Overriding Generated Fields =========================== Any field generated by a `SQLAlchemyAutoSchema ` can be overridden. .. code-block:: python from marshmallow import fields from marshmallow_sqlalchemy import SQLAlchemyAutoSchema from marshmallow_sqlalchemy.fields import Nested class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author # Override books field to use a nested representation rather than pks books = Nested(BookSchema, many=True, exclude=("author",)) You can use the `auto_field ` 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 SQLAlchemyAutoSchema, field_for class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author # Generate a field, passing in an additional dump_only argument date_created = auto_field(dump_only=True) If a field's external data key differs from the model's column name, you can pass a column name to `auto_field `. .. code-block:: python class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author # Exclude date_created because we're aliasing it below exclude = ("date_created",) # Generate "created_date" field from "date_created" column created_date = auto_field("date_created", dump_only=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, SQLAlchemyAutoSchema 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, (SQLAlchemyAutoSchema,), {"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 SQLAlchemySchema, auto_field class BookSchema(SQLAlchemySchema): id = auto_field() author = SmartNested(AuthorSchema) class Meta: model = Book sqla_session = Session book = Book(id=1) book.author = Author(name="Chuck Paluhniuk") session.add(book) 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 `SQLAlchemySchema `. .. code-block:: python from marshmallow_sqlalchemy import SQLAlchemyAutoSchema class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author load_instance = True 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 SQLAlchemyAutoSchema class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author sqla_session = Session load_instance = True 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. Controlling Instance Loading ============================ You can override the schema ``load_instance`` flag by passing in a ``load_instance`` argument when creating the schema instance. Use this to switch between loading to a dictionary or to a model instance: .. code-block:: python from marshmallow_sqlalchemy import SQLAlchemyAutoSchema class AuthorSchema(SQLAlchemyAutoSchema): class Meta: model = Author sqla_session = Session load_instance = True dump_data = {"id": 1, "name": "John Steinbeck"} print(AuthorSchema().load(dump_data)) # loading an instance # print(AuthorSchema(load_instance=False).load(dump_data)) # loading a dict # {"id": 1, "name": "John Steinbeck"} python-marshmallow-sqlalchemy-1.0.0/pyproject.toml000066400000000000000000000034441455623560200224460ustar00rootroot00000000000000[project] name = "marshmallow-sqlalchemy" version = "1.0.0" description = "SQLAlchemy integration with the marshmallow (de)serialization library" readme = "README.rst" license = { file = "LICENSE" } maintainers = [{ name = "Steven Loria", email = "sloria1@gmail.com" }] classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] requires-python = ">=3.8" dependencies = ["marshmallow>=3.10.0", "SQLAlchemy>=1.4.40,<3.0"] [project.urls] Changelog = "https://marshmallow-sqlalchemy.readthedocs.io/en/latest/changelog.html" Funding = "https://opencollective.com/marshmallow" Issues = "https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues" Source = "https://github.com/marshmallow-code/marshmallow-sqlalchemy" [project.optional-dependencies] docs = ["sphinx==7.2.6", "alabaster==0.7.16", "sphinx-issues==4.0.0"] # TODO: Remove pytest pin when https://github.com/TvoroG/pytest-lazy-fixture/issues/65 is resolved tests = ["pytest<8", "pytest-lazy-fixture>=0.6.2"] dev = ["marshmallow-sqlalchemy[tests]", "tox", "pre-commit~=3.5"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.sdist] include = ["docs/", "tests/", "CHANGELOG.rst", "CONTRIBUTING.rst", "tox.ini"] exclude = ["docs/_build/"] [tool.ruff] src = ["src"] fix = true show-fixes = true show-source = true [tool.ruff.lint] ignore = ["E203", "E266", "E501", "E731"] select = [ "B", # flake8-bugbear "E", # pycodestyle error "F", # pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warning ] python-marshmallow-sqlalchemy-1.0.0/pytest.ini000066400000000000000000000002641455623560200215600ustar00rootroot00000000000000[pytest] filterwarnings = ignore:.*Binary.*:sqlalchemy.exc.SADeprecationWarning ignore:.*Decimal.*:sqlalchemy.exc.SAWarning ignore:.*Passing `info=*:DeprecationWarning python-marshmallow-sqlalchemy-1.0.0/src/000077500000000000000000000000001455623560200203145ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/000077500000000000000000000000001455623560200250645ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/__init__.py000066400000000000000000000011131455623560200271710ustar00rootroot00000000000000from .convert import ( ModelConverter, column2field, field_for, fields_for_model, property2field, ) from .exceptions import ModelConversionError from .schema import ( SQLAlchemyAutoSchema, SQLAlchemyAutoSchemaOpts, SQLAlchemySchema, SQLAlchemySchemaOpts, auto_field, ) __all__ = [ "SQLAlchemySchema", "SQLAlchemyAutoSchema", "SQLAlchemySchemaOpts", "SQLAlchemyAutoSchemaOpts", "auto_field", "ModelConverter", "fields_for_model", "property2field", "column2field", "ModelConversionError", "field_for", ] python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/convert.py000066400000000000000000000340671455623560200271300ustar00rootroot00000000000000import functools import inspect import uuid import marshmallow as ma import sqlalchemy as sa from marshmallow import fields, validate from sqlalchemy.dialects import mssql, mysql, postgresql from sqlalchemy.orm import SynonymProperty from .exceptions import ModelConversionError from .fields import Related, RelatedList def _is_field(value): return isinstance(value, type) and issubclass(value, fields.Field) def _base_column(column): """Unwrap proxied columns""" if column not in column.base_columns and len(column.base_columns) == 1: [base] = column.base_columns return base return column 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) ) def _field_update_kwargs(field_class, field_kwargs, kwargs): if not kwargs: return field_kwargs if isinstance(field_class, functools.partial): # Unwrap partials, assuming that they bind a Field to arguments field_class = field_class.func possible_field_keywords = { key for cls in inspect.getmro(field_class) for key, param in inspect.signature(cls).parameters.items() if param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD or param.kind is inspect.Parameter.KEYWORD_ONLY } for k, v in kwargs.items(): if k in possible_field_keywords: field_kwargs[k] = v else: field_kwargs["metadata"][k] = v return field_kwargs 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.OID: fields.Integer, postgresql.UUID: fields.UUID, postgresql.MACADDR: fields.String, postgresql.INET: fields.String, postgresql.CIDR: fields.String, postgresql.JSON: fields.Raw, postgresql.JSONB: fields.Raw, postgresql.HSTORE: fields.Raw, postgresql.ARRAY: _postgres_array_factory, postgresql.MONEY: fields.Decimal, postgresql.DATE: fields.Date, postgresql.TIME: fields.Time, mysql.BIT: fields.Integer, mysql.YEAR: fields.Integer, mysql.SET: fields.List, mysql.ENUM: fields.Field, mysql.INTEGER: fields.Integer, mysql.DATETIME: fields.DateTime, mssql.BIT: fields.Integer, mssql.UNIQUEIDENTIFIER: fields.UUID, } 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, include_relationships=False, fields=None, exclude=None, base_fields=None, dict_cls=dict, ): result = dict_cls() base_fields = base_fields or {} for prop in model.__mapper__.attrs: key = self._get_field_name(prop) if self._should_exclude_field(prop, fields=fields, exclude=exclude): # Allow marshmallow to validate and exclude the field key. result[key] = None continue if isinstance(prop, SynonymProperty): 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 if not include_relationships and hasattr(prop, "direction"): continue field = base_fields.get(key) or self.property2field(prop) if field: result[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: key = self._get_field_name(column) if self._should_exclude_field(column, fields=fields, exclude=exclude): # Allow marshmallow to validate and exclude the field key. result[key] = None continue if not include_fk and column.foreign_keys: continue # Overridden fields are specified relative to key generated by # self._get_key_for_column(...), rather than keys in source model field = base_fields.get(key) or self.column2field(column) if field: result[key] = field return result def property2field(self, prop, *, instance=True, field_class=None, **kwargs): # handle synonyms # Attribute renamed "_proxied_object" in 1.4 for attr in ("_proxied_property", "_proxied_object"): proxied_obj = getattr(prop, attr, None) if proxied_obj is not None: prop = proxied_obj 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_update_kwargs(field_class, field_kwargs, kwargs) ret = field_class(**field_kwargs) if ( hasattr(prop, "direction") and self.DIRECTION_MAPPING[prop.direction.name] and prop.uselist is True ): related_list_kwargs = _field_update_kwargs( RelatedList, self.get_base_kwargs(), kwargs ) ret = RelatedList(ret, **related_list_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_update_kwargs(field_class, field_kwargs, kwargs) return field_class(**field_kwargs) def field_for(self, model, property_name, **kwargs): target_model = model prop_name = property_name attr = getattr(model, property_name) remote_with_local_multiplicity = False if hasattr(attr, "remote_attr"): target_model = attr.target_class prop_name = attr.value_attr remote_with_local_multiplicity = attr.local_attr.prop.uselist prop = target_model.__mapper__.attrs.get(prop_name) converted_prop = self.property2field(prop, **kwargs) if remote_with_local_multiplicity: related_list_kwargs = _field_update_kwargs( RelatedList, self.get_base_kwargs(), kwargs ) return RelatedList(converted_prop, **related_list_kwargs) else: return converted_prop def _get_field_name(self, prop_or_column): return prop_or_column.key 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( f"Could not find field column of type {types[0]}." ) return field_cls def _get_field_class_for_property(self, prop): if hasattr(prop, "direction"): field_cls = Related else: column = _base_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 = _base_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["metadata"]["description"] = prop.doc return kwargs def _add_column_kwargs(self, kwargs, column): """Add keyword arguments to kwargs (in-place) based on the passed in `Column `. """ if hasattr(column, "nullable"): if column.nullable: kwargs["allow_none"] = True kwargs["required"] = not column.nullable and not _has_default(column) # If there is no nullable attribute, we are dealing with a property # that does not derive from the Column class. Mark as dump_only. else: kwargs["dump_only"] = True if hasattr(column.type, "enums") and not kwargs.get("dump_only"): 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") and not kwargs.get("dump_only"): column_length = column.type.length if column_length is not None: 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_length)) if getattr(column.type, "asdecimal", False): 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): key = self._get_field_name(column) if fields and key not in fields: return True if exclude and key in exclude: return True return False def get_base_kwargs(self): return {"validate": [], "metadata": {}} 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. Note: SynonymProperties are ignored. Use an explicit field if you want to include a synonym. :param model: The SQLAlchemy model :param bool include_fk: Whether to include foreign key fields in the output. :param bool include_relationships: Whether to include relationships 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. """ python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/exceptions.py000066400000000000000000000007721455623560200276250ustar00rootroot00000000000000class 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. """ class IncorrectSchemaTypeError(ModelConversionError): """Raised when a ``SQLAlchemyAutoField`` is bound to ``Schema`` that is not an instance of ``SQLAlchemySchema``. """ python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/fields.py000066400000000000000000000124211455623560200267040ustar00rootroot00000000000000import warnings from marshmallow import fields from marshmallow.utils import is_iterable_but_not_string from sqlalchemy import inspect 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:`SQLAlchemySchema`. :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, columns=None, column=None, **kwargs): if column is not None: warnings.warn( "`column` parameter is deprecated and will be removed in future releases. " "Use `columns` instead.", DeprecationWarning, stacklevel=2, ) if columns is None: columns = column super().__init__(**kwargs) self.columns = ensure_list(columns 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: insp = inspect(self.related_model) return [insp.attrs[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] raise self.make_error("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.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, related_model, value): """Retrieve the related object from an existing instance in the DB. :param related_model: The related model to query :param value: The serialized value to mapto an existing instance. :raises NoResultFound: if there is no matching record. """ if self.columns: result = ( self.session.query(related_model) .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 = self.session.get(related_model, lookup_values) except TypeError as error: keys = [prop.key for prop in self.related_keys] raise self.make_error("invalid", value=value, keys=keys) from error 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) python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/load_instance_mixin.py000066400000000000000000000122271455623560200314510ustar00rootroot00000000000000"""Mixin that adds model instance loading behavior. .. warning:: This module is treated as private API. Users should not need to use this module directly. """ import marshmallow as ma from sqlalchemy.orm.exc import ObjectDeletedError from .fields import get_primary_keys class LoadInstanceMixin: class Opts: def __init__(self, meta, *args, **kwargs): super().__init__(meta, *args, **kwargs) self.sqla_session = getattr(meta, "sqla_session", None) self.load_instance = getattr(meta, "load_instance", False) self.transient = getattr(meta, "transient", False) class Schema: @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): if self._transient is not None: return self._transient return 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) self._load_instance = kwargs.pop("load_instance", self.opts.load_instance) 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(): try: return self.session.get(self.opts.model, filters) except ObjectDeletedError: return None return None @ma.post_load def make_instance(self, data, **kwargs): """Deserialize data to an instance of the model if self.load_instance is True. 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. """ if not self._load_instance: return data 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 self._load_instance and 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 python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/py.typed000066400000000000000000000000001455623560200265510ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/src/marshmallow_sqlalchemy/schema.py000066400000000000000000000173621455623560200267070ustar00rootroot00000000000000import sqlalchemy as sa from marshmallow.fields import Field from marshmallow.schema import Schema, SchemaMeta, SchemaOpts from sqlalchemy.ext.declarative import DeclarativeMeta from .convert import ModelConverter from .exceptions import IncorrectSchemaTypeError from .load_instance_mixin import LoadInstanceMixin # This isn't really a field; it's a placeholder for the metaclass. # This should be considered private API. class SQLAlchemyAutoField(Field): def __init__(self, *, column_name=None, model=None, table=None, field_kwargs): super().__init__() if model and table: raise ValueError("Cannot pass both `model` and `table` options.") self.column_name = column_name self.model = model self.table = table self.field_kwargs = field_kwargs def create_field(self, schema_opts, column_name, converter): model = self.model or schema_opts.model if model: return converter.field_for(model, column_name, **self.field_kwargs) else: table = self.table if self.table is not None else schema_opts.table column = getattr(table.columns, column_name) return converter.column2field(column, **self.field_kwargs) # This field should never be bound to a schema. # If this method is called, it's probably because the schema is not a SQLAlchemySchema. def _bind_to_schema(self, field_name, schema): raise IncorrectSchemaTypeError( f"Cannot bind SQLAlchemyAutoField. Make sure that {schema} is a SQLAlchemySchema or SQLAlchemyAutoSchema." ) class SQLAlchemySchemaOpts(LoadInstanceMixin.Opts, SchemaOpts): """Options class for `SQLAlchemySchema`. Adds the following options: - ``model``: The SQLAlchemy model to generate the `Schema` from (mutually exclusive with ``table``). - ``table``: The SQLAlchemy table to generate the `Schema` from (mutually exclusive with ``model``). - ``load_instance``: Whether to load model instances. - ``sqla_session``: SQLAlchemy session to be used for deserialization. This is only needed when ``load_instance`` is `True`. You can also pass a session to the Schema's `load` method. - ``transient``: Whether to load model instances in a transient state (effectively ignoring the session). Only relevant when ``load_instance`` is `True`. - ``model_converter``: `ModelConverter` class to use for converting the SQLAlchemy model to marshmallow fields. """ def __init__(self, meta, *args, **kwargs): super().__init__(meta, *args, **kwargs) self.model = getattr(meta, "model", None) self.table = getattr(meta, "table", None) if self.model is not None and self.table is not None: raise ValueError("Cannot set both `model` and `table` options.") self.model_converter = getattr(meta, "model_converter", ModelConverter) class SQLAlchemyAutoSchemaOpts(SQLAlchemySchemaOpts): """Options class for `SQLAlchemyAutoSchema`. Has the same options as `SQLAlchemySchemaOpts`, with the addition of: - ``include_fk``: Whether to include foreign fields; defaults to `False`. - ``include_relationships``: Whether to include relationships; defaults to `False`. """ def __init__(self, meta, *args, **kwargs): super().__init__(meta, *args, **kwargs) self.include_fk = getattr(meta, "include_fk", False) self.include_relationships = getattr(meta, "include_relationships", False) if self.table is not None and self.include_relationships: raise ValueError("Cannot set `table` and `include_relationships = True`.") class SQLAlchemySchemaMeta(SchemaMeta): @classmethod def get_declared_fields(mcs, klass, cls_fields, inherited_fields, dict_cls): opts = klass.opts Converter = opts.model_converter converter = Converter(schema_cls=klass) fields = super().get_declared_fields( klass, cls_fields, inherited_fields, dict_cls ) fields.update(mcs.get_declared_sqla_fields(fields, converter, opts, dict_cls)) fields.update(mcs.get_auto_fields(fields, converter, opts, dict_cls)) return fields @classmethod def get_declared_sqla_fields(mcs, base_fields, converter, opts, dict_cls): return {} @classmethod def get_auto_fields(mcs, fields, converter, opts, dict_cls): return dict_cls( { field_name: field.create_field( opts, field.column_name or field_name, converter ) for field_name, field in fields.items() if isinstance(field, SQLAlchemyAutoField) and field_name not in opts.exclude } ) class SQLAlchemyAutoSchemaMeta(SQLAlchemySchemaMeta): @classmethod def get_declared_sqla_fields(cls, base_fields, converter, opts, dict_cls): fields = dict_cls() if opts.table is not None: fields.update( 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, ) ) elif opts.model is not None: fields.update( converter.fields_for_model( opts.model, fields=opts.fields, exclude=opts.exclude, include_fk=opts.include_fk, include_relationships=opts.include_relationships, base_fields=base_fields, dict_cls=dict_cls, ) ) return fields class SQLAlchemySchema( LoadInstanceMixin.Schema, Schema, metaclass=SQLAlchemySchemaMeta ): """Schema for a SQLAlchemy model or table. Use together with `auto_field` to generate fields from columns. Example: :: from marshmallow_sqlalchemy import SQLAlchemySchema, auto_field from mymodels import User class UserSchema(SQLAlchemySchema): class Meta: model = User id = auto_field() created_at = auto_field(dump_only=True) name = auto_field() """ OPTIONS_CLASS = SQLAlchemySchemaOpts class SQLAlchemyAutoSchema(SQLAlchemySchema, metaclass=SQLAlchemyAutoSchemaMeta): """Schema that automatically generates fields from the columns of a SQLAlchemy model or table. Example: :: from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, auto_field from mymodels import User class UserSchema(SQLAlchemyAutoSchema): class Meta: model = User # OR # table = User.__table__ created_at = auto_field(dump_only=True) """ OPTIONS_CLASS = SQLAlchemyAutoSchemaOpts def auto_field( column_name: str = None, *, model: DeclarativeMeta = None, table: sa.Table = None, **kwargs, ): """Mark a field to autogenerate from a model or table. :param column_name: Name of the column to generate the field from. If ``None``, matches the field name. If ``attribute`` is unspecified, ``attribute`` will be set to the same value as ``column_name``. :param model: Model to generate the field from. If ``None``, uses ``model`` specified on ``class Meta``. :param table: Table to generate the field from. If ``None``, uses ``table`` specified on ``class Meta``. :param kwargs: Field argument overrides. """ if column_name is not None: kwargs.setdefault("attribute", column_name) return SQLAlchemyAutoField( column_name=column_name, model=model, table=table, field_kwargs=kwargs ) python-marshmallow-sqlalchemy-1.0.0/tests/000077500000000000000000000000001455623560200206675ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/tests/__init__.py000066400000000000000000000000001455623560200227660ustar00rootroot00000000000000python-marshmallow-sqlalchemy-1.0.0/tests/conftest.py000066400000000000000000000147551455623560200231020ustar00rootroot00000000000000import datetime as dt from types import SimpleNamespace import pytest import sqlalchemy as sa from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import ( backref, column_property, declarative_base, relationship, sessionmaker, synonym, ) 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, future=True) @pytest.fixture() def session(Base, models, engine): Session = sessionmaker(bind=engine) Base.metadata.create_all(bind=engine) return Session(future=True) @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) 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) student_ids = association_proxy( "students", "id", creator=lambda sid: Student(id=sid) ) @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=lambda: dt.datetime.now(dt.timezone.utc), 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"), ) # Test complex column property course_count = column_property( sa.select(sa.func.count(student_course.c.course_id)) .where(student_course.c.student_id == id) .scalar_subquery() ) @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")) curr_school_id = synonym("current_school_id") substitute = relationship("SubstituteTeacher", uselist=False, backref="teacher") @property def fname(self): return self.full_name 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, ) python-marshmallow-sqlalchemy-1.0.0/tests/test_conversion.py000066400000000000000000000326031455623560200244710ustar00rootroot00000000000000import datetime as dt import decimal import uuid import pytest import sqlalchemy as sa from marshmallow import Schema, fields, validate from sqlalchemy.dialects import mysql, postgresql from sqlalchemy.orm import column_property from marshmallow_sqlalchemy import ( ModelConversionError, ModelConverter, column2field, field_for, fields_for_model, property2field, ) from marshmallow_sqlalchemy.fields import Related, RelatedList def contains_validator(field, v_type): for v in field.validators: if isinstance(v, v_type): return v return False 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_none_length_validator_not_set(self, models): fields_ = fields_for_model(models.Course) assert not contains_validator(fields_["transcription"], validate.Length) 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, include_relationships=True) assert type(student_fields["courses"]) is RelatedList course_fields = fields_for_model(models.Course, include_relationships=True) assert type(course_fields["students"]) is RelatedList def test_many_to_one_relationship(self, models): student_fields = fields_for_model(models.Student, include_relationships=True) assert type(student_fields["current_school"]) is Related school_fields = fields_for_model(models.School, include_relationships=True) 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_rename_key(self, models): class RenameConverter(ModelConverter): def _get_field_name(self, prop): if prop.key == "name": return "title" return prop.key converter = RenameConverter() fields = converter.fields_for_model(models.Paper) assert "title" in fields assert "name" not in fields def test_subquery_proxies(self, session, Base, models): # Model from a subquery, columns are proxied. # https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/383 first_graders = session.query(models.Student).filter( models.Student.courses.any(models.Course.grade == 1) ) class FirstGradeStudent(Base): __table__ = first_graders.subquery("first_graders") fields_ = fields_for_model(FirstGradeStudent) assert fields_["dob"].allow_none is True 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) is MyDateTimeField @pytest.mark.parametrize( ("sa_type", "field_type"), ( (sa.String, fields.Str), (sa.Unicode, 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), (sa.Interval, fields.TimeDelta), (postgresql.UUID, fields.UUID), (postgresql.MACADDR, fields.Str), (postgresql.INET, fields.Str), (postgresql.BIT, fields.Integer), (postgresql.OID, fields.Integer), (postgresql.CIDR, fields.String), (postgresql.DATE, fields.Date), (postgresql.TIME, fields.Time), (mysql.INTEGER, fields.Integer), (mysql.DATETIME, fields.DateTime), ), ) def test_convert_types(self, converter, sa_type, field_type): prop = make_property(sa_type()) field = converter.property2field(prop) assert type(field) is field_type def test_convert_Numeric(self, converter): prop = make_property(sa.Numeric(scale=2)) field = converter.property2field(prop) assert type(field) is 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) is fields.List inner_field = getattr(field, "inner", getattr(field, "container", None)) assert type(inner_field) is fields.Str def test_convert_ARRAY_Integer(self, converter): prop = make_property(postgresql.ARRAY(sa.Integer)) field = converter.property2field(prop) assert type(field) is fields.List inner_field = getattr(field, "inner", getattr(field, "container", None)) assert type(inner_field) is 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__.attrs.get("id") field = converter.property2field(prop) assert field.required is False def test_handle_expression_based_column_property(self, models, converter): """ Tests ability to handle a column_property with a mapped expression value. Such properties should be marked as dump_only, and the type should be properly inferred. """ prop = models.Student.__mapper__.attrs.get("course_count") field = converter.property2field(prop) assert type(field) is fields.Integer assert field.dump_only is True def test_handle_simple_column_property(self, models, converter): """ Tests handling of column properties that do not derive directly from Column """ prop = models.Seminar.__mapper__.attrs.get("label") field = converter.property2field(prop) assert type(field) is fields.String assert field.dump_only is True class TestPropToFieldClass: def test_property2field(self): prop = make_property(sa.Integer()) field = property2field(prop, instance=True) assert type(field) is fields.Int field_cls = property2field(prop, instance=False) assert field_cls is 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) is fields.String field_cls = column2field(column, instance=False) assert field_cls is 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) is fields.Str field = field_for(models.Student, "current_school", session=session) assert type(field) is Related field = field_for(models.Student, "full_name", field_class=fields.Date) assert type(field) is fields.Date def test_related_initialization_warning(self, models, session): with pytest.warns( DeprecationWarning, match="column` parameter is deprecated and will be removed in future releases. Use `columns` instead.", ): Related(column=[]) def test_related_initialization_with_columns(self, models, session): ret = Related(columns=["TestCol"]) assert len(ret.columns) == 1 assert ret.columns[0] == "TestCol" ret = Related(columns="TestCol") assert isinstance(ret.columns, list) assert len(ret.columns) == 1 assert ret.columns[0] == "TestCol" 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 == [] def tests_postgresql_array_with_args(self, Base): # regression test for #392 from sqlalchemy import Column, Integer, String from sqlalchemy.dialects.postgresql import ARRAY class ModelWithArray(Base): __tablename__ = "model_with_array" id = Column(Integer, primary_key=True) bar = Column(ARRAY(String)) field = field_for(ModelWithArray, "bar", dump_only=True) assert type(field) is fields.List assert field.dump_only is True 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) python-marshmallow-sqlalchemy-1.0.0/tests/test_sqlalchemy_schema.py000066400000000000000000000405701455623560200257700ustar00rootroot00000000000000import marshmallow import pytest import sqlalchemy as sa from marshmallow import Schema, ValidationError, validate from marshmallow_sqlalchemy import SQLAlchemyAutoSchema, SQLAlchemySchema, auto_field from marshmallow_sqlalchemy.exceptions import IncorrectSchemaTypeError from marshmallow_sqlalchemy.fields import Related # ----------------------------------------------------------------------------- @pytest.fixture def teacher(models, session): school = models.School(id=42, name="Univ. Of Whales") teacher_ = models.Teacher( id=24, full_name="Teachy McTeachFace", current_school=school ) session.add(teacher_) session.flush() return teacher_ @pytest.fixture def school(models, session): school = models.School(id=42, name="Univ. Of Whales") students = [ models.Student(id=35, full_name="Bob Smith", current_school=school), models.Student(id=53, full_name="John Johnson", current_school=school), ] session.add_all(students) session.flush() return school class EntityMixin: id = auto_field(dump_only=True) # Auto schemas with default options @pytest.fixture def sqla_auto_model_schema(models, request): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher full_name = auto_field(validate=validate.Length(max=20)) return TeacherSchema() @pytest.fixture def sqla_auto_table_schema(models, request): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: table = models.Teacher.__table__ full_name = auto_field(validate=validate.Length(max=20)) return TeacherSchema() # Schemas with relationships @pytest.fixture def sqla_schema_with_relationships(models, request): class TeacherSchema(EntityMixin, SQLAlchemySchema): class Meta: model = models.Teacher full_name = auto_field(validate=validate.Length(max=20)) current_school = auto_field() substitute = auto_field() return TeacherSchema() @pytest.fixture def sqla_auto_model_schema_with_relationships(models, request): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher include_relationships = True full_name = auto_field(validate=validate.Length(max=20)) return TeacherSchema() # Schemas with foreign keys @pytest.fixture def sqla_schema_with_fks(models, request): class TeacherSchema(EntityMixin, SQLAlchemySchema): class Meta: model = models.Teacher full_name = auto_field(validate=validate.Length(max=20)) current_school_id = auto_field() return TeacherSchema() @pytest.fixture def sqla_auto_model_schema_with_fks(models, request): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher include_fk = True include_relationships = False full_name = auto_field(validate=validate.Length(max=20)) return TeacherSchema() # ----------------------------------------------------------------------------- @pytest.mark.parametrize( "schema", ( pytest.lazy_fixture("sqla_schema_with_relationships"), pytest.lazy_fixture("sqla_auto_model_schema_with_relationships"), ), ) def test_dump_with_relationships(teacher, schema): assert schema.dump(teacher) == { "id": teacher.id, "full_name": teacher.full_name, "current_school": 42, "substitute": None, } @pytest.mark.parametrize( "schema", ( pytest.lazy_fixture("sqla_schema_with_fks"), pytest.lazy_fixture("sqla_auto_model_schema_with_fks"), ), ) def test_dump_with_foreign_keys(teacher, schema): assert schema.dump(teacher) == { "id": teacher.id, "full_name": teacher.full_name, "current_school_id": 42, } def test_table_schema_dump(teacher, sqla_auto_table_schema): assert sqla_auto_table_schema.dump(teacher) == { "id": teacher.id, "full_name": teacher.full_name, } @pytest.mark.parametrize( "schema", ( pytest.lazy_fixture("sqla_schema_with_relationships"), pytest.lazy_fixture("sqla_schema_with_fks"), pytest.lazy_fixture("sqla_auto_model_schema"), pytest.lazy_fixture("sqla_auto_table_schema"), ), ) def test_load(schema): assert schema.load({"full_name": "Teachy T"}) == {"full_name": "Teachy T"} class TestLoadInstancePerSchemaInstance: @pytest.fixture def schema_no_load_instance(self, models, session): class TeacherSchema(SQLAlchemySchema): class Meta: model = models.Teacher sqla_session = session # load_instance = False is the default full_name = auto_field(validate=validate.Length(max=20)) current_school = auto_field() substitute = auto_field() return TeacherSchema @pytest.fixture def schema_with_load_instance(self, schema_no_load_instance): class TeacherSchema(schema_no_load_instance): class Meta(schema_no_load_instance.Meta): load_instance = True return TeacherSchema @pytest.fixture def auto_schema_no_load_instance(self, models, session): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher sqla_session = session # load_instance = False is the default return TeacherSchema @pytest.fixture def auto_schema_with_load_instance(self, auto_schema_no_load_instance): class TeacherSchema(auto_schema_no_load_instance): class Meta(auto_schema_no_load_instance.Meta): load_instance = True return TeacherSchema @pytest.mark.parametrize( "Schema", ( pytest.lazy_fixture("schema_no_load_instance"), pytest.lazy_fixture("schema_with_load_instance"), pytest.lazy_fixture("auto_schema_no_load_instance"), pytest.lazy_fixture("auto_schema_with_load_instance"), ), ) def test_toggle_load_instance_per_schema(self, models, Schema): tname = "Teachy T" source = {"full_name": tname} # No per-instance override load_instance_default = Schema() result = load_instance_default.load(source) default = load_instance_default.opts.load_instance default_type = models.Teacher if default else dict assert isinstance(result, default_type) # Override the default override = Schema(load_instance=not default) result = override.load(source) override_type = dict if default else models.Teacher assert isinstance(result, override_type) @pytest.mark.parametrize( "schema", ( pytest.lazy_fixture("sqla_schema_with_relationships"), pytest.lazy_fixture("sqla_schema_with_fks"), pytest.lazy_fixture("sqla_auto_model_schema"), pytest.lazy_fixture("sqla_auto_table_schema"), ), ) def test_load_validation_errors(schema): with pytest.raises(ValidationError): schema.load({"full_name": "x" * 21}) def test_auto_field_on_plain_schema_raises_error(): class BadSchema(Schema): name = auto_field() with pytest.raises(IncorrectSchemaTypeError): BadSchema() def test_cannot_set_both_model_and_table(models): with pytest.raises(ValueError, match="Cannot set both"): class BadWidgetSchema(SQLAlchemySchema): class Meta: model = models.Teacher table = models.Teacher def test_passing_model_to_auto_field(models, teacher): class TeacherSchema(SQLAlchemySchema): current_school_id = auto_field(model=models.Teacher) schema = TeacherSchema() assert schema.dump(teacher) == {"current_school_id": teacher.current_school_id} def test_passing_table_to_auto_field(models, teacher): class TeacherSchema(SQLAlchemySchema): current_school_id = auto_field(table=models.Teacher.__table__) schema = TeacherSchema() assert schema.dump(teacher) == {"current_school_id": teacher.current_school_id} # https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/190 def test_auto_schema_skips_synonyms(models): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher include_fk = True schema = TeacherSchema() assert "current_school_id" in schema.fields assert "curr_school_id" not in schema.fields def test_auto_field_works_with_synonym(models): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher include_fk = True curr_school_id = auto_field() schema = TeacherSchema() assert "current_school_id" in schema.fields assert "curr_school_id" in schema.fields # Regresion test https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/306 def test_auto_field_works_with_ordered_flag(models): class StudentSchema(SQLAlchemyAutoSchema): class Meta: model = models.Student ordered = True full_name = auto_field() schema = StudentSchema() # Declared fields precede auto-generated fields assert tuple(schema.fields.keys()) == ( "full_name", "course_count", "id", "dob", "date_created", ) class TestAliasing: @pytest.fixture def aliased_schema(self, models): class TeacherSchema(SQLAlchemySchema): class Meta: model = models.Teacher # Generate field from "full_name", pull from "full_name" attribute, output to "name" name = auto_field("full_name") return TeacherSchema() @pytest.fixture def aliased_auto_schema(self, models): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher exclude = ("full_name",) # Generate field from "full_name", pull from "full_name" attribute, output to "name" name = auto_field("full_name") return TeacherSchema() @pytest.fixture def aliased_attribute_schema(self, models): class TeacherSchema(SQLAlchemySchema): class Meta: model = models.Teacher # Generate field from "full_name", pull from "fname" attribute, output to "name" name = auto_field("full_name", attribute="fname") return TeacherSchema() @pytest.mark.parametrize( "schema", ( pytest.lazy_fixture("aliased_schema"), pytest.lazy_fixture("aliased_auto_schema"), ), ) def test_passing_column_name(self, schema, teacher): assert schema.fields["name"].attribute == "full_name" dumped = schema.dump(teacher) assert dumped["name"] == teacher.full_name def test_passing_column_name_and_attribute(self, teacher, aliased_attribute_schema): assert aliased_attribute_schema.fields["name"].attribute == "fname" dumped = aliased_attribute_schema.dump(teacher) assert dumped["name"] == teacher.fname class TestModelInstanceDeserialization: @pytest.fixture def sqla_schema_class(self, models, session): class TeacherSchema(SQLAlchemySchema): class Meta: model = models.Teacher load_instance = True sqla_session = session full_name = auto_field(validate=validate.Length(max=20)) current_school = auto_field() substitute = auto_field() return TeacherSchema @pytest.fixture def sqla_auto_schema_class(self, models, session): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher include_relationships = True load_instance = True sqla_session = session return TeacherSchema @pytest.mark.parametrize( "SchemaClass", ( pytest.lazy_fixture("sqla_schema_class"), pytest.lazy_fixture("sqla_auto_schema_class"), ), ) def test_load(self, teacher, SchemaClass, models): schema = SchemaClass(unknown=marshmallow.INCLUDE) dump_data = schema.dump(teacher) load_data = schema.load(dump_data) assert isinstance(load_data, models.Teacher) def test_load_transient(self, models, teacher): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher load_instance = True transient = True schema = TeacherSchema() dump_data = schema.dump(teacher) load_data = schema.load(dump_data) assert isinstance(load_data, models.Teacher) state = sa.inspect(load_data) assert state.transient def test_override_transient(self, models, teacher): # marshmallow-code/marshmallow-sqlalchemy#388 class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher load_instance = True transient = True schema = TeacherSchema(transient=False) assert schema.transient is False def test_related_when_model_attribute_name_distinct_from_column_name( models, session, teacher, ): class TeacherSchema(SQLAlchemyAutoSchema): class Meta: model = models.Teacher load_instance = True sqla_session = session current_school = Related(["id", "name"]) dump_data = TeacherSchema().dump(teacher) assert "school_id" not in dump_data["current_school"] assert dump_data["current_school"]["id"] == teacher.current_school.id new_teacher = TeacherSchema().load(dump_data, transient=True) assert new_teacher.current_school.id == teacher.current_school.id assert TeacherSchema().load(dump_data) is teacher # https://github.com/marshmallow-code/marshmallow-sqlalchemy/issues/338 def test_auto_field_works_with_assoc_proxy(models): class StudentSchema(SQLAlchemySchema): class Meta: model = models.Student possible_teachers = auto_field() schema = StudentSchema() assert "possible_teachers" in schema.fields def test_dump_and_load_with_assoc_proxy_multiplicity(models, session, school): class SchoolSchema(SQLAlchemySchema): class Meta: model = models.School load_instance = True sqla_session = session student_ids = auto_field() schema = SchoolSchema() assert "student_ids" in schema.fields dump_data = schema.dump(school) assert "student_ids" in dump_data assert dump_data["student_ids"] == list(school.student_ids) new_school = schema.load(dump_data, transient=True) assert list(new_school.student_ids) == list(school.student_ids) def test_dump_and_load_with_assoc_proxy_multiplicity_dump_only_kwargs( models, session, school ): class SchoolSchema(SQLAlchemySchema): class Meta: model = models.School load_instance = True sqla_session = session student_ids = auto_field(dump_only=True, data_key="student_identifiers") schema = SchoolSchema() assert "student_ids" in schema.fields assert schema.fields["student_ids"] not in schema.load_fields.values() assert schema.fields["student_ids"] in schema.dump_fields.values() dump_data = schema.dump(school) assert "student_ids" not in dump_data assert "student_identifiers" in dump_data assert dump_data["student_identifiers"] == list(school.student_ids) with pytest.raises(ValidationError): schema.load(dump_data, transient=True) def test_dump_and_load_with_assoc_proxy_multiplicity_load_only_only_kwargs( models, session, school ): class SchoolSchema(SQLAlchemySchema): class Meta: model = models.School load_instance = True sqla_session = session student_ids = auto_field(load_only=True, data_key="student_identifiers") schema = SchoolSchema() assert "student_ids" in schema.fields assert schema.fields["student_ids"] not in schema.dump_fields.values() assert schema.fields["student_ids"] in schema.load_fields.values() dump_data = schema.dump(school) assert "student_identifers" not in dump_data new_school = schema.load( {"student_identifiers": list(school.student_ids)}, transient=True ) assert list(new_school.student_ids) == list(school.student_ids) python-marshmallow-sqlalchemy-1.0.0/tox.ini000066400000000000000000000015471455623560200210470ustar00rootroot00000000000000[tox] envlist= lint py{38,39,310,311,312}-marshmallow3 py312-marshmallowdev py38-lowest docs [testenv] extras = tests deps = marshmallow3: marshmallow>=3.10.0,<4.0.0 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz lowest: marshmallow==3.10.0 lowest: sqlalchemy==1.4.40 commands = pytest {posargs} [testenv:lint] deps = pre-commit~=3.6 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} --watch src/marshmallow_sqlalchemy --delay 2 [testenv:watch-readme] deps = restview skip_install = true commands = restview README.rst