├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGES ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ ├── eve-sidebar.png │ ├── eve_leaf.png │ ├── favicon.ico │ ├── favicon.png │ └── forkme_right_green_007200.png ├── _templates │ ├── artwork.html │ └── sidebarintro.html ├── _themes │ ├── .gitignore │ ├── LICENSE │ ├── README │ ├── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── conf.py ├── contributing.rst ├── index.rst ├── install.rst ├── trivial.rst ├── tutorial.rst └── upgrading.rst ├── eve_sqlalchemy ├── __about__.py ├── __init__.py ├── config │ ├── __init__.py │ ├── domainconfig.py │ ├── fieldconfig.py │ └── resourceconfig.py ├── examples │ ├── __init__.py │ ├── auth │ │ └── __init__.py │ ├── foreign_primary_key │ │ ├── __init__.py │ │ ├── app.py │ │ ├── domain.py │ │ └── settings.py │ ├── many_to_many │ │ ├── __init__.py │ │ ├── app.py │ │ ├── domain.py │ │ └── settings.py │ ├── many_to_one │ │ ├── __init__.py │ │ ├── app.py │ │ ├── domain.py │ │ └── settings.py │ ├── multiple_dbs │ │ ├── __init__.py │ │ ├── app.py │ │ ├── domain.py │ │ └── settings.py │ ├── one_to_many │ │ ├── __init__.py │ │ ├── app.py │ │ ├── domain.py │ │ └── settings.py │ ├── simple │ │ ├── __init__.py │ │ ├── app.py │ │ ├── example_data.py │ │ ├── settings.py │ │ └── tables.py │ └── trivial │ │ ├── __init__.py │ │ └── trivial.py ├── media.py ├── parser.py ├── structures.py ├── tests │ ├── __init__.py │ ├── config │ │ ├── __init__.py │ │ ├── domainconfig │ │ │ ├── __init__.py │ │ │ └── ambiguous_relations.py │ │ └── resourceconfig │ │ │ ├── __init__.py │ │ │ ├── association_proxy.py │ │ │ ├── column_property.py │ │ │ ├── datasource.py │ │ │ ├── foreign_primary_key.py │ │ │ ├── hybrid_property.py │ │ │ ├── id_field.py │ │ │ ├── inheritance.py │ │ │ ├── item_lookup_field.py │ │ │ ├── item_url.py │ │ │ ├── many_to_many_relationship.py │ │ │ ├── many_to_one_relationship.py │ │ │ ├── one_to_many_relationship.py │ │ │ ├── one_to_one_relationship.py │ │ │ ├── schema.py │ │ │ └── self_referential_relationship.py │ ├── delete.py │ ├── get.py │ ├── integration │ │ ├── __init__.py │ │ ├── collection_class_set.py │ │ ├── get_none_values.py │ │ ├── many_to_many.py │ │ ├── many_to_one.py │ │ ├── nested_relations.py │ │ └── one_to_many.py │ ├── patch.py │ ├── post.py │ ├── put.py │ ├── sql.py │ ├── test_settings.py │ ├── test_sql_tables.py │ ├── test_validation.py │ └── utils.py ├── utils.py └── validation.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.pydevproject 3 | /.settings 4 | .ropeproject 5 | 6 | # Eve 7 | /run.py 8 | /settings.py 9 | 10 | # Python 11 | *.py[co] 12 | 13 | # Gedit 14 | *~ 15 | 16 | # Packages 17 | *.egg 18 | *.egg-info 19 | dist 20 | build 21 | eggs 22 | parts 23 | bin 24 | var 25 | sdist 26 | develop-eggs 27 | .installed.cfg 28 | .eggs 29 | 30 | # Installer logs 31 | pip-log.txt 32 | 33 | # Unit test / coverage reports 34 | .coverage 35 | .tox 36 | 37 | #Translations 38 | *.mo 39 | 40 | #Mr Developer 41 | .mr.developer.cfg 42 | 43 | # SublimeText project files 44 | *.sublime-* 45 | 46 | # vim temp files 47 | *.swp 48 | 49 | #virtualenv 50 | Include 51 | Lib 52 | Scripts 53 | 54 | #pyenv 55 | .python-version 56 | .venv 57 | 58 | #OSX 59 | .Python 60 | .DS_Store 61 | 62 | #Sphinx 63 | _build 64 | 65 | # PyCharm 66 | .idea 67 | 68 | .cache 69 | 70 | # vscode 71 | .vscode 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | cache: pip 4 | 5 | matrix: 6 | include: 7 | - env: TOXENV=py27 8 | python: "2.7" 9 | - env: TOXENV=py34 10 | python: "3.4" 11 | - env: TOXENV=py35 12 | python: "3.5" 13 | - env: TOXENV=py36 14 | python: "3.6" 15 | - env: TOXENV=py37 16 | python: "3.7" 17 | - env: TOXENV=pypy 18 | python: "pypy2.7-6.0" 19 | - env: TOXENV=pypy3 20 | python: "pypy3.5-6.0" 21 | - env: TOXENV=flake8 22 | python: "3.7" 23 | - env: TOXENV=isort 24 | python: "3.7" 25 | - env: TOXENV=rstcheck 26 | python: "3.7" 27 | - env: TOXENV=whitespace 28 | python: "3.7" 29 | 30 | install: travis_retry pip install tox 31 | script: tox 32 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Eve-SQLAlchemy was originally written and maintained by Andrew Mleczko and 2 | is now maintained by Dominik Kellner. 3 | 4 | Development Lead 5 | ```````````````` 6 | 7 | - since 2016: Dominik Kellner 8 | - 2015 - 2016: Andrew Mleczko 9 | 10 | Patches and Contributions (in alphabetical order) 11 | ````````````````````````````````````````````````` 12 | 13 | - Alessandro De Angelis 14 | - Alex Kerney 15 | - Asif Mahmud Shimon 16 | - Bruce Frederiksen 17 | - Conrad Burchert 18 | - Cuong Manh Le 19 | - David Durieux 20 | - Dominik Kellner 21 | - Goneri Le Bouder 22 | - Jacopo Sabbatini 23 | - Kevin Roy 24 | - Leonidaz0r 25 | - Mandar Vaze 26 | - Marc Vila (@LaQuay) 27 | - Mario Kralj 28 | - Nicola Iarocci 29 | - Peter Zinng 30 | - Tomasz Jezierski (Tefnet) 31 | - fubu 32 | - Øystein S. Haaland 33 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 0.7.2 (unreleased) 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | - Nothing changed yet. 8 | 9 | 10 | 0.7.1 (2019-08-10) 11 | ~~~~~~~~~~~~~~~~~~ 12 | 13 | - Updated Tutorial to use werkzeug.security module (#196) [Mandar Vaze] 14 | - Require Flask-SQLAlchemy >= 2.4 and SQLAlchemy >= 1.3 due to security issues 15 | [Dominik Kellner] 16 | - Support filtering on embedded document fields / across relations (#186) 17 | [Dominik Kellner] 18 | - Fix sorting across relations [Dominik Kellner] 19 | - Add Python 3.7 and PyPy3 to supported (and tested) versions [Dominik Kellner] 20 | - Pin SQLAlchemy version due to warnings in Flask-SQLAlchemy [Dominik Kellner] 21 | - Improve documentation (#187, #189) [Marc Vila] 22 | 23 | 24 | 0.7.0 (2018-10-08) 25 | ~~~~~~~~~~~~~~~~~~ 26 | 27 | - Eve 0.7 support (#178) [Nicola Iarocci] 28 | 29 | 30 | 0.6.0 (2018-08-15) 31 | ~~~~~~~~~~~~~~~~~~ 32 | 33 | - Fix querying of list relations using `where` [Dominik Kellner] 34 | - Update Tutorial (#177) [Nicola Iarocci] 35 | - Return None-values again (#155) [Cuong Manh Le] 36 | - Allow to supply own Flask-SQLAlchemy driver (#86) [fubu] 37 | - Support columns with server_default (#160) [Asif Mahmud Shimon] 38 | 39 | 40 | 0.5.0 (2017-10-22) 41 | ~~~~~~~~~~~~~~~~~~ 42 | 43 | - Add DomainConfig and ResourceConfig to ease configuration (#152) 44 | [Dominik Kellner] 45 | - Fixes in documentation (#151) [Alessandro De Angelis] 46 | - Fix deprecated import warning (#142) [Cuong Manh Le] 47 | - Configure `zest.releaser` for release management (#137) 48 | [Dominik Kellner, Øystein S. Haaland] 49 | - Leverage further automated syntax and formatting checks (#138) 50 | [Dominik Kellner] 51 | - Clean up specification of dependencies [Dominik Kellner] 52 | - Added 'Contributing' section to docs (#129) [Mario Kralj] 53 | - Fix trivial app output in documentation (#131) [Michal Vlasák] 54 | - Added dialect-specific PostgreSQL JSON type (#133) [Mario Kralj] 55 | - Fix url field in documentation about additional lookup (#110) [Killian Kemps] 56 | - Compatibility with Eve 0.6.4 and refactoring of tests (#92) [Dominik Kellner] 57 | 58 | 59 | 0.4.1 (2015-12-16) 60 | ~~~~~~~~~~~~~~~~~~ 61 | 62 | - improve query with null values [amleczko] 63 | 64 | 65 | 0.4.0a3 (2015-10-20) 66 | ~~~~~~~~~~~~~~~~~~~~ 67 | 68 | - `hybrid_properties` are now readonly in Eve schema [amleczko] 69 | 70 | 71 | 0.4.0a2 (2015-09-17) 72 | ~~~~~~~~~~~~~~~~~~~~ 73 | 74 | - PUT drops/recreates item in the same transaction [goneri] 75 | 76 | 77 | 0.4.0a1 (2015-06-18) 78 | ~~~~~~~~~~~~~~~~~~~~ 79 | 80 | - support the Python-Eve generic sorting syntax [Goneri Le Bouder] 81 | - add support for `and_` and `or_` conjunctions in sqla expressions [toxsick] 82 | - embedded table: use DOMAIN to look up the resource fields [Goneri Le Bouder] 83 | 84 | 85 | 0.3.4 (2015-05-18) 86 | ~~~~~~~~~~~~~~~~~~ 87 | 88 | - fix setup.py metadata 89 | - fix how embedded documents are resolved [amleczko] 90 | 91 | 92 | 0.3.3 (2015-05-13) 93 | ~~~~~~~~~~~~~~~~~~ 94 | 95 | - added support of SA association proxy [Kevin Roy] 96 | - make sure relationships are generated properly [amleczko] 97 | 98 | 99 | 0.3.2 (2015-05-01) 100 | ~~~~~~~~~~~~~~~~~~ 101 | 102 | - add fallback on attr.op if the operator doesn't exists in the 103 | `ColumnProperty` [Kevin Roy] 104 | - add support for PostgreSQL JSON type [Goneri Le Bouder] 105 | 106 | 107 | 0.3.1 (2015-04-29) 108 | ~~~~~~~~~~~~~~~~~~ 109 | 110 | - more flexible handling sqlalchemy operators [amleczko] 111 | 112 | 113 | 0.3 (2015-04-17) 114 | ~~~~~~~~~~~~~~~~ 115 | 116 | - return everything as dicts instead of SQLAResult, remove SQLAResult 117 | [Leonidaz0r] 118 | - fix update function, this closes #22 [David Durieux] 119 | - fixed replaced method, we are compatible with Eve>=0.5.1 [Kevin Roy] 120 | - fixed jsonify function [Leonidaz0r] 121 | - update documentation [Alex Kerney] 122 | - use id_field column from the config [Goneri Le Bouder] 123 | - add flake8 in tox [Goneri Le Bouder] 124 | 125 | 126 | 0.2.1 (2015-02-25) 127 | ~~~~~~~~~~~~~~~~~~ 128 | 129 | - always wrap embedded documents [amleczko] 130 | 131 | 132 | 0.2 (2015-01-27) 133 | ~~~~~~~~~~~~~~~~ 134 | 135 | - various bugfixing [Arie Brosztein, toxsick] 136 | - refactor sorting parser, add sql order by expresssions; please check 137 | https://eve-sqlalchemy.readthedocs.org/#sqlalchemy-sorting for more details 138 | [amleczko] 139 | 140 | 141 | 0.1 (2015-01-13) 142 | ~~~~~~~~~~~~~~~~ 143 | 144 | - First public preview release. [amleczko] 145 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ################# 3 | 4 | Contributions are welcome! Not familiar with the codebase yet? No problem! 5 | There are many ways to contribute to open source projects: reporting bugs, 6 | helping with the documentation, spreading the word and of course, adding 7 | new features and patches. 8 | 9 | Getting Started 10 | --------------- 11 | #. Make sure you have a GitHub_ account. 12 | #. Open a `new issue`_, assuming one does not already exist. 13 | #. Clearly describe the issue including steps to reproduce when it is a bug. 14 | 15 | Making Changes 16 | -------------- 17 | * Fork_ the repository_ on GitHub. 18 | * Create a topic branch from where you want to base your work. 19 | * This is usually the ``master`` branch. 20 | * Please avoid working directly on the ``master`` branch. 21 | * Make commits of logical units (if needed rebase your feature branch before 22 | submitting it). 23 | * Check for unnecessary whitespace with ``git diff --check`` before committing. 24 | * Make sure your commit messages are in the `proper format`_. 25 | * If your commit fixes an open issue, reference it in the commit message (#15). 26 | * Make sure your code conforms to PEP8_ (we're using flake8_ for PEP8 and extra 27 | checks). 28 | * Make sure you have added the necessary tests for your changes. 29 | * Run all the tests to assure nothing else was accidentally broken. 30 | * Run again the entire suite via tox_ to check your changes against multiple 31 | python versions. ``pip install tox; tox`` 32 | * Don't forget to add yourself to AUTHORS_. 33 | 34 | These guidelines also apply when helping with documentation (actually, 35 | for typos and minor additions you might choose to `fork and 36 | edit`_). 37 | 38 | Submitting Changes 39 | ------------------ 40 | * Push your changes to a topic branch in your fork of the repository. 41 | * Submit a `Pull Request`_. 42 | * Wait for maintainer feedback. 43 | 44 | Keep fork in sync 45 | ----------------- 46 | The fork can be kept in sync by following the instructions `here 47 | `_. 48 | 49 | Join us on IRC 50 | -------------- 51 | If you're interested in contributing to the Eve-SQLAlchemy project or have any 52 | questions about it, come join us in Eve's #python-eve channel on 53 | irc.freenode.net. 54 | 55 | First time contributor? 56 | ----------------------- 57 | It's alright. We've all been there. See next chapter. 58 | 59 | Don't know where to start? 60 | -------------------------- 61 | There are usually several TODO comments scattered around the codebase, maybe 62 | check them out and see if you have ideas, or can help with them. Also, check 63 | the `open issues`_ in case there's something that sparks your interest. There's 64 | also a special ``contributor-friendly`` label flagging some interesting feature 65 | requests and issues that will easily get you started - even without knowing the 66 | codebase yet. If you're fluent in English (or notice any typo and/or mistake), 67 | feel free to improve the documentation. In any case, other than GitHub help_ 68 | pages, you might want to check this excellent `Effective Guide to Pull 69 | Requests`_ 70 | 71 | .. _repository: https://github.com/pyeve/eve-sqlalchemy 72 | .. _AUTHORS: https://github.com/pyeve/eve-sqlalchemy/blob/master/AUTHORS 73 | .. _`open issues`: https://github.com/pyeve/eve-sqlalchemy/issues 74 | .. _`new issue`: https://github.com/pyeve/eve-sqlalchemy/issues/new 75 | .. _GitHub: https://github.com/ 76 | .. _Fork: https://help.github.com/articles/fork-a-repo 77 | .. _`proper format`: https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 78 | .. _PEP8: https://www.python.org/dev/peps/pep-0008/ 79 | .. _flake8: https://flake8.readthedocs.io/ 80 | .. _tox: https://tox.readthedocs.org/ 81 | .. _help: https://help.github.com/ 82 | .. _`Effective Guide to Pull Requests`: https://codeinthehole.com/tips/pull-requests-and-other-good-practices-for-teams-using-github/ 83 | .. _`fork and edit`: https://github.com/blog/844-forking-with-the-edit-button 84 | .. _`Pull Request`: https://help.github.com/articles/creating-a-pull-request 85 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 by Dominik Kellner, Andrew Mleczko and contributors. 2 | See AUTHORS for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS CHANGES CONTRIBUTING.rst LICENSE 2 | include requirements.txt tox.ini 3 | graft docs 4 | prune docs/_build 5 | 6 | global-exclude __pycache__ *.pyc *.pyo 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Eve-SQLAlchemy extension 2 | ======================== 3 | 4 | .. image:: https://travis-ci.org/pyeve/eve-sqlalchemy.svg?branch=master 5 | :target: https://travis-ci.org/pyeve/eve-sqlalchemy 6 | 7 | Powered by Eve, SQLAlchemy and good intentions this extension allows 8 | to effortlessly build and deploy highly customizable, fully featured 9 | RESTful Web Services with SQL-based backends. 10 | 11 | Maintenance Notice 12 | ------------------ 13 | 14 | As of some years ago, Eve-SQLAlchemy has been effectively unmaintained. I am no 15 | longer actively using it, which makes it increasingly difficult to find the 16 | time for even small updates. If anyone is interested in taking over maintenance 17 | of Eve-SQLAlchemy, please reach out to me. 18 | 19 | Eve-SQLAlchemy is simple 20 | ------------------------ 21 | 22 | The following code blocks are excerpts of ``examples/one_to_many`` and should 23 | give you an idea of how Eve-SQLAlchemy is used. A complete working example can 24 | be found there. If you are not familiar with `Eve `_ 25 | and `SQLAlchemy `_, it is recommended to read up 26 | on them first. 27 | 28 | For this example, we declare two SQLAlchemy mappings (from ``domain.py``): 29 | 30 | .. code-block:: python 31 | 32 | class Parent(BaseModel): 33 | __tablename__ = 'parent' 34 | id = Column(Integer, primary_key=True) 35 | children = relationship("Child") 36 | 37 | class Child(BaseModel): 38 | __tablename__ = 'child' 39 | id = Column(Integer, primary_key=True) 40 | parent_id = Column(Integer, ForeignKey('parent.id')) 41 | 42 | As for Eve, a ``settings.py`` is used to configure our API. Eve-SQLAlchemy, 43 | having access to a lot of metadata from your models, can automatically generate 44 | a great deal of the `DOMAIN` dictionary for you: 45 | 46 | .. code-block:: python 47 | 48 | DEBUG = True 49 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 50 | SQLALCHEMY_TRACK_MODIFICATIONS = False 51 | RESOURCE_METHODS = ['GET', 'POST'] 52 | 53 | DOMAIN = DomainConfig({ 54 | 'parents': ResourceConfig(Parent), 55 | 'children': ResourceConfig(Child) 56 | }).render() 57 | 58 | Finally, running our application server is easy (from ``app.py``): 59 | 60 | .. code-block:: python 61 | 62 | app = Eve(validator=ValidatorSQL, data=SQL) 63 | 64 | db = app.data.driver 65 | Base.metadata.bind = db.engine 66 | db.Model = Base 67 | 68 | # create database schema on startup and populate some example data 69 | db.create_all() 70 | db.session.add_all([Parent(children=[Child() for k in range(n)]) 71 | for n in range(10)]) 72 | db.session.commit() 73 | 74 | # using reloader will destroy the in-memory sqlite db 75 | app.run(debug=True, use_reloader=False) 76 | 77 | The API is now live, ready to be consumed: 78 | 79 | .. code-block:: console 80 | 81 | $ curl -s http://localhost:5000/parents | python -m json.tool 82 | 83 | .. code-block:: json 84 | 85 | { 86 | "_items": [ 87 | { 88 | "_created": "Sun, 22 Oct 2017 07:58:28 GMT", 89 | "_etag": "f56d7cb013bf3d8449e11e8e1f0213f5efd0f07d", 90 | "_links": { 91 | "self": { 92 | "href": "parents/1", 93 | "title": "Parent" 94 | } 95 | }, 96 | "_updated": "Sun, 22 Oct 2017 07:58:28 GMT", 97 | "children": [], 98 | "id": 1 99 | }, 100 | { 101 | "_created": "Sun, 22 Oct 2017 07:58:28 GMT", 102 | "_etag": "dd1698161cb6beef04f564b2e18804d4a7c4330d", 103 | "_links": { 104 | "self": { 105 | "href": "parents/2", 106 | "title": "Parent" 107 | } 108 | }, 109 | "_updated": "Sun, 22 Oct 2017 07:58:28 GMT", 110 | "children": [ 111 | 1 112 | ], 113 | "id": 2 114 | }, 115 | "..." 116 | ], 117 | "_links": { 118 | "parent": { 119 | "href": "/", 120 | "title": "home" 121 | }, 122 | "self": { 123 | "href": "parents", 124 | "title": "parents" 125 | } 126 | }, 127 | "_meta": { 128 | "max_results": 25, 129 | "page": 1, 130 | "total": 10 131 | } 132 | } 133 | 134 | All you need to bring your API online is a database, a configuration 135 | file (defaults to ``settings.py``) and a launch script. Overall, you 136 | will find that configuring and fine-tuning your API is a very simple 137 | process. 138 | 139 | Eve-SQLAlchemy is thoroughly tested under Python 2.7-3.7 and PyPy. 140 | 141 | Documentation 142 | ------------- 143 | 144 | The offical project documentation can be accessed at 145 | `eve-sqlalchemy.readthedocs.org 146 | `_. For full working examples, 147 | especially regarding different relationship types, see the ``examples`` 148 | directory in this repository. 149 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | # This is based on the Makefile for Eve's documentation. 4 | 5 | # You can set these variables from the command line. 6 | SPHINXOPTS = 7 | SPHINXBUILD = sphinx-build 8 | PAPER = 9 | BUILDDIR = _build 10 | 11 | # Internal variables. 12 | PAPEROPT_a4 = -D latex_paper_size=a4 13 | PAPEROPT_letter = -D latex_paper_size=letter 14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 15 | # the i18n builder cannot share the environment and doctrees with the others 16 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 17 | 18 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and a HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " texinfo to make Texinfo files" 36 | @echo " info to make Texinfo files and run them through makeinfo" 37 | @echo " gettext to make PO message catalogs" 38 | @echo " changes to make an overview of all changed/added/deprecated items" 39 | @echo " linkcheck to check all external links for integrity" 40 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 41 | 42 | clean: 43 | -rm -rf $(BUILDDIR)/* 44 | 45 | html: 46 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 47 | @echo 48 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 49 | 50 | dirhtml: 51 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 52 | @echo 53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 54 | 55 | singlehtml: 56 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 57 | @echo 58 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 59 | 60 | pickle: 61 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 62 | @echo 63 | @echo "Build finished; now you can process the pickle files." 64 | 65 | json: 66 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 67 | @echo 68 | @echo "Build finished; now you can process the JSON files." 69 | 70 | htmlhelp: 71 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 72 | @echo 73 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 74 | ".hhp project file in $(BUILDDIR)/htmlhelp." 75 | 76 | qthelp: 77 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 78 | @echo 79 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 80 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 81 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Eve.qhcp" 82 | @echo "To view the help file:" 83 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Eve.qhc" 84 | 85 | devhelp: 86 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 87 | @echo 88 | @echo "Build finished." 89 | @echo "To view the help file:" 90 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Eve" 91 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Eve" 92 | @echo "# devhelp" 93 | 94 | epub: 95 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 96 | @echo 97 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 98 | 99 | latex: 100 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 101 | @echo 102 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 103 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 104 | "(use \`make latexpdf' here to do that automatically)." 105 | 106 | latexpdf: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo "Running LaTeX files through pdflatex..." 109 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 110 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 111 | 112 | text: 113 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 114 | @echo 115 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 116 | 117 | man: 118 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 119 | @echo 120 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 121 | 122 | texinfo: 123 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 124 | @echo 125 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 126 | @echo "Run \`make' in that directory to run these through makeinfo" \ 127 | "(use \`make info' here to do that automatically)." 128 | 129 | info: 130 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 131 | @echo "Running Texinfo files through makeinfo..." 132 | make -C $(BUILDDIR)/texinfo info 133 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 134 | 135 | gettext: 136 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 137 | @echo 138 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 139 | 140 | changes: 141 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 142 | @echo 143 | @echo "The overview file is in $(BUILDDIR)/changes." 144 | 145 | linkcheck: 146 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 147 | @echo 148 | @echo "Link check complete; look for any errors in the above output " \ 149 | "or in $(BUILDDIR)/linkcheck/output.txt." 150 | 151 | doctest: 152 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 153 | @echo "Testing of doctests in the sources finished, look at the " \ 154 | "results in $(BUILDDIR)/doctest/output.txt." 155 | -------------------------------------------------------------------------------- /docs/_static/eve-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/docs/_static/eve-sidebar.png -------------------------------------------------------------------------------- /docs/_static/eve_leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/docs/_static/eve_leaf.png -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/docs/_static/favicon.png -------------------------------------------------------------------------------- /docs/_static/forkme_right_green_007200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/docs/_static/forkme_right_green_007200.png -------------------------------------------------------------------------------- /docs/_templates/artwork.html: -------------------------------------------------------------------------------- 1 |

2 | Artwork by Kalamun © 2013 3 |

4 | -------------------------------------------------------------------------------- /docs/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

Other Projects

2 | 3 | 13 | 14 |

Useful Links

15 | 23 | -------------------------------------------------------------------------------- /docs/_themes/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /docs/_themes/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the theme, with or 6 | without modification, are permitted provided that the following conditions 7 | are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | We kindly ask you to only use these themes in an unmodified manner just 22 | for Flask and Flask-related products, not for unrelated projects. If you 23 | like the visual style and want to use it for your own projects, please 24 | consider making some larger changes to the themes (such as changing 25 | font faces, sizes, colors or margins). 26 | 27 | THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 28 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 29 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 30 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 31 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 32 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 33 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 34 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 35 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 36 | ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE 37 | POSSIBILITY OF SUCH DAMAGE. 38 | -------------------------------------------------------------------------------- /docs/_themes/README: -------------------------------------------------------------------------------- 1 | Flask Sphinx Styles 2 | =================== 3 | 4 | This repository contains sphinx styles for Flask and Flask related 5 | projects. To use this style in your Sphinx documentation, follow 6 | this guide: 7 | 8 | 1. put this folder as _themes into your docs folder. Alternatively 9 | you can also use git submodules to check out the contents there. 10 | 2. add this to your conf.py: 11 | 12 | sys.path.append(os.path.abspath('_themes')) 13 | html_theme_path = ['_themes'] 14 | html_theme = 'flask' 15 | 16 | The following themes exist: 17 | 18 | - 'flask' - the standard flask documentation theme for large 19 | projects 20 | - 'flask_small' - small one-page theme. Intended to be used by 21 | very small addon libraries for flask. 22 | 23 | The following options exist for the flask_small theme: 24 | 25 | [options] 26 | index_logo = '' filename of a picture in _static 27 | to be used as replacement for the 28 | h1 in the index.rst file. 29 | index_logo_height = 120px height of the index logo 30 | github_fork = '' repository name on github for the 31 | "fork me" badge 32 | -------------------------------------------------------------------------------- /docs/_themes/flask/layout.html: -------------------------------------------------------------------------------- 1 | {%- extends "basic/layout.html" %} 2 | {%- block extrahead %} 3 | {{ super() }} 4 | {% if theme_touch_icon %} 5 | 6 | {% endif %} 7 | 8 | {% endblock %} 9 | {%- block relbar2 %}{% endblock %} 10 | {% block header %} 11 | {{ super() }} 12 | {% if pagename == 'index' %} 13 |
14 | {% endif %} 15 | {% endblock %} 16 | {%- block footer %} 17 | 20 | 21 | Fork me on GitHub 22 | 23 | {% if pagename == 'index' %} 24 |
25 | {% endif %} 26 | {%- endblock %} 27 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = 8 | index_logo_height = 120px 9 | touch_icon = 10 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import Keyword, Name, Comment, String, Error, \ 4 | Number, Operator, Generic, Whitespace, Punctuation, Other, Literal 5 | 6 | 7 | class FlaskyStyle(Style): 8 | background_color = "#f8f8f8" 9 | default_style = "" 10 | 11 | styles = { 12 | # No corresponding class for the following: 13 | #Text: "", # class: '' 14 | Whitespace: "underline #f8f8f8", # class: 'w' 15 | Error: "#a40000 border:#ef2929", # class: 'err' 16 | Other: "#000000", # class 'x' 17 | 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | 21 | Keyword: "bold #004461", # class: 'k' 22 | Keyword.Constant: "bold #004461", # class: 'kc' 23 | Keyword.Declaration: "bold #004461", # class: 'kd' 24 | Keyword.Namespace: "bold #004461", # class: 'kn' 25 | Keyword.Pseudo: "bold #004461", # class: 'kp' 26 | Keyword.Reserved: "bold #004461", # class: 'kr' 27 | Keyword.Type: "bold #004461", # class: 'kt' 28 | 29 | Operator: "#582800", # class: 'o' 30 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 31 | 32 | Punctuation: "bold #000000", # class: 'p' 33 | 34 | # because special names such as Name.Class, Name.Function, etc. 35 | # are not recognized as such later in the parsing, we choose them 36 | # to look the same as ordinary variables. 37 | Name: "#000000", # class: 'n' 38 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 39 | Name.Builtin: "#004461", # class: 'nb' 40 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 41 | Name.Class: "#000000", # class: 'nc' - to be revised 42 | Name.Constant: "#000000", # class: 'no' - to be revised 43 | Name.Decorator: "#888", # class: 'nd' - to be revised 44 | Name.Entity: "#ce5c00", # class: 'ni' 45 | Name.Exception: "bold #cc0000", # class: 'ne' 46 | Name.Function: "#000000", # class: 'nf' 47 | Name.Property: "#000000", # class: 'py' 48 | Name.Label: "#f57900", # class: 'nl' 49 | Name.Namespace: "#000000", # class: 'nn' - to be revised 50 | Name.Other: "#000000", # class: 'nx' 51 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 52 | Name.Variable: "#000000", # class: 'nv' - to be revised 53 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 54 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 55 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 56 | 57 | Number: "#990000", # class: 'm' 58 | 59 | Literal: "#000000", # class: 'l' 60 | Literal.Date: "#000000", # class: 'ld' 61 | 62 | String: "#4e9a06", # class: 's' 63 | String.Backtick: "#4e9a06", # class: 'sb' 64 | String.Char: "#4e9a06", # class: 'sc' 65 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 66 | String.Double: "#4e9a06", # class: 's2' 67 | String.Escape: "#4e9a06", # class: 'se' 68 | String.Heredoc: "#4e9a06", # class: 'sh' 69 | String.Interpol: "#4e9a06", # class: 'si' 70 | String.Other: "#4e9a06", # class: 'sx' 71 | String.Regex: "#4e9a06", # class: 'sr' 72 | String.Single: "#4e9a06", # class: 's1' 73 | String.Symbol: "#4e9a06", # class: 'ss' 74 | 75 | Generic: "#000000", # class: 'g' 76 | Generic.Deleted: "#a40000", # class: 'gd' 77 | Generic.Emph: "italic #000000", # class: 'ge' 78 | Generic.Error: "#ef2929", # class: 'gr' 79 | Generic.Heading: "bold #000080", # class: 'gh' 80 | Generic.Inserted: "#00A000", # class: 'gi' 81 | Generic.Output: "#888", # class: 'go' 82 | Generic.Prompt: "#745334", # class: 'gp' 83 | Generic.Strong: "bold #000000", # class: 'gs' 84 | Generic.Subheading: "bold #800080", # class: 'gu' 85 | Generic.Traceback: "bold #a40000", # class: 'gt' 86 | } 87 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Eve-SQLAlchemy 2 | ============== 3 | 4 | Use `Eve`_ with `SQLAlchemy`_ instead of MongoDB. Re-use your existing SQL data 5 | model and expose it via a RESTful Web Service with no hassle. 6 | 7 | Documentation 8 | ------------- 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | install 13 | tutorial 14 | trivial 15 | upgrading 16 | contributing 17 | 18 | .. include:: ../CHANGES 19 | 20 | .. _Eve: https://python-eve.org 21 | .. _SQLAlchemy: https://www.sqlalchemy.org 22 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | This part of the documentation covers the installation of Eve-SQLAlchemy. The 5 | first step to using any software package is getting it properly installed. 6 | 7 | Installing Eve-SQLAlchemy is simple with `pip 8 | `_: 9 | 10 | .. code-block:: console 11 | 12 | $ pip install eve-sqlalchemy 13 | 14 | 15 | Development Version 16 | ------------------- 17 | 18 | Eve-SQLAlchemy is actively developed on GitHub, where the code is `always 19 | available `_. If you want to work with 20 | the development version of Eve-SQLAlchemy, there are two ways: you can either 21 | let `pip` pull in the development version, or you can tell it to operate on a 22 | git checkout. Either way, virtualenv is recommended. 23 | 24 | Get the git checkout in a new virtualenv and run in development mode. 25 | 26 | .. code-block:: console 27 | 28 | $ git clone https://github.com/pyeve/eve-sqlalchemy.git 29 | Cloning into 'eve-sqlalchemy'... 30 | ... 31 | 32 | $ cd eve-sqlalchemy 33 | $ virtualenv venv 34 | ... 35 | Installing setuptools, pip, wheel... 36 | done. 37 | 38 | $ . venv/bin/activate 39 | $ pip install . 40 | ... 41 | Successfully installed ... 42 | 43 | This will pull in the dependencies and activate the git head as the current 44 | version inside the virtualenv. Then all you have to do is run ``git pull 45 | origin`` to update to the latest version. 46 | 47 | To just get the development version without git, do this instead: 48 | 49 | .. code-block:: console 50 | 51 | $ mkdir eve-sqlalchemy 52 | $ cd eve-sqlalchemy 53 | $ virtualenv venv 54 | $ . venv/bin/activate 55 | $ pip install git+https://github.com/pyeve/eve-sqlalchemy.git 56 | ... 57 | Successfully installed ... 58 | 59 | And you're done! 60 | -------------------------------------------------------------------------------- /docs/trivial.rst: -------------------------------------------------------------------------------- 1 | Simple example 2 | ============== 3 | 4 | Create a file, called trivial.py, and include the following: 5 | 6 | .. literalinclude:: ../eve_sqlalchemy/examples/trivial/trivial.py 7 | 8 | Run this command to start the server: 9 | 10 | .. code-block:: console 11 | 12 | python trivial.py 13 | 14 | Open the following in your browser to confirm that the server is serving: 15 | 16 | .. code-block:: console 17 | 18 | http://127.0.0.1:5000/ 19 | 20 | You will see something like this: 21 | 22 | .. code-block:: xml 23 | 24 | 25 | 26 | 27 | 28 | Now try the people URL: 29 | 30 | .. code-block:: console 31 | 32 | http://127.0.0.1:5000/people 33 | 34 | You will see the three records we preloaded. 35 | 36 | .. code-block:: xml 37 | 38 | 39 | 40 | <_meta> 41 | 25 42 | 1 43 | 3 44 | 45 | <_updated>Sun, 22 Feb 2015 16:28:00 GMT 46 | George 47 | George Washington 48 | 1 49 | Washington 50 | 51 | -------------------------------------------------------------------------------- /docs/upgrading.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Upgrading 3 | ========= 4 | 5 | Upgrading from 0.6.0 to 0.7.0 6 | ============================= 7 | 8 | Eve-SQLAlchemy is now based on Eve 0.7, which introduces potentially breaking 9 | changes: 10 | 11 | - The ETag format was changed to comply with RFC 7232-2.3. Be aware the ETag 12 | header values are now enclosed with double-quotes. 13 | - Eve now returns a `428 Precondition Required` instead of a generic `403 14 | Forbidden` when the `If-Match` request header is missing. 15 | 16 | For a comprehensive list of changes refer to the `official changelog`_. 17 | 18 | .. _official changelog: https://docs.python-eve.org/changelog.html#version-0-7 19 | 20 | Upgrading from 0.5.0 to 0.6.0 21 | ============================= 22 | 23 | There is one potentially breaking change in 0.6.0: Due to a regression 0.5.0 24 | did not return `None`/`null` values anymore (as Eve does and 0.4.1 did). That 25 | means your API might return slightly different responses after upgrading to 26 | 0.6.0 than it did before. If it's really a breaking change for you depends on 27 | your API specification and your clients. 28 | 29 | Upgrading from 0.4.1 to 0.5.0 30 | ============================= 31 | 32 | There are two breaking changes in 0.5.0: 33 | 34 | 1. Eve-SQLAlchemy now handles related IDs and embedded objects with just one 35 | field in the payload, just as Eve does. This will most likely affect your 36 | consumers, too! 37 | 2. We introduced a new way to register your SQLAlchemy models with Eve. So far 38 | there is no backward compatible wrapper for the former ``registerSchema`` 39 | decorator. 40 | 41 | Let's look at the needed changes in more detail. To illustrate both changes, we 42 | will look at the following models (the full code is in the `examples` 43 | directory): 44 | 45 | .. code-block:: python 46 | 47 | class People(CommonColumns): 48 | __tablename__ = 'people' 49 | id = Column(Integer, primary_key=True, autoincrement=True) 50 | firstname = Column(String(80)) 51 | lastname = Column(String(120)) 52 | fullname = column_property(firstname + " " + lastname) 53 | 54 | 55 | class Invoices(CommonColumns): 56 | __tablename__ = 'invoices' 57 | id = Column(Integer, primary_key=True, autoincrement=True) 58 | number = Column(Integer) 59 | people_id = Column(Integer, ForeignKey('people.id')) 60 | people = relationship(People, uselist=False) 61 | 62 | 1. Related IDs and embedding 63 | ---------------------------- 64 | 65 | Getting an invoice in 0.4.1 will return the `people_id` in the payload: 66 | 67 | .. code-block:: json 68 | 69 | { 70 | "_created": "Sat, 15 Jul 2017 02:24:58 GMT", 71 | "_etag": null, 72 | "_id": 1, 73 | "_updated": "Sat, 15 Jul 2017 02:24:58 GMT", 74 | "id": 1, 75 | "number": 42, 76 | "people_id": 1 77 | } 78 | 79 | And, if you embed the related ``People`` object, you will get: 80 | 81 | .. code-block:: json 82 | 83 | { 84 | "_created": "Sat, 15 Jul 2017 02:39:25 GMT", 85 | "_etag": null, 86 | "_id": 1, 87 | "_updated": "Sat, 15 Jul 2017 02:39:25 GMT", 88 | "id": 1, 89 | "number": 42, 90 | "people": { 91 | "_created": "Sat, 15 Jul 2017 02:39:25 GMT", 92 | "_etag": null, 93 | "_id": 1, 94 | "_updated": "Sat, 15 Jul 2017 02:39:25 GMT", 95 | "firstname": "George", 96 | "fullname": "George Washington", 97 | "id": 1, 98 | "lastname": "Washington" 99 | }, 100 | "people_id": 1 101 | } 102 | 103 | But this was actually not how Eve itself is handling this. In order to follow 104 | the APIs generated by Eve more closely, we decided to adopt the way Eve is 105 | doing embedding and use the same field for both the related ID and the embedded 106 | document. Which means starting in 0.5.0, the first response looks like this: 107 | 108 | .. code-block:: json 109 | 110 | { 111 | "_created": "Sat, 15 Jul 2017 02:52:20 GMT", 112 | "_etag": "26abc30d70f57de186d9f99a7192444fcf538519", 113 | "_updated": "Sat, 15 Jul 2017 02:52:20 GMT", 114 | "id": 1, 115 | "number": 42, 116 | "people": 1 117 | } 118 | 119 | And the second one (with embedding): 120 | 121 | .. code-block:: json 122 | 123 | { 124 | "_created": "Sat, 15 Jul 2017 02:54:44 GMT", 125 | "_etag": "8a1121cacb77a21f9ff3b5a85cfba0a501a538ea", 126 | "_updated": "Sat, 15 Jul 2017 02:54:44 GMT", 127 | "id": 1, 128 | "number": 42, 129 | "people": { 130 | "_created": "Sat, 15 Jul 2017 02:54:44 GMT", 131 | "_updated": "Sat, 15 Jul 2017 02:54:44 GMT", 132 | "firstname": "George", 133 | "fullname": "George Washington", 134 | "id": 1, 135 | "lastname": "Washington" 136 | } 137 | } 138 | 139 | 2. Registering of SQLAlchemy models 140 | ----------------------------------- 141 | 142 | In 0.4.1, you were most likely doing something along the following lines in 143 | your `settings.py`: 144 | 145 | .. code-block:: python 146 | 147 | ID_FIELD = 'id' 148 | config.ID_FIELD = ID_FIELD 149 | 150 | registerSchema('people')(People) 151 | registerSchema('invoices')(Invoices) 152 | 153 | DOMAIN = { 154 | 'people': People._eve_schema['people'], 155 | 'invoices': Invoices._eve_schema['invoices'] 156 | } 157 | 158 | There are good news: manually (and globally) setting ``ID_FIELD``, including 159 | the workaround of setting ``config.ID_FIELD``, is not required anymore. The 160 | same applies to ``ITEM_LOOKUP_FIELD`` and ``ITEM_URL``. While you can still 161 | override them, they are now preconfigured at the resource level depending on 162 | your models' primary keys. 163 | 164 | The required configuration for the models above simplifies to: 165 | 166 | .. code-block:: python 167 | 168 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 169 | 170 | DOMAIN = DomainConfig({ 171 | 'people': ResourceConfig(People), 172 | 'invoices': ResourceConfig(Invoices) 173 | }).render() 174 | 175 | *Note:* If you've modified ``DATE_CREATED``, ``LAST_UPDATED`` or ``ETAG``, you 176 | have to pass their value to ``DomainConfig.render()``. They are needed during 177 | rendering the final ``DOMAIN`` configuration. 178 | 179 | .. code-block:: python 180 | 181 | DomainConfig(domain_dict).render(date_created=DATE_CREATED, 182 | last_updated=LAST_UPDATED, 183 | etag=ETAG) 184 | -------------------------------------------------------------------------------- /eve_sqlalchemy/__about__.py: -------------------------------------------------------------------------------- 1 | __title__ = 'Eve-SQLAlchemy' 2 | __summary__ = 'REST API framework powered by Eve, SQLAlchemy and good ' \ 3 | 'intentions.' 4 | __url__ = 'https://github.com/pyeve/eve-sqlalchemy' 5 | 6 | __version__ = '0.7.2.dev0' 7 | 8 | __author__ = 'Dominik Kellner' 9 | __email__ = 'dkellner@dkellner.de' 10 | 11 | __license__ = 'BSD' 12 | __copyright__ = '2017 %s' % __author__ 13 | -------------------------------------------------------------------------------- /eve_sqlalchemy/config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .domainconfig import DomainConfig # noqa 4 | from .resourceconfig import ResourceConfig # noqa 5 | -------------------------------------------------------------------------------- /eve_sqlalchemy/config/domainconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from eve.utils import config 5 | 6 | 7 | class DomainConfig(object): 8 | """Create an Eve `DOMAIN` dict out of :class:`ResourceConfig`s. 9 | 10 | Upon rendering the `DOMAIN` dictionary, we will first collect all given 11 | :class:`ResourceConfig` objects and pass them as `related_resource_configs` 12 | to their `render` methods. This way each :class:`ResourceConfig` knows 13 | about all existent endpoints in `DOMAIN` and can properly set up relations. 14 | 15 | A special case occurs if one model is referenced for more than one 16 | endpoint, e.g.: 17 | 18 | DomainConfig({ 19 | 'users': ResourceConfig(User), 20 | 'admins': ResourceConfig(User), 21 | 'groups': ResourceConfig(Group) 22 | }) 23 | 24 | Here, we cannot reliably determine which resource should be used for 25 | relations to `User`. In this case you have to specify the target resource 26 | for all such relations: 27 | 28 | DomainConfig({ 29 | 'users': ResourceConfig(User), 30 | 'admins': ResourceConfig(User), 31 | 'groups': ResourceConfig(Group) 32 | }, related_resources={ 33 | (Group, 'members'): 'users', 34 | (Group, 'admins'): 'admins' 35 | }) 36 | 37 | """ 38 | 39 | def __init__(self, resource_configs, related_resources={}): 40 | """Initializes the :class:`DomainConfig` object. 41 | 42 | :param resource_configs: mapping of endpoint names to 43 | :class:`ResourceConfig` objects 44 | :param related_resources: mapping of (model, field name) to a resource 45 | """ 46 | self.resource_configs = resource_configs 47 | self.related_resources = related_resources 48 | 49 | def render(self, date_created=config.DATE_CREATED, 50 | last_updated=config.LAST_UPDATED, etag=config.ETAG): 51 | """Renders the Eve `DOMAIN` dictionary. 52 | 53 | If you change any of `DATE_CREATED`, `LAST_UPDATED` or `ETAG`, make 54 | sure you pass your new value. 55 | 56 | :param date_created: value of `DATE_CREATED` 57 | :param last_updated: value of `LAST_UPDATED` 58 | :param etag: value of `ETAG` 59 | """ 60 | domain_def = {} 61 | related_resource_configs = self._create_related_resource_configs() 62 | for endpoint, resource_config in self.resource_configs.items(): 63 | domain_def[endpoint] = resource_config.render( 64 | date_created, last_updated, etag, related_resource_configs) 65 | return domain_def 66 | 67 | def _create_related_resource_configs(self): 68 | """Creates a mapping from model to (resource, :class:`ResourceConfig`). 69 | 70 | This mapping will be passed to all :class:`ResourceConfig` objects' 71 | `render` methods. 72 | 73 | If there is more than one resource using the same model, relations for 74 | this model cannot be set up automatically. In this case you will have 75 | to manually set `related_resources` when creating the 76 | :class:`DomainConfig` object. 77 | """ 78 | result = {} 79 | keys_to_remove = set() 80 | for resource, resource_config in self.resource_configs.items(): 81 | model = resource_config.model 82 | if model in result: 83 | keys_to_remove.add(model) 84 | result[model] = (resource, resource_config) 85 | for key in keys_to_remove: 86 | del result[key] 87 | for field, resource in self.related_resources.items(): 88 | result[field] = (resource, self.resource_configs[resource]) 89 | return result 90 | -------------------------------------------------------------------------------- /eve_sqlalchemy/config/fieldconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import sqlalchemy.dialects.postgresql as postgresql 5 | from eve.exceptions import ConfigException 6 | from sqlalchemy import types 7 | from sqlalchemy.ext.declarative.api import DeclarativeMeta 8 | 9 | 10 | class FieldConfig(object): 11 | 12 | def __init__(self, name, model, mapper): 13 | self._name = name 14 | self._model = model 15 | self._mapper = mapper 16 | self._field = getattr(model, name) 17 | 18 | def render(self, related_resource_configs): 19 | self._related_resource_configs = related_resource_configs 20 | return self._render() 21 | 22 | def _get_field_type(self, sqla_column): 23 | sqla_type_mapping = { 24 | postgresql.JSON: 'json', 25 | types.Boolean: 'boolean', 26 | types.DATETIME: 'datetime', 27 | types.Date: 'datetime', 28 | types.DateTime: 'datetime', 29 | types.Float: 'float', 30 | types.Integer: 'integer', 31 | types.JSON: 'json', 32 | types.PickleType: None, 33 | } 34 | for sqla_type, field_type in sqla_type_mapping.items(): 35 | if isinstance(sqla_column.type, sqla_type): 36 | return field_type 37 | return 'string' 38 | 39 | 40 | class ColumnFieldConfig(FieldConfig): 41 | 42 | def __init__(self, *args, **kwargs): 43 | super(ColumnFieldConfig, self).__init__(*args, **kwargs) 44 | self._sqla_column = self._field.expression 45 | 46 | def _render(self): 47 | return {k: v for k, v in { 48 | 'type': self._get_field_type(self._sqla_column), 49 | 'nullable': self._get_field_nullable(), 50 | 'required': self._get_field_required(), 51 | 'unique': self._get_field_unique(), 52 | 'maxlength': self._get_field_maxlength(), 53 | 'default': self._get_field_default(), 54 | }.items() if v is not None} 55 | 56 | def _get_field_nullable(self): 57 | return getattr(self._sqla_column, 'nullable', True) 58 | 59 | def _has_server_default(self): 60 | return bool(getattr(self._sqla_column, 'server_default')) 61 | 62 | def _get_field_required(self): 63 | autoincrement = (self._sqla_column.primary_key 64 | and self._sqla_column.autoincrement 65 | and isinstance(self._sqla_column.type, types.Integer)) 66 | return not (self._get_field_nullable() 67 | or autoincrement 68 | or self._has_server_default()) 69 | 70 | def _get_field_unique(self): 71 | return getattr(self._sqla_column, 'unique', None) 72 | 73 | def _get_field_maxlength(self): 74 | try: 75 | return self._sqla_column.type.length 76 | except AttributeError: 77 | return None 78 | 79 | def _get_field_default(self): 80 | try: 81 | return self._sqla_column.default.arg 82 | except AttributeError: 83 | return None 84 | 85 | 86 | class RelationshipFieldConfig(FieldConfig): 87 | 88 | def __init__(self, *args, **kwargs): 89 | super(RelationshipFieldConfig, self).__init__(*args, **kwargs) 90 | self._relationship = self._mapper.relationships[self._name] 91 | 92 | def _render(self): 93 | if self._relationship.uselist: 94 | if self._relationship.collection_class == set: 95 | return { 96 | 'type': 'set', 97 | 'coerce': set, 98 | 'schema': self._get_foreign_key_definition() 99 | } 100 | else: 101 | return { 102 | 'type': 'list', 103 | 'schema': self._get_foreign_key_definition() 104 | } 105 | else: 106 | field_def = self._get_foreign_key_definition() 107 | # This is a workaround to support PUT with integer ids. 108 | # TODO: Investigate this and fix it properly. 109 | if field_def['type'] == 'integer': 110 | field_def['coerce'] = int 111 | return field_def 112 | 113 | def _get_foreign_key_definition(self): 114 | resource, resource_config = self._get_resource() 115 | if len(self.local_foreign_keys) > 0: 116 | # TODO: does this make sense? 117 | remote_column = tuple(self._relationship.remote_side)[0] 118 | local_column = tuple(self.local_foreign_keys)[0] 119 | else: 120 | # TODO: Would item_lookup_field make sense here, too? 121 | remote_column = getattr(resource_config.model, 122 | resource_config.id_field) 123 | local_column = None 124 | field_def = { 125 | 'data_relation': { 126 | 'resource': resource, 127 | 'field': remote_column.key 128 | }, 129 | 'type': self._get_field_type(remote_column), 130 | 'nullable': True 131 | } 132 | if local_column is not None: 133 | field_def['local_id_field'] = local_column.key 134 | if not getattr(local_column, 'nullable', True): 135 | field_def['required'] = True 136 | field_def['nullable'] = False 137 | if getattr(local_column, 'unique') or \ 138 | getattr(local_column, 'primary_key'): 139 | field_def['unique'] = True 140 | return field_def 141 | 142 | def _get_resource(self): 143 | try: 144 | return self._related_resource_configs[(self._model, self._name)] 145 | except LookupError: 146 | try: 147 | arg = self._relationship.argument 148 | if isinstance(arg, DeclarativeMeta): 149 | return self._related_resource_configs[arg] 150 | elif callable(arg): 151 | return self._related_resource_configs[arg()] 152 | else: 153 | return self._related_resource_configs[arg.class_] 154 | except LookupError: 155 | raise ConfigException( 156 | 'Cannot determine related resource for {model}.{field}. ' 157 | 'Please specify `related_resources` manually.' 158 | .format(model=self._model.__name__, field=self._name)) 159 | 160 | @property 161 | def local_foreign_keys(self): 162 | return set(c for c in self._relationship.local_columns 163 | if len(c.expression.foreign_keys) > 0) 164 | 165 | 166 | class AssociationProxyFieldConfig(FieldConfig): 167 | 168 | def _render(self): 169 | resource, resource_config = self._get_resource() 170 | remote_column = getattr(resource_config.model, 171 | self._field.value_attr) 172 | remote_column_type = self._get_field_type(remote_column) 173 | return { 174 | 'type': 'list', 175 | 'schema': { 176 | 'type': remote_column_type, 177 | 'data_relation': { 178 | 'resource': resource, 179 | 'field': remote_column.key 180 | } 181 | } 182 | } 183 | 184 | def _get_resource(self): 185 | try: 186 | return self._related_resource_configs[(self._model, self._name)] 187 | except LookupError: 188 | try: 189 | relationship = self._mapper.relationships[ 190 | self._field.target_collection] 191 | return self._related_resource_configs[relationship.argument()] 192 | except LookupError: 193 | model = self._mapper.class_ 194 | raise ConfigException( 195 | 'Cannot determine related resource for {model}.{field}. ' 196 | 'Please specify `related_resources` manually.' 197 | .format(model=model.__name__, 198 | field=self._name)) 199 | 200 | @property 201 | def proxied_relationship(self): 202 | return self._field.target_collection 203 | 204 | 205 | class ColumnPropertyFieldConfig(FieldConfig): 206 | 207 | def _render(self): 208 | return { 209 | 'type': self._get_field_type(self._field.expression), 210 | 'readonly': True, 211 | } 212 | 213 | 214 | class HybridPropertyFieldConfig(FieldConfig): 215 | 216 | def _render(self): 217 | # TODO: For now all hybrid properties will be returned as strings. 218 | # Investigate and see if we actually can do better than this. 219 | return { 220 | 'type': 'string', 221 | 'readonly': True, 222 | } 223 | -------------------------------------------------------------------------------- /eve_sqlalchemy/config/resourceconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from eve.exceptions import ConfigException 5 | from sqlalchemy import types 6 | from sqlalchemy.ext.associationproxy import ASSOCIATION_PROXY 7 | from sqlalchemy.ext.hybrid import HYBRID_PROPERTY 8 | from sqlalchemy.sql import expression 9 | 10 | from eve_sqlalchemy.utils import merge_dicts 11 | 12 | from .fieldconfig import ( 13 | AssociationProxyFieldConfig, ColumnFieldConfig, ColumnPropertyFieldConfig, 14 | HybridPropertyFieldConfig, RelationshipFieldConfig, 15 | ) 16 | 17 | 18 | class ResourceConfig(object): 19 | """Create an Eve resource dict out of an SQLAlchemy model. 20 | 21 | In most cases, we can deduce all required information by inspecting the 22 | model. This includes setting `id_field`, `item_lookup_field` and `item_url` 23 | at the resource level. 24 | """ 25 | 26 | def __init__(self, model, id_field=None, item_lookup_field=None): 27 | """Initializes the :class:`ResourceConfig` object. 28 | 29 | If you want to customize `id_field` or `item_lookup_field`, pass them 30 | to this function instead of altering the configuration at a later 31 | point. Other settings like `item_url` depend on them! 32 | 33 | :param id_field: overwrite resource-level `id_field` setting 34 | :param item_lookup_field: overwrite resource-level `item_lookup_field` 35 | setting 36 | """ 37 | self.model = model 38 | self._mapper = self.model.__mapper__ # just for convenience 39 | self.id_field = id_field or self._deduce_id_field() 40 | self.item_lookup_field = item_lookup_field or self.id_field 41 | 42 | def render(self, date_created, last_updated, etag, 43 | related_resource_configs={}): 44 | """Renders the Eve resource configuration. 45 | 46 | :param date_created: value of `DATE_CREATED` 47 | :param last_updated: value of `LAST_UPDATED` 48 | :param etag: value of `ETAG` 49 | :param related_resource_configs: Mapping of SQLAlchemy models or tuples 50 | of model + field name to a tuple of endpoint name and 51 | :class:`ResourceConfig` object. This is needed to properly set up 52 | the relationship configuration expected by Eve. 53 | """ 54 | self._ignored_fields = set( 55 | [f for f in self.model.__dict__ if f[0] == '_'] + 56 | [date_created, last_updated, etag]) - \ 57 | set([self.id_field, self.item_lookup_field]) 58 | field_configs = self._create_field_configs() 59 | return { 60 | 'id_field': self.id_field, 61 | 'item_lookup_field': self.item_lookup_field, 62 | 'item_url': self.item_url, 63 | 'schema': self._render_schema(field_configs, 64 | related_resource_configs), 65 | 'datasource': self._render_datasource(field_configs, etag), 66 | } 67 | 68 | @property 69 | def id_field(self): 70 | return self._id_field 71 | 72 | @id_field.setter 73 | def id_field(self, id_field): 74 | pk_columns = [c.name for c in self._mapper.primary_key] 75 | if not (len(pk_columns) == 1 and pk_columns[0] == id_field): 76 | column = self._get_column(id_field) 77 | if not column.unique: 78 | raise ConfigException( 79 | "{model}.{id_field} is not unique." 80 | .format(model=self.model.__name__, id_field=id_field)) 81 | self._id_field = id_field 82 | 83 | def _deduce_id_field(self): 84 | pk_columns = [c.name for c in self.model.__mapper__.primary_key] 85 | if len(pk_columns) == 1: 86 | return pk_columns[0] 87 | else: 88 | raise ConfigException( 89 | "{model}'s primary key consists of zero or multiple columns, " 90 | "thus we cannot deduce which one to use. Please manually " 91 | "specify a unique column to use as `id_field`: " 92 | "`ResourceConfig({model}, id_field=...)`" 93 | .format(model=self.model.__name__)) 94 | 95 | @property 96 | def item_lookup_field(self): 97 | return self._item_lookup_field 98 | 99 | @item_lookup_field.setter 100 | def item_lookup_field(self, item_lookup_field): 101 | if item_lookup_field != self.id_field: 102 | column = self._get_column(item_lookup_field) 103 | if not column.unique: 104 | raise ConfigException( 105 | "{model}.{item_lookup_field} is not unique." 106 | .format(model=self.model.__name__, 107 | item_lookup_field=item_lookup_field)) 108 | self._item_lookup_field = item_lookup_field 109 | 110 | @property 111 | def item_url(self): 112 | column = self._get_column(self.item_lookup_field) 113 | if isinstance(column.type, types.Integer): 114 | return 'regex("[0-9]+")' 115 | else: 116 | return 'regex("[a-zA-Z0-9_-]+")' 117 | 118 | def _get_column(self, column_name): 119 | try: 120 | return self._mapper.columns[column_name] 121 | except KeyError: 122 | raise ConfigException("{model}.{column_name} does not exist." 123 | .format(model=self.model.__name__, 124 | column_name=column_name)) 125 | 126 | def _create_field_configs(self): 127 | association_proxies = { 128 | k: AssociationProxyFieldConfig(k, self.model, self._mapper) 129 | for k in self._get_association_proxy_fields()} 130 | proxied_relationships = \ 131 | set([p.proxied_relationship for p in association_proxies.values()]) 132 | relationships = { 133 | k: RelationshipFieldConfig(k, self.model, self._mapper) 134 | for k in self._get_relationship_fields(proxied_relationships)} 135 | columns = { 136 | k: ColumnFieldConfig(k, self.model, self._mapper) 137 | for k in self._get_column_fields()} 138 | column_properties = { 139 | k: ColumnPropertyFieldConfig(k, self.model, self._mapper) 140 | for k in self._get_column_property_fields()} 141 | hybrid_properties = { 142 | k: HybridPropertyFieldConfig(k, self.model, self._mapper) 143 | for k in self._get_hybrid_property_fields()} 144 | return merge_dicts(association_proxies, relationships, columns, 145 | column_properties, hybrid_properties) 146 | 147 | def _get_association_proxy_fields(self): 148 | return [k for k, v in self.model.__dict__.items() 149 | if k not in self._ignored_fields 150 | and getattr(v, 'extension_type', None) == ASSOCIATION_PROXY] 151 | 152 | def _get_relationship_fields(self, proxied_relationships): 153 | return (f.key for f in self._mapper.relationships 154 | if f.key not in self._ignored_fields | proxied_relationships) 155 | 156 | def _get_column_fields(self): 157 | # We don't include "plain" foreign keys in our schema, as embedding 158 | # would not work for them (except the id_field, which is always 159 | # included). 160 | # TODO: Think about this decision again and maybe implement support for 161 | # foreign keys without relationships. 162 | return (f.key for f in self._mapper.column_attrs 163 | if f.key not in self._ignored_fields 164 | and isinstance(f.expression, expression.ColumnElement) 165 | and (f.key == self._id_field 166 | or len(f.expression.foreign_keys) == 0)) 167 | 168 | def _get_column_property_fields(self): 169 | return (f.key for f in self._mapper.column_attrs 170 | if f.key not in self._ignored_fields 171 | and isinstance(f.expression, expression.Label)) 172 | 173 | def _get_hybrid_property_fields(self): 174 | return [k for k, v in self.model.__dict__.items() 175 | if k not in self._ignored_fields 176 | and getattr(v, 'extension_type', None) == HYBRID_PROPERTY] 177 | 178 | def _render_schema(self, field_configs, related_resource_configs): 179 | schema = {k: v.render(related_resource_configs) 180 | for k, v in field_configs.items()} 181 | # The id field has to be unique in all cases. 182 | schema[self._id_field]['unique'] = True 183 | # This is a workaround to support PUT for resources with integer ids. 184 | # TODO: Investigate this and fix it properly. 185 | if schema[self._item_lookup_field]['type'] == 'integer': 186 | schema[self._item_lookup_field]['coerce'] = int 187 | return schema 188 | 189 | def _render_datasource(self, field_configs, etag): 190 | projection = {k: 1 for k in field_configs.keys()} 191 | projection[etag] = 0 # is handled automatically based on IF_MATCH 192 | return { 193 | 'source': self.model.__name__, 194 | 'projection': projection 195 | } 196 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/auth/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/foreign_primary_key/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/foreign_primary_key/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/foreign_primary_key/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.examples.foreign_primary_key.domain import Base, Lock, Node 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | 7 | app = Eve(validator=ValidatorSQL, data=SQL) 8 | 9 | db = app.data.driver 10 | Base.metadata.bind = db.engine 11 | db.Model = Base 12 | db.create_all() 13 | 14 | nodes = [Node(), Node()] 15 | locks = [Lock(node=nodes[1])] 16 | db.session.add_all(nodes + locks) 17 | db.session.commit() 18 | 19 | # using reloader will destroy in-memory sqlite db 20 | app.run(debug=True, use_reloader=False) 21 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/foreign_primary_key/domain.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import relationship 4 | 5 | Base = declarative_base() 6 | 7 | 8 | class BaseModel(Base): 9 | __abstract__ = True 10 | _created = Column(DateTime, default=func.now()) 11 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 12 | _etag = Column(String(40)) 13 | 14 | 15 | class Node(BaseModel): 16 | __tablename__ = 'node' 17 | id = Column(Integer, primary_key=True, autoincrement=True) 18 | lock = relationship('Lock', uselist=False) 19 | 20 | 21 | class Lock(BaseModel): 22 | __tablename__ = 'lock' 23 | node_id = Column(Integer, ForeignKey('node.id'), 24 | primary_key=True, nullable=False) 25 | node = relationship(Node, uselist=False, back_populates='lock') 26 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/foreign_primary_key/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | from eve_sqlalchemy.examples.foreign_primary_key.domain import Lock, Node 3 | 4 | DEBUG = True 5 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | RESOURCE_METHODS = ['GET', 'POST'] 8 | 9 | # The following two lines will output the SQL statements executed by 10 | # SQLAlchemy. This is useful while debugging and in development, but is turned 11 | # off by default. 12 | # -------- 13 | # SQLALCHEMY_ECHO = True 14 | # SQLALCHEMY_RECORD_QUERIES = True 15 | 16 | # The default schema is generated using DomainConfig: 17 | DOMAIN = DomainConfig({ 18 | 'nodes': ResourceConfig(Node), 19 | 'locks': ResourceConfig(Lock) 20 | }).render() 21 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_many/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/many_to_many/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_many/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.examples.many_to_many.domain import Base, Child, Parent 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | 7 | app = Eve(validator=ValidatorSQL, data=SQL) 8 | 9 | db = app.data.driver 10 | Base.metadata.bind = db.engine 11 | db.Model = Base 12 | db.create_all() 13 | 14 | children = [Child() for _ in range(20)] 15 | parents = [Parent(children=children[:n]) for n in range(10)] 16 | db.session.add_all(parents) 17 | db.session.commit() 18 | 19 | # using reloader will destroy in-memory sqlite db 20 | app.run(debug=True, use_reloader=False) 21 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_many/domain.py: -------------------------------------------------------------------------------- 1 | """Basic Many-To-Many relationship configuration in SQLAlchemy. 2 | 3 | This is taken from the official SQLAlchemy documentation: 4 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#many-to-many 5 | """ 6 | 7 | from sqlalchemy import ( 8 | Column, DateTime, ForeignKey, Integer, String, Table, func, 9 | ) 10 | from sqlalchemy.ext.declarative import declarative_base 11 | from sqlalchemy.orm import relationship 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class BaseModel(Base): 17 | __abstract__ = True 18 | _created = Column(DateTime, default=func.now()) 19 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 20 | _etag = Column(String(40)) 21 | 22 | 23 | association_table = Table( 24 | 'association', Base.metadata, 25 | Column('left_id', Integer, ForeignKey('left.id')), 26 | Column('right_id', Integer, ForeignKey('right.id')) 27 | ) 28 | 29 | 30 | class Parent(BaseModel): 31 | __tablename__ = 'left' 32 | id = Column(Integer, primary_key=True) 33 | children = relationship("Child", secondary=association_table, 34 | backref="parents") 35 | 36 | 37 | class Child(BaseModel): 38 | __tablename__ = 'right' 39 | id = Column(Integer, primary_key=True) 40 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_many/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | from eve_sqlalchemy.examples.many_to_many.domain import Child, Parent 3 | 4 | DEBUG = True 5 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | RESOURCE_METHODS = ['GET', 'POST'] 8 | ITEM_METHODS = ['GET', 'PATCH'] 9 | 10 | # The following two lines will output the SQL statements executed by 11 | # SQLAlchemy. This is useful while debugging and in development, but is turned 12 | # off by default. 13 | # -------- 14 | # SQLALCHEMY_ECHO = True 15 | # SQLALCHEMY_RECORD_QUERIES = True 16 | 17 | # The default schema is generated using DomainConfig: 18 | DOMAIN = DomainConfig({ 19 | 'parents': ResourceConfig(Parent), 20 | 'children': ResourceConfig(Child) 21 | }).render() 22 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_one/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/many_to_one/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_one/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.examples.many_to_one.domain import Base, Child, Parent 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | 7 | app = Eve(validator=ValidatorSQL, data=SQL) 8 | 9 | db = app.data.driver 10 | Base.metadata.bind = db.engine 11 | db.Model = Base 12 | db.create_all() 13 | 14 | children = [Child(), Child()] 15 | parents = [Parent(child=children[n % 2]) for n in range(10)] 16 | db.session.add_all(parents) 17 | db.session.commit() 18 | 19 | # using reloader will destroy in-memory sqlite db 20 | app.run(debug=True, use_reloader=False) 21 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_one/domain.py: -------------------------------------------------------------------------------- 1 | """Basic Many-To-One relationship configuration in SQLAlchemy. 2 | 3 | This is taken from the official SQLAlchemy documentation: 4 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#many-to-one 5 | """ 6 | 7 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import relationship 10 | 11 | Base = declarative_base() 12 | 13 | 14 | class BaseModel(Base): 15 | __abstract__ = True 16 | _created = Column(DateTime, default=func.now()) 17 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 18 | _etag = Column(String(40)) 19 | 20 | 21 | class Parent(BaseModel): 22 | __tablename__ = 'parent' 23 | id = Column(Integer, primary_key=True) 24 | child_id = Column(Integer, ForeignKey('child.id')) 25 | child = relationship("Child", backref="parents") 26 | 27 | 28 | class Child(BaseModel): 29 | __tablename__ = 'child' 30 | id = Column(Integer, primary_key=True) 31 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/many_to_one/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | from eve_sqlalchemy.examples.many_to_one.domain import Child, Parent 3 | 4 | DEBUG = True 5 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | RESOURCE_METHODS = ['GET', 'POST'] 8 | 9 | # The following two lines will output the SQL statements executed by 10 | # SQLAlchemy. This is useful while debugging and in development, but is turned 11 | # off by default. 12 | # -------- 13 | # SQLALCHEMY_ECHO = True 14 | # SQLALCHEMY_RECORD_QUERIES = True 15 | 16 | # The default schema is generated using DomainConfig: 17 | DOMAIN = DomainConfig({ 18 | 'parents': ResourceConfig(Parent), 19 | 'children': ResourceConfig(Child) 20 | }).render() 21 | 22 | DOMAIN['children']['datasource']['projection']['child_id'] = 1 23 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/multiple_dbs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/multiple_dbs/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/multiple_dbs/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.examples.multiple_dbs.domain import db as flask_db 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | 7 | app = Eve(validator=ValidatorSQL, data=SQL) 8 | db = app.data.driver 9 | flask_db.Model.metadata.bind = db.engine 10 | db.Model = flask_db.Model 11 | db.create_all() 12 | 13 | # using reloader will destroy in-memory sqlite db 14 | app.run(debug=True, use_reloader=False) 15 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/multiple_dbs/domain.py: -------------------------------------------------------------------------------- 1 | from flask_sqlalchemy import SQLAlchemy 2 | from sqlalchemy import Column, DateTime, Integer, String, func 3 | 4 | db = SQLAlchemy() 5 | 6 | 7 | class CommonColumns(db.Model): 8 | __abstract__ = True 9 | _created = Column(DateTime, default=func.now()) 10 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 11 | _etag = Column(String(40)) 12 | 13 | 14 | class Table1(CommonColumns): 15 | __tablename__ = "table1" 16 | id = Column(String(255), primary_key=True) 17 | 18 | 19 | class Table2(CommonColumns): 20 | __bind_key__ = 'db2' 21 | __tablename__ = "table2" 22 | id = Column(Integer, primary_key=True) 23 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/multiple_dbs/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | from eve_sqlalchemy.examples.multiple_dbs.domain import Table1, Table2 3 | 4 | DEBUG = True 5 | SQLALCHEMY_TRACK_MODIFICATIONS = False 6 | RESOURCE_METHODS = ['GET', 'POST'] 7 | ITEM_METHODS = ['GET', 'PATCH', 'PUT', 'DELETE'] 8 | 9 | SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/db1.sqlite' 10 | SQLALCHEMY_BINDS = { 11 | 'db2': 'sqlite:////tmp/db2.sqlite' 12 | } 13 | 14 | # The following two lines will output the SQL statements executed by 15 | # SQLAlchemy. This is useful while debugging and in development, but is turned 16 | # off by default. 17 | # -------- 18 | # SQLALCHEMY_ECHO = True 19 | # SQLALCHEMY_RECORD_QUERIES = True 20 | 21 | # The default schema is generated using DomainConfig: 22 | DOMAIN = DomainConfig({ 23 | 'table1': ResourceConfig(Table1), 24 | 'table2': ResourceConfig(Table2) 25 | }).render() 26 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/one_to_many/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/one_to_many/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/one_to_many/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.examples.one_to_many.domain import Base, Child, Parent 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | 7 | app = Eve(validator=ValidatorSQL, data=SQL) 8 | 9 | db = app.data.driver 10 | Base.metadata.bind = db.engine 11 | db.Model = Base 12 | 13 | # create database schema on startup and populate some example data 14 | db.create_all() 15 | db.session.add_all([Parent(children=[Child() for k in range(n)]) 16 | for n in range(10)]) 17 | db.session.commit() 18 | 19 | # using reloader will destroy the in-memory sqlite db 20 | app.run(debug=True, use_reloader=False) 21 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/one_to_many/domain.py: -------------------------------------------------------------------------------- 1 | """Basic One-To-Many relationship configuration in SQLAlchemy. 2 | 3 | This is taken from the official SQLAlchemy documentation: 4 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#one-to-many 5 | """ 6 | 7 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import relationship 10 | 11 | Base = declarative_base() 12 | 13 | 14 | class BaseModel(Base): 15 | __abstract__ = True 16 | _created = Column(DateTime, default=func.now()) 17 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 18 | _etag = Column(String(40)) 19 | 20 | 21 | class Parent(BaseModel): 22 | __tablename__ = 'parent' 23 | id = Column(Integer, primary_key=True) 24 | children = relationship("Child", backref="parent") 25 | 26 | 27 | class Child(BaseModel): 28 | __tablename__ = 'child' 29 | id = Column(Integer, primary_key=True) 30 | parent_id = Column(Integer, ForeignKey('parent.id')) 31 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/one_to_many/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | from eve_sqlalchemy.examples.one_to_many.domain import Child, Parent 3 | 4 | DEBUG = True 5 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | RESOURCE_METHODS = ['GET', 'POST'] 8 | 9 | # The following two lines will output the SQL statements executed by 10 | # SQLAlchemy. This is useful while debugging and in development, but is turned 11 | # off by default. 12 | # -------- 13 | # SQLALCHEMY_ECHO = True 14 | # SQLALCHEMY_RECORD_QUERIES = True 15 | 16 | # The default schema is generated using DomainConfig: 17 | DOMAIN = DomainConfig({ 18 | 'parents': ResourceConfig(Parent), 19 | 'children': ResourceConfig(Child) 20 | }).render() 21 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/simple/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/simple/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/simple/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.examples.simple.tables import Base, Invoices, People 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | 7 | app = Eve(validator=ValidatorSQL, data=SQL) 8 | 9 | # bind SQLAlchemy 10 | db = app.data.driver 11 | Base.metadata.bind = db.engine 12 | db.Model = Base 13 | db.create_all() 14 | 15 | # Insert some example data in the db 16 | if not db.session.query(People).count(): 17 | from eve_sqlalchemy.examples.simple import example_data 18 | for item in example_data.test_data: 19 | db.session.add(People(firstname=item[0], lastname=item[1])) 20 | db.session.add(Invoices(number=42, people_id=1)) 21 | db.session.commit() 22 | 23 | # using reloader will destroy in-memory sqlite db 24 | app.run(debug=True, use_reloader=False) 25 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/simple/example_data.py: -------------------------------------------------------------------------------- 1 | test_data = [ 2 | (u'George', u'Washington'), 3 | (u'John', u'Adams'), 4 | (u'Thomas', u'Jefferson'), 5 | (u'George', u'Clinton'), 6 | (u'James', u'Madison'), 7 | (u'Elbridge', u'Gerry'), 8 | (u'James', u'Monroe'), 9 | (u'John', u'Adams'), 10 | (u'Andrew', u'Jackson'), 11 | (u'Martin', u'Van Buren'), 12 | (u'William', u'Harrison'), 13 | (u'John', u'Tyler'), 14 | (u'James', u'Polk'), 15 | (u'Zachary', u'Taylor'), 16 | (u'Millard', u'Fillmore'), 17 | (u'Franklin', u'Pierce'), 18 | (u'James', u'Buchanan'), 19 | (u'Abraham', u'Lincoln'), 20 | (u'Andrew', u'Johnson'), 21 | (u'Ulysses', u'Grant'), 22 | (u'Henry', u'Wilson'), 23 | (u'Rutherford', u'Hayes'), 24 | (u'James', u'Garfield'), 25 | (u'Chester', u'Arthur'), 26 | (u'Grover', u'Cleveland'), 27 | (u'Benjamin', u'Harrison'), 28 | (u'Grover', u'Cleveland'), 29 | (u'William', u'McKinley'), 30 | (u'Theodore', u'Roosevelt'), 31 | (u'Charles', u'Fairbanks'), 32 | (u'William', u'Taft'), 33 | (u'Woodrow', u'Wilson'), 34 | (u'Warren', u'Harding'), 35 | (u'Calvin', u'Coolidge'), 36 | (u'Charles', u'Dawes'), 37 | (u'Herbert', u'Hoover'), 38 | (u'Franklin', u'Roosevelt'), 39 | (u'Henry', u'Wallace'), 40 | (u'Harry', u'Truman'), 41 | (u'Alben', u'Barkley'), 42 | (u'Dwight', u'Eisenhower'), 43 | (u'John', u'Kennedy'), 44 | (u'Lyndon', u'Johnson'), 45 | (u'Hubert', u'Humphrey'), 46 | (u'Richard', u'Nixon'), 47 | (u'Gerald', u'Ford'), 48 | (u'Nelson', u'Rockefeller'), 49 | (u'Jimmy', u'Carter'), 50 | (u'Ronald', u'Reagan'), 51 | (u'George', u'Bush'), 52 | (u'Bill', u'Clinton'), 53 | (u'George', u'Bush'), 54 | (u'Barack', u'Obama') 55 | ] 56 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/simple/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | from eve_sqlalchemy.examples.simple.tables import Invoices, People 3 | 4 | DEBUG = True 5 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 6 | SQLALCHEMY_TRACK_MODIFICATIONS = False 7 | RESOURCE_METHODS = ['GET', 'POST'] 8 | 9 | # The following two lines will output the SQL statements executed by 10 | # SQLAlchemy. This is useful while debugging and in development, but is turned 11 | # off by default. 12 | # -------- 13 | # SQLALCHEMY_ECHO = True 14 | # SQLALCHEMY_RECORD_QUERIES = True 15 | 16 | # The default schema is generated using DomainConfig: 17 | DOMAIN = DomainConfig({ 18 | 'people': ResourceConfig(People), 19 | 'invoices': ResourceConfig(Invoices) 20 | }).render() 21 | 22 | # But you can always customize it: 23 | DOMAIN['people'].update({ 24 | 'item_title': 'person', 25 | 'cache_control': 'max-age=10,must-revalidate', 26 | 'cache_expires': 10, 27 | 'resource_methods': ['GET', 'POST', 'DELETE'] 28 | }) 29 | 30 | # Even adding custom validations just for the REST-layer is possible: 31 | DOMAIN['invoices']['schema']['number'].update({ 32 | 'min': 10000 33 | }) 34 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/simple/tables.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy.orm import column_property, relationship 4 | 5 | Base = declarative_base() 6 | 7 | 8 | class CommonColumns(Base): 9 | __abstract__ = True 10 | _created = Column(DateTime, default=func.now()) 11 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 12 | _etag = Column(String(40)) 13 | 14 | 15 | class People(CommonColumns): 16 | __tablename__ = 'people' 17 | id = Column(Integer, primary_key=True, autoincrement=True) 18 | firstname = Column(String(80)) 19 | lastname = Column(String(120)) 20 | fullname = column_property(firstname + " " + lastname) 21 | 22 | 23 | class Invoices(CommonColumns): 24 | __tablename__ = 'invoices' 25 | id = Column(Integer, primary_key=True, autoincrement=True) 26 | number = Column(Integer) 27 | people_id = Column(Integer, ForeignKey('people.id')) 28 | people = relationship(People, uselist=False) 29 | -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/trivial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/examples/trivial/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/examples/trivial/trivial.py: -------------------------------------------------------------------------------- 1 | ''' Trivial Eve-SQLAlchemy example. ''' 2 | from eve import Eve 3 | from sqlalchemy import Column, Integer, String 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import column_property 6 | 7 | from eve_sqlalchemy import SQL 8 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 9 | from eve_sqlalchemy.validation import ValidatorSQL 10 | 11 | Base = declarative_base() 12 | 13 | 14 | class People(Base): 15 | __tablename__ = 'people' 16 | id = Column(Integer, primary_key=True, autoincrement=True) 17 | firstname = Column(String(80)) 18 | lastname = Column(String(120)) 19 | fullname = column_property(firstname + " " + lastname) 20 | 21 | 22 | SETTINGS = { 23 | 'DEBUG': True, 24 | 'SQLALCHEMY_DATABASE_URI': 'sqlite://', 25 | 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 26 | 'DOMAIN': DomainConfig({ 27 | 'people': ResourceConfig(People) 28 | }).render() 29 | } 30 | 31 | app = Eve(auth=None, settings=SETTINGS, validator=ValidatorSQL, data=SQL) 32 | 33 | # bind SQLAlchemy 34 | db = app.data.driver 35 | Base.metadata.bind = db.engine 36 | db.Model = Base 37 | db.create_all() 38 | 39 | # Insert some example data in the db 40 | if not db.session.query(People).count(): 41 | db.session.add_all([ 42 | People(firstname=u'George', lastname=u'Washington'), 43 | People(firstname=u'John', lastname=u'Adams'), 44 | People(firstname=u'Thomas', lastname=u'Jefferson')]) 45 | db.session.commit() 46 | 47 | # using reloader will destroy in-memory sqlite db 48 | app.run(debug=True, use_reloader=False) 49 | -------------------------------------------------------------------------------- /eve_sqlalchemy/media.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Media storage for sqlalchemy extension. 4 | 5 | :copyright: (c) 2014 by Andrew Mleczko 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from io import BytesIO 11 | 12 | 13 | class SQLBlobMediaStorage(object): 14 | """ The MediaStorage class provides a standardized API for storing files, 15 | along with a set of default behaviors that all other storage systems can 16 | inherit or override as necessary. 17 | 18 | ..versioneadded:: 0.3 19 | """ 20 | 21 | def __init__(self, app=None): 22 | """ 23 | :param app: the flask application (eve itself). This can be used by 24 | the class to access, amongst other things, the app.config object to 25 | retrieve class-specific settings. 26 | """ 27 | self.app = app 28 | 29 | def get(self, content): 30 | """ Opens the file given by name or unique id. Note that although the 31 | returned file is guaranteed to be a File object, it might actually be 32 | some subclass. Returns None if no file was found. 33 | """ 34 | return BytesIO(content) 35 | 36 | def put(self, content, filename=None, content_type=None): 37 | """ Saves a new file using the storage system, preferably with the name 38 | specified. If there already exists a file with this name name, the 39 | storage system may modify the filename as necessary to get a unique 40 | name. Depending on the storage system, a unique id or the actual name 41 | of the stored file will be returned. The content type argument is used 42 | to appropriately identify the file when it is retrieved. 43 | """ 44 | content.stream.seek(0) 45 | return content.stream.read() 46 | 47 | def delete(self, id_or_filename): 48 | """ Deletes the file referenced by name or unique id. If deletion is 49 | not supported on the target storage system this will raise 50 | NotImplementedError instead 51 | """ 52 | if not id_or_filename: # there is nothing to remove 53 | return 54 | 55 | def exists(self, id_or_filename): 56 | """ Returns True if a file referenced by the given name or unique id 57 | already exists in the storage system, or False if the name is available 58 | for a new file. 59 | """ 60 | raise NotImplementedError 61 | -------------------------------------------------------------------------------- /eve_sqlalchemy/structures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | These classes provide a middle layer to transform a SQLAlchemy query into 4 | a series of object that Eve understands and can be rendered as JSON. 5 | 6 | :copyright: (c) 2013 by Andrew Mleczko and Tomasz Jezierski (Tefnet) 7 | :license: BSD, see LICENSE for more details. 8 | 9 | """ 10 | from __future__ import unicode_literals 11 | 12 | from .utils import sqla_object_to_dict 13 | 14 | 15 | class SQLAResultCollection(object): 16 | """ 17 | Collection of results. The object holds onto a Flask-SQLAlchemy query 18 | object and serves a generator off it. 19 | 20 | :param query: Base SQLAlchemy query object for the requested resource 21 | :param fields: fields to be rendered in the response, as a list of strings 22 | :param spec: filter to be applied to the query 23 | :param sort: sorting requirements 24 | :param max_results: number of entries to be returned per page 25 | :param page: page requested 26 | """ 27 | def __init__(self, query, fields, **kwargs): 28 | self._query = query 29 | self._fields = fields 30 | self._spec = kwargs.get('spec') 31 | self._sort = kwargs.get('sort') 32 | self._max_results = kwargs.get('max_results') 33 | self._page = kwargs.get('page') 34 | self._resource = kwargs.get('resource') 35 | if self._spec: 36 | self._query = self._query.filter(*self._spec) 37 | if self._sort: 38 | for (order_by, joins) in self._sort: 39 | self._query = self._query.filter(*joins).order_by(order_by) 40 | 41 | # save the count of items to an internal variables before applying the 42 | # limit to the query as that screws the count returned by it 43 | self._count = self._query.count() 44 | if self._max_results: 45 | self._query = self._query.limit(self._max_results) 46 | if self._page: 47 | self._query = self._query.offset((self._page - 1) * 48 | self._max_results) 49 | 50 | def __iter__(self): 51 | for i in self._query: 52 | yield sqla_object_to_dict(i, self._fields) 53 | 54 | def count(self, **kwargs): 55 | return self._count 56 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Base classes for Eve-SQLAlchemy tests. 3 | 4 | We try to mimic Eve tests as closely as possible. Therefore we introduce 5 | derived classes for testing of HTTP methods (get, post, put, patch, delete). 6 | Those run the same integration tests as are run for Eve, with some 7 | modifications here and there if the Eve counterparts don't work for us. For 8 | each overridden method there is a comment stating the changes made to the 9 | original Eve test code. 10 | """ 11 | from __future__ import unicode_literals 12 | 13 | import collections 14 | import os 15 | import random 16 | 17 | import eve 18 | import eve.tests 19 | from eve import ISSUES 20 | 21 | from eve_sqlalchemy import SQL 22 | from eve_sqlalchemy.tests.test_sql_tables import Base 23 | from eve_sqlalchemy.validation import ValidatorSQL 24 | 25 | 26 | class TestMinimal(eve.tests.TestMinimal): 27 | 28 | def setUp(self, settings_file=None, url_converters=None, 29 | declarative_base=None): 30 | """ Prepare the test fixture 31 | 32 | :param settings_file: the name of the settings file. Defaults 33 | to `eve/tests/test_settings.py`. 34 | 35 | This is mostly the same as in eve.tests.__init__.py, except the 36 | creation of self.app. 37 | """ 38 | self.this_directory = os.path.dirname(os.path.realpath(__file__)) 39 | if settings_file is None: 40 | # Load the settings file, using a robust path 41 | settings_file = os.path.join(self.this_directory, 42 | 'test_settings.py') 43 | 44 | self.known_resource_count = 101 45 | 46 | self.settings_file = settings_file 47 | if declarative_base is not None: 48 | SQL.driver.Model = declarative_base 49 | else: 50 | SQL.driver.Model = Base 51 | 52 | self.app = eve.Eve(settings=self.settings_file, 53 | url_converters=url_converters, data=SQL, 54 | validator=ValidatorSQL) 55 | self.setupDB() 56 | 57 | self.test_client = self.app.test_client() 58 | 59 | self.domain = self.app.config['DOMAIN'] 60 | 61 | def setupDB(self): 62 | self.connection = self.app.data.driver 63 | self.connection.session.execute('pragma foreign_keys=on') 64 | self.connection.drop_all() 65 | self.connection.create_all() 66 | self.bulk_insert() 67 | 68 | def dropDB(self): 69 | self.connection.session.remove() 70 | self.connection.drop_all() 71 | 72 | def assertValidationError(self, response, matches): 73 | self.assertTrue(eve.STATUS in response) 74 | self.assertTrue(eve.STATUS_ERR in response[eve.STATUS]) 75 | self.assertTrue(ISSUES in response) 76 | issues = response[ISSUES] 77 | self.assertTrue(len(issues)) 78 | 79 | for k, v in matches.items(): 80 | self.assertTrue(k in issues) 81 | if isinstance(issues[k], collections.Sequence): 82 | self.assertTrue(v in issues[k]) 83 | if isinstance(issues[k], collections.Mapping): 84 | self.assertTrue(v in issues[k].values()) 85 | 86 | 87 | class TestBase(eve.tests.TestBase, TestMinimal): 88 | 89 | def setUp(self, url_converters=None): 90 | super(TestBase, self).setUp(url_converters) 91 | self.unknown_item_id = 424242 92 | self.unknown_item_id_url = ('/%s/%s' % 93 | (self.domain[self.known_resource]['url'], 94 | self.unknown_item_id)) 95 | 96 | def random_contacts(self, num, standard_date_fields=True): 97 | contacts = \ 98 | super(TestBase, self).random_contacts(num, standard_date_fields) 99 | return [self._create_contact_dict(dict_) for dict_ in contacts] 100 | 101 | def _create_contact_dict(self, dict_): 102 | result = self._filter_keys_by_schema(dict_, 'contacts') 103 | result['tid'] = random.randint(1, 10000) 104 | if 'username' not in dict_ or dict_['username'] is None: 105 | result['username'] = '' 106 | return result 107 | 108 | def _filter_keys_by_schema(self, dict_, resource): 109 | allowed_keys = self.app.config['DOMAIN'][resource]['schema'].keys() 110 | keys = set(dict_.keys()) & set(allowed_keys) 111 | return dict([(key, dict_[key]) for key in keys]) 112 | 113 | def random_payments(self, num): 114 | payments = super(TestBase, self).random_payments(num) 115 | return [self._filter_keys_by_schema(dict_, 'payments') 116 | for dict_ in payments] 117 | 118 | def random_invoices(self, num): 119 | invoices = super(TestBase, self).random_invoices(num) 120 | return [self._filter_keys_by_schema(dict_, 'invoices') 121 | for dict_ in invoices] 122 | 123 | def random_internal_transactions(self, num): 124 | transactions = super(TestBase, self).random_internal_transactions(num) 125 | return [self._filter_keys_by_schema(dict_, 'internal_transactions') 126 | for dict_ in transactions] 127 | 128 | def random_products(self, num): 129 | products = super(TestBase, self).random_products(num) 130 | return [self._filter_keys_by_schema(dict_, 'products') 131 | for dict_ in products] 132 | 133 | def bulk_insert(self): 134 | self.app.data.insert('contacts', 135 | self.random_contacts(self.known_resource_count)) 136 | self.app.data.insert('users', self.random_users(2)) 137 | self.app.data.insert('payments', self.random_payments(10)) 138 | self.app.data.insert('invoices', self.random_invoices(1)) 139 | self.app.data.insert('internal_transactions', 140 | self.random_internal_transactions(4)) 141 | self.app.data.insert('products', self.random_products(2)) 142 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, DateTime, String, func 5 | from sqlalchemy.ext.declarative import declared_attr 6 | 7 | 8 | def call_for(*args): 9 | def decorator(f): 10 | def loop(self): 11 | for arg in args: 12 | f(self, arg) 13 | return loop 14 | return decorator 15 | 16 | 17 | class BaseModel(object): 18 | __abstract__ = True 19 | _created = Column(DateTime, default=func.now()) 20 | _updated = Column(DateTime, default=func.now()) 21 | _etag = Column(String) 22 | 23 | @declared_attr 24 | def __tablename__(cls): 25 | return cls.__name__.lower() 26 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/domainconfig/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/tests/config/domainconfig/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/domainconfig/ambiguous_relations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from unittest import TestCase 5 | 6 | from eve.exceptions import ConfigException 7 | from sqlalchemy import Boolean, Column, ForeignKey, Integer, Table 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import relationship 10 | 11 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 12 | 13 | from .. import BaseModel 14 | 15 | Base = declarative_base(cls=BaseModel) 16 | 17 | group_members = Table( 18 | 'group_members', Base.metadata, 19 | Column('group_id', Integer, ForeignKey('group.id')), 20 | Column('user_id', Integer, ForeignKey('user.id')) 21 | ) 22 | 23 | 24 | class User(Base): 25 | id = Column(Integer, primary_key=True) 26 | is_admin = Column(Boolean, default=False) 27 | 28 | 29 | class Group(Base): 30 | id = Column(Integer, primary_key=True) 31 | members = relationship(User, secondary=group_members) 32 | admin_id = Column(Integer, ForeignKey('user.id')) 33 | admin = relationship(User) 34 | 35 | 36 | class TestAmbiguousRelations(TestCase): 37 | 38 | def setUp(self): 39 | super(TestAmbiguousRelations, self).setUp() 40 | self._domain = DomainConfig({ 41 | 'users': ResourceConfig(User), 42 | 'admins': ResourceConfig(User), 43 | 'groups': ResourceConfig(Group) 44 | }) 45 | 46 | def test_missing_related_resources_without_groups(self): 47 | del self._domain.resource_configs['groups'] 48 | domain_dict = self._domain.render() 49 | self.assertIn('users', domain_dict) 50 | self.assertIn('admins', domain_dict) 51 | 52 | def test_missing_related_resources(self): 53 | with self.assertRaises(ConfigException) as cm: 54 | self._domain.render() 55 | self.assertIn('Cannot determine related resource for {}' 56 | .format(Group.__name__), str(cm.exception)) 57 | 58 | def test_two_endpoints_for_one_model(self): 59 | self._domain.related_resources = { 60 | (Group, 'members'): 'users', 61 | (Group, 'admin'): 'admins' 62 | } 63 | groups_schema = self._domain.render()['groups']['schema'] 64 | self.assertEqual(groups_schema['admin']['data_relation']['resource'], 65 | 'admins') 66 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from unittest import TestCase 5 | 6 | from eve_sqlalchemy.config import ResourceConfig 7 | 8 | 9 | class ResourceConfigTestCase(TestCase): 10 | 11 | def setUp(self): 12 | self._created = '_created' 13 | self._updated = '_updated' 14 | self._etag = '_etag' 15 | self._related_resource_configs = {} 16 | 17 | def _render(self, model_or_resource_config): 18 | if hasattr(model_or_resource_config, 'render'): 19 | rc = model_or_resource_config 20 | else: 21 | rc = ResourceConfig(model_or_resource_config) 22 | return rc.render(self._created, self._updated, self._etag, 23 | self._related_resource_configs) 24 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/association_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, Integer, String, Table 5 | from sqlalchemy.ext.associationproxy import association_proxy 6 | from sqlalchemy.ext.declarative import declarative_base 7 | from sqlalchemy.orm import relationship 8 | 9 | from eve_sqlalchemy.config import ResourceConfig 10 | 11 | from .. import BaseModel 12 | from . import ResourceConfigTestCase 13 | 14 | Base = declarative_base(cls=BaseModel) 15 | 16 | 17 | class User(Base): 18 | __tablename__ = 'user' 19 | id = Column(Integer, primary_key=True) 20 | name = Column(String(64)) 21 | kw = relationship("Keyword", secondary=lambda: userkeywords_table) 22 | 23 | def __init__(self, name): 24 | self.name = name 25 | 26 | keywords = association_proxy('kw', 'keyword') 27 | 28 | 29 | class Keyword(Base): 30 | __tablename__ = 'keyword' 31 | id = Column(Integer, primary_key=True) 32 | keyword = Column('keyword', String(64), unique=True, nullable=False) 33 | 34 | def __init__(self, keyword): 35 | self.keyword = keyword 36 | 37 | 38 | userkeywords_table = Table( 39 | 'userkeywords', Base.metadata, 40 | Column('user_id', Integer, ForeignKey("user.id"), primary_key=True), 41 | Column('keyword_id', Integer, ForeignKey("keyword.id"), primary_key=True) 42 | ) 43 | 44 | 45 | class TestAssociationProxy(ResourceConfigTestCase): 46 | """Test an Association Proxy in SQLAlchemy. 47 | 48 | The model definitions are taken from the official documentation: 49 | https://docs.sqlalchemy.org/en/rel_1_1/orm/extensions/associationproxy.html#simplifying-scalar-collections 50 | """ 51 | 52 | def setUp(self): 53 | super(TestAssociationProxy, self).setUp() 54 | self._related_resource_configs = { 55 | User: ('users', ResourceConfig(User)), 56 | Keyword: ('keywords', ResourceConfig(Keyword)) 57 | } 58 | 59 | def test_user_projection(self): 60 | projection = self._render(User)['datasource']['projection'] 61 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'name': 1, 62 | 'keywords': 1}) 63 | 64 | def test_keyword_projection(self): 65 | projection = self._render(Keyword)['datasource']['projection'] 66 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'keyword': 1}) 67 | 68 | def test_user_schema(self): 69 | schema = self._render(User)['schema'] 70 | self.assertNotIn('kw', schema) 71 | self.assertIn('keywords', schema) 72 | self.assertEqual(schema['keywords'], { 73 | 'type': 'list', 74 | 'schema': { 75 | 'type': 'string', 76 | 'data_relation': { 77 | 'resource': 'keywords', 78 | 'field': 'keyword' 79 | } 80 | } 81 | }) 82 | 83 | def test_keyword_schema(self): 84 | schema = self._render(Keyword)['schema'] 85 | self.assertIn('keyword', schema) 86 | self.assertEqual(schema['keyword'], { 87 | 'type': 'string', 88 | 'unique': True, 89 | 'maxlength': 64, 90 | 'required': True, 91 | 'nullable': False 92 | }) 93 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/column_property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, Integer, String 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import column_property 7 | 8 | from .. import BaseModel 9 | from . import ResourceConfigTestCase 10 | 11 | Base = declarative_base(cls=BaseModel) 12 | 13 | 14 | class User(Base): 15 | id = Column(Integer, primary_key=True) 16 | firstname = Column(String(50)) 17 | lastname = Column(String(50)) 18 | fullname = column_property(firstname + " " + lastname) 19 | 20 | 21 | class TestColumnProperty(ResourceConfigTestCase): 22 | """Test a basic column property in SQLAlchemy. 23 | 24 | The model definition is taken from the official documentation: 25 | https://docs.sqlalchemy.org/en/rel_1_1/orm/mapping_columns.html#using-column-property-for-column-level-options 26 | """ 27 | 28 | def test_appears_in_projection(self): 29 | projection = self._render(User)['datasource']['projection'] 30 | self.assertIn('fullname', projection.keys()) 31 | self.assertEqual(projection['fullname'], 1) 32 | 33 | def test_schema(self): 34 | schema = self._render(User)['schema'] 35 | self.assertIn('fullname', schema.keys()) 36 | self.assertEqual(schema['fullname'], { 37 | 'type': 'string', 38 | 'readonly': True 39 | }) 40 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/datasource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, Integer, String 5 | from sqlalchemy.ext.declarative import declarative_base 6 | 7 | from .. import BaseModel 8 | from . import ResourceConfigTestCase 9 | 10 | Base = declarative_base(cls=BaseModel) 11 | 12 | 13 | class SomeModel(Base): 14 | id = Column(Integer, primary_key=True) 15 | unique = Column(String, unique=True) 16 | non_unique = Column(String) 17 | 18 | 19 | class TestDatasource(ResourceConfigTestCase): 20 | 21 | def test_set_source_to_model_name(self): 22 | endpoint_def = self._render(SomeModel) 23 | self.assertEqual(endpoint_def['datasource']['source'], 'SomeModel') 24 | 25 | def test_projection_for_regular_columns(self): 26 | endpoint_def = self._render(SomeModel) 27 | self.assertEqual(endpoint_def['datasource']['projection'], { 28 | '_etag': 0, 29 | 'id': 1, 30 | 'unique': 1, 31 | 'non_unique': 1, 32 | }) 33 | 34 | def test_projection_with_custom_automatically_handled_fields(self): 35 | self._created = '_date_created' 36 | self._updated = '_last_updated' 37 | self._etag = 'non_unique' 38 | endpoint_def = self._render(SomeModel) 39 | self.assertEqual(endpoint_def['datasource']['projection'], { 40 | 'non_unique': 0, 41 | 'id': 1, 42 | 'unique': 1, 43 | }) 44 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/foreign_primary_key.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, Integer 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import relationship 7 | 8 | from eve_sqlalchemy.config import ResourceConfig 9 | 10 | from .. import BaseModel 11 | from . import ResourceConfigTestCase 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | class Node(Base): 17 | id = Column(Integer, primary_key=True, autoincrement=True) 18 | 19 | 20 | class Lock(Base): 21 | node_id = Column(Integer, ForeignKey('node.id'), 22 | primary_key=True, nullable=False) 23 | node = relationship('Node', uselist=False, backref='lock') 24 | 25 | 26 | class TestForeignPrimaryKey(ResourceConfigTestCase): 27 | 28 | def setUp(self): 29 | super(TestForeignPrimaryKey, self).setUp() 30 | self._related_resource_configs = { 31 | Node: ('nodes', ResourceConfig(Node)), 32 | Lock: ('locks', ResourceConfig(Lock)) 33 | } 34 | 35 | def test_lock_schema(self): 36 | schema = self._render(Lock)['schema'] 37 | self.assertIn('node_id', schema) 38 | self.assertIn('node', schema) 39 | self.assertEqual(schema['node_id'], { 40 | 'type': 'integer', 41 | 'unique': True, 42 | 'coerce': int, 43 | 'nullable': False, 44 | 'required': False 45 | }) 46 | self.assertEqual(schema['node'], { 47 | 'type': 'integer', 48 | 'coerce': int, 49 | 'data_relation': { 50 | 'resource': 'nodes', 51 | 'field': 'id' 52 | }, 53 | 'local_id_field': 'node_id', 54 | 'nullable': False, 55 | 'required': True, 56 | 'unique': True 57 | }) 58 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/hybrid_property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, Integer 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.ext.hybrid import hybrid_property 7 | 8 | from .. import BaseModel 9 | from . import ResourceConfigTestCase 10 | 11 | Base = declarative_base(cls=BaseModel) 12 | 13 | 14 | class Interval(Base): 15 | id = Column(Integer, primary_key=True) 16 | start = Column(Integer, nullable=False) 17 | end = Column(Integer, nullable=False) 18 | 19 | @hybrid_property 20 | def length(self): 21 | return self.end - self.start 22 | 23 | 24 | class TestHybridProperty(ResourceConfigTestCase): 25 | """Test a basic hybrid property in SQLAlchemy. 26 | 27 | The model definition is taken from the official documentation: 28 | https://docs.sqlalchemy.org/en/rel_1_1/orm/extensions/hybrid.html 29 | """ 30 | 31 | def test_appears_in_projection(self): 32 | projection = self._render(Interval)['datasource']['projection'] 33 | self.assertIn('length', projection.keys()) 34 | self.assertEqual(projection['length'], 1) 35 | 36 | def test_schema(self): 37 | schema = self._render(Interval)['schema'] 38 | self.assertIn('length', schema.keys()) 39 | self.assertEqual(schema['length'], { 40 | 'type': 'string', 41 | 'readonly': True 42 | }) 43 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/id_field.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from unittest import TestCase 5 | 6 | from eve.exceptions import ConfigException 7 | from sqlalchemy import Column, Integer, String 8 | from sqlalchemy.ext.declarative import declarative_base 9 | 10 | from eve_sqlalchemy.config import ResourceConfig 11 | 12 | from .. import BaseModel, call_for 13 | 14 | Base = declarative_base(cls=BaseModel) 15 | 16 | 17 | class SingleColumnPrimaryKey(Base): 18 | id = Column(Integer, primary_key=True) 19 | unique = Column(String, unique=True) 20 | non_unique = Column(String) 21 | 22 | 23 | class MultiColumnPrimaryKey(Base): 24 | id_1 = Column(Integer, primary_key=True) 25 | id_2 = Column(Integer, primary_key=True) 26 | unique = Column(String, unique=True) 27 | non_unique = Column(String) 28 | 29 | 30 | class TestIDField(TestCase): 31 | """ 32 | Test setting/deducing the resource-level `id_field` setting. 33 | 34 | There is no need to test the case where we have a model without a primary 35 | key, as such a model cannot be defined using sqlalchemy.ext.declarative. 36 | """ 37 | 38 | def test_set_to_primary_key_by_default(self): 39 | rc = ResourceConfig(SingleColumnPrimaryKey) 40 | self.assertEqual(rc.id_field, 'id') 41 | 42 | def test_set_to_primary_key_by_user(self): 43 | rc = ResourceConfig(SingleColumnPrimaryKey, id_field='id') 44 | self.assertEqual(rc.id_field, 'id') 45 | 46 | @call_for(SingleColumnPrimaryKey, MultiColumnPrimaryKey) 47 | def test_fail_for_non_existent_user_specified_id_field(self, model): 48 | with self.assertRaises(ConfigException) as cm: 49 | ResourceConfig(model, id_field='foo') 50 | self.assertIn('{}.foo does not exist.'.format(model.__name__), 51 | str(cm.exception)) 52 | 53 | @call_for(SingleColumnPrimaryKey, MultiColumnPrimaryKey) 54 | def test_fail_for_non_unique_user_specified_id_field(self, model): 55 | with self.assertRaises(ConfigException) as cm: 56 | ResourceConfig(model, id_field='non_unique') 57 | self.assertIn('{}.non_unique is not unique.'.format(model.__name__), 58 | str(cm.exception)) 59 | 60 | def test_fail_without_user_specified_id_field(self): 61 | model = MultiColumnPrimaryKey 62 | with self.assertRaises(ConfigException) as cm: 63 | ResourceConfig(model) 64 | self.assertIn("{}'s primary key consists of zero or multiple columns, " 65 | "thus we cannot deduce which one to use." 66 | .format(model.__name__), str(cm.exception)) 67 | 68 | def test_fail_with_user_specified_id_field_as_subset_of_primary_key(self): 69 | model = MultiColumnPrimaryKey 70 | with self.assertRaises(ConfigException) as cm: 71 | ResourceConfig(model, id_field='id_1') 72 | self.assertIn('{}.id_1 is not unique.'.format(model.__name__), 73 | str(cm.exception)) 74 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/inheritance.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, Integer 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import relationship 7 | 8 | from eve_sqlalchemy.config import ResourceConfig 9 | 10 | from .. import BaseModel 11 | from . import ResourceConfigTestCase 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | class Node(Base): 17 | id = Column(Integer, primary_key=True, autoincrement=True) 18 | 19 | 20 | class Thing(Node): 21 | id = Column(Integer, ForeignKey('node.id'), primary_key=True, 22 | nullable=False) 23 | group_id = Column(Integer, ForeignKey('group.id')) 24 | group = relationship('Group', uselist=False, back_populates='things') 25 | 26 | 27 | class Group(Base): 28 | id = Column(Integer, primary_key=True, autoincrement=True) 29 | things = relationship('Thing', back_populates='group') 30 | 31 | 32 | class TestPolymorphy(ResourceConfigTestCase): 33 | 34 | def setUp(self): 35 | super(TestPolymorphy, self).setUp() 36 | self._related_resource_configs = { 37 | Node: ('nodes', ResourceConfig(Node)), 38 | Thing: ('things', ResourceConfig(Thing)), 39 | Group: ('groups', ResourceConfig(Group)), 40 | } 41 | 42 | def test_node_schema(self): 43 | schema = self._render(Node)['schema'] 44 | self.assertIn('id', schema) 45 | 46 | def test_thing_schema(self): 47 | schema = self._render(Thing)['schema'] 48 | self.assertIn('id', schema) 49 | 50 | def test_group_schema(self): 51 | schema = self._render(Group)['schema'] 52 | self.assertIn('id', schema) 53 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/item_lookup_field.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from unittest import TestCase 5 | 6 | from eve.exceptions import ConfigException 7 | from sqlalchemy import Column, Integer, String 8 | from sqlalchemy.ext.declarative import declarative_base 9 | 10 | from eve_sqlalchemy.config import ResourceConfig 11 | 12 | from .. import BaseModel 13 | 14 | Base = declarative_base(cls=BaseModel) 15 | 16 | 17 | class SomeModel(Base): 18 | id = Column(Integer, primary_key=True) 19 | unique = Column(String, unique=True) 20 | non_unique = Column(String) 21 | 22 | 23 | class TestItemLookupField(TestCase): 24 | """Test setting/deducing the resource-level `item_lookup_field` setting.""" 25 | 26 | def test_set_to_id_field_by_default(self): 27 | rc = ResourceConfig(SomeModel) 28 | self.assertEqual(rc.item_lookup_field, rc.id_field) 29 | 30 | def test_set_to_user_specified_id_field_by_default(self): 31 | rc = ResourceConfig(SomeModel, id_field='unique') 32 | self.assertEqual(rc.item_lookup_field, 'unique') 33 | 34 | def test_set_to_user_specified_field(self): 35 | rc = ResourceConfig(SomeModel, item_lookup_field='unique') 36 | self.assertEqual(rc.item_lookup_field, 'unique') 37 | 38 | def test_set_to_id_field_by_user(self): 39 | rc = ResourceConfig(SomeModel, item_lookup_field='id') 40 | self.assertEqual(rc.item_lookup_field, 'id') 41 | 42 | def test_fail_for_non_existent_user_specified_item_lookup_field(self): 43 | model = SomeModel 44 | with self.assertRaises(ConfigException) as cm: 45 | ResourceConfig(model, item_lookup_field='foo') 46 | self.assertIn('{}.foo does not exist.'.format(model.__name__), 47 | str(cm.exception)) 48 | 49 | def test_fail_for_non_unique_user_specified_item_lookup_field(self): 50 | model = SomeModel 51 | with self.assertRaises(ConfigException) as cm: 52 | ResourceConfig(model, item_lookup_field='non_unique') 53 | self.assertIn('{}.non_unique is not unique.'.format(model.__name__), 54 | str(cm.exception)) 55 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/item_url.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from unittest import TestCase 5 | 6 | from sqlalchemy import Column, Integer, String 7 | from sqlalchemy.ext.declarative import declarative_base 8 | 9 | from eve_sqlalchemy.config import ResourceConfig 10 | 11 | from .. import BaseModel 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | class SomeModel(Base): 17 | id = Column(Integer, primary_key=True) 18 | unique = Column(String, unique=True) 19 | non_unique = Column(String) 20 | 21 | 22 | class TestItemUrl(TestCase): 23 | 24 | def test_set_to_default_regex_for_integer_item_lookup_field(self): 25 | rc = ResourceConfig(SomeModel) 26 | self.assertEqual(rc.item_url, 'regex("[0-9]+")') 27 | 28 | def test_set_to_default_regex_for_string_item_lookup_field(self): 29 | rc = ResourceConfig(SomeModel, item_lookup_field='unique') 30 | self.assertEqual(rc.item_url, 'regex("[a-zA-Z0-9_-]+")') 31 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/many_to_many_relationship.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, Integer, Table 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import relationship 7 | 8 | from eve_sqlalchemy.config import ResourceConfig 9 | 10 | from .. import BaseModel 11 | from . import ResourceConfigTestCase 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | association_table = Table( 17 | 'association', Base.metadata, 18 | Column('left_id', Integer, ForeignKey('left.id')), 19 | Column('right_id', Integer, ForeignKey('right.id')) 20 | ) 21 | 22 | 23 | class Parent(Base): 24 | __tablename__ = 'left' 25 | id = Column(Integer, primary_key=True) 26 | children = relationship('Child', secondary=association_table, 27 | backref='parents') 28 | 29 | 30 | class Child(Base): 31 | __tablename__ = 'right' 32 | id = Column(Integer, primary_key=True) 33 | 34 | 35 | class TestManyToManyRelationship(ResourceConfigTestCase): 36 | """Test a basic Many-To-Many relationship in SQLAlchemy. 37 | 38 | The model definitions are taken from the official documentation: 39 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#many-to-many 40 | """ 41 | 42 | def setUp(self): 43 | super(TestManyToManyRelationship, self).setUp() 44 | self._related_resource_configs = { 45 | Child: ('children', ResourceConfig(Child)), 46 | Parent: ('parents', ResourceConfig(Parent)) 47 | } 48 | 49 | def test_parent_projection(self): 50 | projection = self._render(Parent)['datasource']['projection'] 51 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'children': 1}) 52 | 53 | def test_child_projection(self): 54 | projection = self._render(Child)['datasource']['projection'] 55 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'parents': 1}) 56 | 57 | def test_parent_schema(self): 58 | schema = self._render(Parent)['schema'] 59 | self.assertIn('children', schema) 60 | self.assertEqual(schema['children'], { 61 | 'type': 'list', 62 | 'schema': { 63 | 'type': 'integer', 64 | 'data_relation': { 65 | 'resource': 'children', 66 | 'field': 'id' 67 | }, 68 | 'nullable': True 69 | } 70 | }) 71 | 72 | def test_child_schema(self): 73 | schema = self._render(Child)['schema'] 74 | self.assertIn('parents', schema) 75 | self.assertEqual(schema['parents'], { 76 | 'type': 'list', 77 | 'schema': { 78 | 'type': 'integer', 79 | 'data_relation': { 80 | 'resource': 'parents', 81 | 'field': 'id' 82 | }, 83 | 'nullable': True 84 | } 85 | }) 86 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/many_to_one_relationship.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, Integer 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import relationship 7 | 8 | from eve_sqlalchemy.config import ResourceConfig 9 | 10 | from .. import BaseModel 11 | from . import ResourceConfigTestCase 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | class Parent(Base): 17 | id = Column(Integer, primary_key=True) 18 | child_id = Column(Integer, ForeignKey('child.id')) 19 | child = relationship("Child") 20 | 21 | 22 | class Child(Base): 23 | id = Column(Integer, primary_key=True) 24 | 25 | 26 | class TestManyToOneRelationship(ResourceConfigTestCase): 27 | """Test a basic Many-To-One relationship in SQLAlchemy. 28 | 29 | The model definitions are taken from the official documentation: 30 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#many-to-one 31 | """ 32 | 33 | def setUp(self): 34 | super(TestManyToOneRelationship, self).setUp() 35 | self._related_resource_configs = { 36 | Child: ('children', ResourceConfig(Child)) 37 | } 38 | 39 | def test_parent_projection(self): 40 | projection = self._render(Parent)['datasource']['projection'] 41 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'child': 1}) 42 | 43 | def test_child_projection(self): 44 | projection = self._render(Child)['datasource']['projection'] 45 | self.assertEqual(projection, {'_etag': 0, 'id': 1}) 46 | 47 | def test_parent_schema(self): 48 | schema = self._render(Parent)['schema'] 49 | self.assertIn('child', schema) 50 | self.assertEqual(schema['child'], { 51 | 'type': 'integer', 52 | 'coerce': int, 53 | 'data_relation': { 54 | 'resource': 'children', 55 | 'field': 'id' 56 | }, 57 | 'local_id_field': 'child_id', 58 | 'nullable': True 59 | }) 60 | 61 | def test_child_schema(self): 62 | schema = self._render(Child)['schema'] 63 | self.assertNotIn('parent', schema) 64 | self.assertNotIn('parent_id', schema) 65 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/one_to_many_relationship.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from eve.exceptions import ConfigException 5 | from sqlalchemy import Column, ForeignKey, Integer 6 | from sqlalchemy.ext.declarative import declarative_base 7 | from sqlalchemy.orm import relationship 8 | 9 | from eve_sqlalchemy.config import ResourceConfig 10 | 11 | from .. import BaseModel 12 | from . import ResourceConfigTestCase 13 | 14 | Base = declarative_base(cls=BaseModel) 15 | 16 | 17 | class Parent(Base): 18 | id = Column(Integer, primary_key=True) 19 | children = relationship("Child") 20 | 21 | 22 | class Child(Base): 23 | id = Column(Integer, primary_key=True) 24 | parent_id = Column(Integer, ForeignKey('parent.id')) 25 | 26 | 27 | class TestOneToManyRelationship(ResourceConfigTestCase): 28 | """Test a basic One-To-Many relationship in SQLAlchemy. 29 | 30 | The model definitions are taken from the official documentation: 31 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#one-to-many 32 | """ 33 | 34 | def setUp(self): 35 | super(TestOneToManyRelationship, self).setUp() 36 | self._related_resource_configs = { 37 | Child: ('children', ResourceConfig(Child)) 38 | } 39 | 40 | def test_related_resources_missing(self): 41 | self._related_resource_configs = {} 42 | model = Parent 43 | with self.assertRaises(ConfigException) as cm: 44 | self._render(model) 45 | self.assertIn('Cannot determine related resource for {}.children' 46 | .format(model.__name__), str(cm.exception)) 47 | 48 | def test_parent_projection(self): 49 | projection = self._render(Parent)['datasource']['projection'] 50 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'children': 1}) 51 | 52 | def test_child_projection(self): 53 | projection = self._render(Child)['datasource']['projection'] 54 | self.assertEqual(projection, {'_etag': 0, 'id': 1}) 55 | 56 | def test_parent_schema(self): 57 | schema = self._render(Parent)['schema'] 58 | self.assertIn('children', schema) 59 | self.assertEqual(schema['children'], { 60 | 'type': 'list', 61 | 'schema': { 62 | 'type': 'integer', 63 | 'data_relation': { 64 | 'resource': 'children', 65 | 'field': 'id' 66 | }, 67 | 'nullable': True 68 | } 69 | }) 70 | 71 | def test_child_schema(self): 72 | schema = self._render(Child)['schema'] 73 | self.assertNotIn('parent', schema) 74 | self.assertNotIn('parent_id', schema) 75 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/one_to_one_relationship.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, Integer 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import relationship 7 | 8 | from eve_sqlalchemy.config import ResourceConfig 9 | 10 | from .. import BaseModel 11 | from . import ResourceConfigTestCase 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | class Parent(Base): 17 | id = Column(Integer, primary_key=True) 18 | child = relationship("Child", uselist=False, back_populates="parent") 19 | 20 | 21 | class Child(Base): 22 | id = Column(Integer, primary_key=True) 23 | parent_id = Column(Integer, ForeignKey('parent.id'), nullable=False) 24 | parent = relationship("Parent", back_populates="child") 25 | 26 | 27 | class TestOneToOneRelationship(ResourceConfigTestCase): 28 | """Test a basic One-To-One relationship in SQLAlchemy. 29 | 30 | The model definitions are taken from the official documentation: 31 | https://docs.sqlalchemy.org/en/rel_1_1/orm/basic_relationships.html#one-to-one 32 | """ 33 | 34 | def setUp(self): 35 | super(TestOneToOneRelationship, self).setUp() 36 | self._related_resource_configs = { 37 | Child: ('children', ResourceConfig(Child)), 38 | Parent: ('parents', ResourceConfig(Parent)) 39 | } 40 | 41 | def test_parent_projection(self): 42 | projection = self._render(Parent)['datasource']['projection'] 43 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'child': 1}) 44 | 45 | def test_child_projection(self): 46 | projection = self._render(Child)['datasource']['projection'] 47 | self.assertEqual(projection, {'_etag': 0, 'id': 1, 'parent': 1}) 48 | 49 | def test_parent_schema(self): 50 | schema = self._render(Parent)['schema'] 51 | self.assertIn('child', schema) 52 | self.assertEqual(schema['child'], { 53 | 'type': 'integer', 54 | 'coerce': int, 55 | 'data_relation': { 56 | 'resource': 'children', 57 | 'field': 'id' 58 | }, 59 | 'nullable': True 60 | }) 61 | self.assertNotIn('child_id', schema) 62 | 63 | def test_child_schema(self): 64 | schema = self._render(Child)['schema'] 65 | self.assertIn('parent', schema) 66 | self.assertEqual(schema['parent'], { 67 | 'type': 'integer', 68 | 'coerce': int, 69 | 'data_relation': { 70 | 'resource': 'parents', 71 | 'field': 'id' 72 | }, 73 | 'local_id_field': 'parent_id', 74 | 'nullable': False, 75 | 'required': True 76 | }) 77 | self.assertNotIn('parent_id', schema) 78 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/schema.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import sqlalchemy as sa 5 | import sqlalchemy.dialects.postgresql as postgresql 6 | from sqlalchemy import Column, types 7 | from sqlalchemy.ext.declarative import declarative_base 8 | 9 | from .. import BaseModel 10 | from . import ResourceConfigTestCase 11 | 12 | Base = declarative_base(cls=BaseModel) 13 | 14 | 15 | class SomeModel(Base): 16 | id = Column(types.Integer, primary_key=True) 17 | a_boolean = Column(types.Boolean, nullable=False) 18 | a_date = Column(types.Date, unique=True) 19 | a_datetime = Column(types.DateTime) 20 | a_float = Column(types.Float) 21 | a_json = Column(types.JSON) 22 | another_json = Column(postgresql.JSON) 23 | a_pickle = Column(types.PickleType) 24 | a_string = Column(types.String(42), default='H2G2') 25 | _internal = Column(types.Integer) 26 | a_server_default_col = Column(types.Integer, server_default=sa.text('0')) 27 | 28 | 29 | class StringPK(Base): 30 | id = Column(types.String, primary_key=True) 31 | 32 | 33 | class IntegerPKWithoutAI(Base): 34 | id = Column(types.Integer, primary_key=True, autoincrement=False) 35 | 36 | 37 | class TestSchema(ResourceConfigTestCase): 38 | 39 | def test_all_columns_appear_in_schema(self): 40 | schema = self._render(SomeModel)['schema'] 41 | self.assertEqual(set(schema.keys()), 42 | set(('id', 'a_boolean', 'a_date', 'a_datetime', 43 | 'a_float', 'a_json', 'another_json', 44 | 'a_pickle', 'a_string', 'a_server_default_col'))) 45 | 46 | def test_field_types(self): 47 | schema = self._render(SomeModel)['schema'] 48 | self.assertEqual(schema['id']['type'], 'integer') 49 | self.assertEqual(schema['a_boolean']['type'], 'boolean') 50 | self.assertEqual(schema['a_date']['type'], 'datetime') 51 | self.assertEqual(schema['a_datetime']['type'], 'datetime') 52 | self.assertEqual(schema['a_float']['type'], 'float') 53 | self.assertEqual(schema['a_json']['type'], 'json') 54 | self.assertEqual(schema['another_json']['type'], 'json') 55 | self.assertNotIn('type', schema['a_pickle']) 56 | 57 | def test_nullable(self): 58 | schema = self._render(SomeModel)['schema'] 59 | self.assertTrue(schema['a_float']['nullable']) 60 | self.assertFalse(schema['id']['nullable']) 61 | self.assertFalse(schema['a_boolean']['nullable']) 62 | 63 | def test_required(self): 64 | schema = self._render(SomeModel)['schema'] 65 | self.assertTrue(schema['a_boolean']['required']) 66 | self.assertFalse(schema['a_float']['required']) 67 | # As the primary key is an integer column, it will have 68 | # autoincrement='auto' per default (see SQLAlchemy docs for 69 | # details). As such, it is not required. 70 | self.assertFalse(schema['id']['required']) 71 | self.assertFalse(schema['a_server_default_col']['required']) 72 | 73 | def test_required_string_pk(self): 74 | schema = self._render(StringPK)['schema'] 75 | self.assertTrue(schema['id']['required']) 76 | 77 | def test_required_integer_pk_without_autoincrement(self): 78 | schema = self._render(IntegerPKWithoutAI)['schema'] 79 | self.assertTrue(schema['id']['required']) 80 | 81 | def test_unique(self): 82 | schema = self._render(SomeModel)['schema'] 83 | self.assertTrue(schema['id']['unique']) 84 | self.assertTrue(schema['a_date']['unique']) 85 | self.assertNotIn('unique', schema['a_float']) 86 | 87 | def test_maxlength(self): 88 | schema = self._render(SomeModel)['schema'] 89 | self.assertEqual(schema['a_string']['maxlength'], 42) 90 | self.assertNotIn('maxlength', schema['id']) 91 | 92 | def test_default(self): 93 | schema = self._render(SomeModel)['schema'] 94 | self.assertEqual(schema['a_string']['default'], 'H2G2') 95 | self.assertNotIn('default', schema['a_boolean']) 96 | 97 | def test_coerce(self): 98 | schema = self._render(SomeModel)['schema'] 99 | self.assertEqual(schema['id']['coerce'], int) 100 | schema = self._render(IntegerPKWithoutAI)['schema'] 101 | self.assertEqual(schema['id']['coerce'], int) 102 | schema = self._render(StringPK)['schema'] 103 | self.assertNotIn('coerce', schema['id']) 104 | 105 | def test_default_is_not_unset(self): 106 | self._render(SomeModel) 107 | self.assertIsNotNone(SomeModel.a_string.default) 108 | self.assertEqual(SomeModel.a_string.default.arg, 'H2G2') 109 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/resourceconfig/self_referential_relationship.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, ForeignKey, String 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import relationship 7 | 8 | from eve_sqlalchemy.config import ResourceConfig 9 | 10 | from .. import BaseModel 11 | from . import ResourceConfigTestCase 12 | 13 | Base = declarative_base(cls=BaseModel) 14 | 15 | 16 | class Node(Base): 17 | name = Column(String(16), primary_key=True) 18 | parent_node_name = Column(String(16), ForeignKey('node.name')) 19 | parent_node = relationship("Node", remote_side=[name]) 20 | 21 | 22 | class TestSelfReferentialRelationship(ResourceConfigTestCase): 23 | 24 | def setUp(self): 25 | super(TestSelfReferentialRelationship, self).setUp() 26 | self._related_resource_configs = { 27 | Node: ('nodes', ResourceConfig(Node)) 28 | } 29 | 30 | def test_node_projection(self): 31 | projection = self._render(Node)['datasource']['projection'] 32 | self.assertEqual(projection, {'_etag': 0, 'name': 1, 'parent_node': 1}) 33 | 34 | def test_node_schema(self): 35 | schema = self._render(Node)['schema'] 36 | self.assertIn('parent_node', schema) 37 | self.assertNotIn('parent_node_name', schema) 38 | self.assertEqual(schema['parent_node'], { 39 | 'type': 'string', 40 | 'data_relation': { 41 | 'resource': 'nodes', 42 | 'field': 'name' 43 | }, 44 | 'local_id_field': 'parent_node_name', 45 | 'nullable': True 46 | }) 47 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/delete.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import pytest 5 | from eve import ETAG 6 | from eve.tests.methods import delete as eve_delete_tests 7 | from eve.tests.utils import DummyEvent 8 | 9 | from eve_sqlalchemy.tests import TestBase 10 | 11 | 12 | class TestDelete(eve_delete_tests.TestDelete, TestBase): 13 | 14 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 15 | def test_delete_from_resource_endpoint_write_concern(self): 16 | pass 17 | 18 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 19 | def test_delete_write_concern(self): 20 | pass 21 | 22 | def test_delete_subresource(self): 23 | # Eve test uses the Mongo layer directly. 24 | # TODO: Fix directly in Eve and remove this override 25 | 26 | with self.app.app_context(): 27 | # create random contact 28 | fake_contact = self.random_contacts(1)[0] 29 | fake_contact['username'] = 'foo' 30 | fake_contact_id = self.app.data.insert('contacts', 31 | [fake_contact])[0] 32 | 33 | # grab parent collection count; we will use this later to make sure 34 | # we didn't delete all the users in the database. We add one extra 35 | # invoice to make sure that the actual count will never be 1 (which 36 | # would invalidate the test) 37 | self.app.data.insert('invoices', [{'inv_number': 1}]) 38 | 39 | response, status = self.get('invoices') 40 | invoices = len(response[self.app.config['ITEMS']]) 41 | 42 | with self.app.app_context(): 43 | # update first invoice to reference the new contact 44 | self.app.data.update('invoices', self.invoice_id, 45 | {'person': fake_contact_id}, None) 46 | 47 | # verify that the only document retrieved is referencing the correct 48 | # parent document 49 | response, status = self.get('users/%s/invoices' % fake_contact_id) 50 | person_id = response[self.app.config['ITEMS']][0]['person'] 51 | self.assertEqual(person_id, fake_contact_id) 52 | 53 | # delete all documents at the sub-resource endpoint 54 | response, status = self.delete('users/%s/invoices' % fake_contact_id) 55 | self.assert204(status) 56 | 57 | # verify that the no documents are left at the sub-resource endpoint 58 | response, status = self.get('users/%s/invoices' % fake_contact_id) 59 | self.assertEqual(len(response['_items']), 0) 60 | 61 | # verify that other documents in the invoices collection have not neen 62 | # deleted 63 | response, status = self.get('invoices') 64 | self.assertEqual(len(response['_items']), invoices - 1) 65 | 66 | def test_delete_subresource_item(self): 67 | # Eve test uses the Mongo layer directly. 68 | # TODO: Fix directly in Eve and remove this override 69 | 70 | with self.app.app_context(): 71 | # create random contact 72 | fake_contact = self.random_contacts(1)[0] 73 | fake_contact['username'] = 'foo' 74 | fake_contact_id = self.app.data.insert('contacts', 75 | [fake_contact])[0] 76 | 77 | # update first invoice to reference the new contact 78 | self.app.data.update('invoices', self.invoice_id, 79 | {'person': fake_contact_id}, None) 80 | 81 | # GET all invoices by new contact 82 | response, status = self.get('users/%s/invoices/%s' % 83 | (fake_contact_id, self.invoice_id)) 84 | etag = response[ETAG] 85 | 86 | headers = [('If-Match', etag)] 87 | response, status = self.delete('users/%s/invoices/%s' % 88 | (fake_contact_id, self.invoice_id), 89 | headers=headers) 90 | self.assert204(status) 91 | 92 | 93 | class TestDeleteEvents(eve_delete_tests.TestDeleteEvents, TestBase): 94 | 95 | def test_on_delete_item(self): 96 | devent = DummyEvent(self.before_delete) 97 | self.app.on_delete_item += devent 98 | self.delete_item() 99 | self.assertEqual('contacts', devent.called[0]) 100 | id_field = self.domain['contacts']['id_field'] 101 | # Eve test casts devent.called[1][id_field] to string, which may be 102 | # appropriate for ObjectIds, but not for integer ids. 103 | # TODO: Fix directly in Eve and remove this override 104 | self.assertEqual(self.item_id, devent.called[1][id_field]) 105 | 106 | def test_on_delete_item_contacts(self): 107 | devent = DummyEvent(self.before_delete) 108 | self.app.on_delete_item_contacts += devent 109 | self.delete_item() 110 | id_field = self.domain['contacts']['id_field'] 111 | # Eve test casts devent.called[1][id_field] to string, which may be 112 | # appropriate for ObjectIds, but not for integer ids. 113 | # TODO: Fix directly in Eve and remove this override 114 | self.assertEqual(self.item_id, devent.called[0][id_field]) 115 | 116 | def test_on_deleted_item(self): 117 | devent = DummyEvent(self.after_delete) 118 | self.app.on_deleted_item += devent 119 | self.delete_item() 120 | self.assertEqual('contacts', devent.called[0]) 121 | id_field = self.domain['contacts']['id_field'] 122 | # Eve test casts devent.called[1][id_field] to string, which may be 123 | # appropriate for ObjectIds, but not for integer ids. 124 | # TODO: Fix directly in Eve and remove this override 125 | self.assertEqual(self.item_id, devent.called[1][id_field]) 126 | 127 | def test_on_deleted_item_contacts(self): 128 | devent = DummyEvent(self.after_delete) 129 | self.app.on_deleted_item_contacts += devent 130 | self.delete_item() 131 | id_field = self.domain['contacts']['id_field'] 132 | # Eve test casts devent.called[1][id_field] to string, which may be 133 | # appropriate for ObjectIds, but not for integer ids. 134 | # TODO: Fix directly in Eve and remove this override 135 | self.assertEqual(self.item_id, devent.called[0][id_field]) 136 | 137 | def before_delete(self): 138 | # Eve method uses the Mongo layer directly. 139 | # TODO: Fix directly in Eve and remove this override 140 | return self.app.data.find_one_raw( 141 | self.known_resource, self.item_id) is not None 142 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve-sqlalchemy/0d5fda3d4739115be84a9bcecdb141f09265a397/eve_sqlalchemy/tests/integration/__init__.py -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/collection_class_set.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import ( 5 | Column, DateTime, ForeignKey, Integer, String, Table, func, 6 | ) 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.orm import relationship 9 | 10 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 11 | from eve_sqlalchemy.tests import TestMinimal 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class BaseModel(Base): 17 | __abstract__ = True 18 | _created = Column(DateTime, default=func.now()) 19 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 20 | _etag = Column(String(40)) 21 | 22 | 23 | association_table = Table( 24 | 'association', Base.metadata, 25 | Column('left_id', Integer, ForeignKey('left.id')), 26 | Column('right_id', Integer, ForeignKey('right.id')) 27 | ) 28 | 29 | 30 | class Parent(BaseModel): 31 | __tablename__ = 'left' 32 | id = Column(Integer, primary_key=True) 33 | children = relationship("Child", secondary=association_table, 34 | backref="parents", collection_class=set) 35 | 36 | 37 | class Child(BaseModel): 38 | __tablename__ = 'right' 39 | id = Column(Integer, primary_key=True) 40 | 41 | 42 | SETTINGS = { 43 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///', 44 | 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 45 | 'RESOURCE_METHODS': ['GET', 'POST', 'DELETE'], 46 | 'ITEM_METHODS': ['GET', 'PATCH', 'DELETE', 'PUT'], 47 | 'DOMAIN': DomainConfig({ 48 | 'parents': ResourceConfig(Parent), 49 | 'children': ResourceConfig(Child), 50 | }).render() 51 | } 52 | 53 | 54 | class TestCollectionClassSet(TestMinimal): 55 | 56 | def setUp(self, url_converters=None): 57 | super(TestCollectionClassSet, self).setUp( 58 | SETTINGS, url_converters, Base) 59 | 60 | def bulk_insert(self): 61 | self.app.data.insert('children', [{'id': k} for k in range(1, 5)]) 62 | self.app.data.insert('parents', [ 63 | {'id': 1, 'children': set([1, 2])}, 64 | {'id': 2, 'children': set()}]) 65 | 66 | def test_get_parents(self): 67 | response, status = self.get('parents') 68 | self.assert200(status) 69 | self.assertEqual(len(response['_items']), 2) 70 | self.assertEqual(response['_items'][0]['children'], [1, 2]) 71 | self.assertEqual(response['_items'][1]['children'], []) 72 | 73 | def test_post_parent(self): 74 | _, status = self.post('parents', {'id': 3, 'children': [3]}) 75 | self.assert201(status) 76 | response, status = self.get('parents', item=3) 77 | self.assert200(status) 78 | self.assertEqual(response['children'], [3]) 79 | 80 | def test_patch_parent(self): 81 | etag = self.get('parents', item=2)[0]['_etag'] 82 | _, status = self.patch('/parents/2', {'children': [3, 4]}, 83 | [('If-Match', etag)]) 84 | self.assert200(status) 85 | response, _ = self.get('parents', item=2) 86 | self.assertEqual(response['children'], [3, 4]) 87 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/get_none_values.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import Column, DateTime, Integer, String, func 5 | from sqlalchemy.ext.declarative import declarative_base 6 | 7 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 8 | from eve_sqlalchemy.tests import TestMinimal 9 | 10 | Base = declarative_base() 11 | 12 | 13 | class BaseModel(Base): 14 | __abstract__ = True 15 | _created = Column(DateTime, default=func.now()) 16 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 17 | _etag = Column(String(40)) 18 | 19 | 20 | class Node(BaseModel): 21 | __tablename__ = 'node' 22 | id = Column(Integer, primary_key=True) 23 | none_field = Column(Integer) 24 | 25 | 26 | SETTINGS = { 27 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///', 28 | 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 29 | 'RESOURCE_METHODS': ['GET', 'POST', 'DELETE'], 30 | 'ITEM_METHODS': ['GET', 'PATCH', 'DELETE', 'PUT'], 31 | 'DOMAIN': DomainConfig({ 32 | 'nodes': ResourceConfig(Node), 33 | }).render() 34 | } 35 | 36 | 37 | class TestGetNoneValues(TestMinimal): 38 | 39 | def setUp(self, url_converters=None): 40 | super(TestGetNoneValues, self).setUp(SETTINGS, url_converters, Base) 41 | 42 | def bulk_insert(self): 43 | self.app.data.insert('nodes', [{'id': k} for k in range(1, 5)]) 44 | 45 | def test_get_can_return_none_value(self): 46 | response, status = self.get('nodes/1') 47 | self.assert200(status) 48 | self.assertIn('none_field', response) 49 | self.assertIsNone(response['none_field']) 50 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/many_to_many.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from eve_sqlalchemy.examples.many_to_many import settings 5 | from eve_sqlalchemy.examples.many_to_many.domain import Base 6 | from eve_sqlalchemy.tests import TestMinimal 7 | 8 | SETTINGS = vars(settings) 9 | 10 | 11 | class TestManyToMany(TestMinimal): 12 | 13 | def setUp(self, url_converters=None): 14 | super(TestManyToMany, self).setUp(SETTINGS, url_converters, Base) 15 | 16 | def bulk_insert(self): 17 | self.app.data.insert('children', [{'id': k} for k in range(1, 5)]) 18 | self.app.data.insert('parents', [ 19 | {'id': 1, 'children': [1, 2]}, 20 | {'id': 2, 'children': [1, 3]}, 21 | {'id': 3, 'children': []}]) 22 | 23 | def test_get_related_children_with_where(self): 24 | response, status = self.get('children', '?where={"parents": 1}') 25 | self.assert200(status) 26 | children = response['_items'] 27 | self.assertEqual([c['id'] for c in children], [1, 2]) 28 | 29 | def test_get_related_parents_with_where(self): 30 | response, status = self.get('parents', '?where={"children": 1}') 31 | self.assert200(status) 32 | parents = response['_items'] 33 | self.assertEqual([p['id'] for p in parents], [1, 2]) 34 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/many_to_one.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from eve_sqlalchemy.examples.many_to_one import settings 5 | from eve_sqlalchemy.examples.many_to_one.domain import Base 6 | from eve_sqlalchemy.tests import TestMinimal 7 | 8 | SETTINGS = vars(settings) 9 | 10 | 11 | class TestManyToOne(TestMinimal): 12 | 13 | def setUp(self, url_converters=None): 14 | super(TestManyToOne, self).setUp(SETTINGS, url_converters, Base) 15 | 16 | def bulk_insert(self): 17 | self.app.data.insert('children', [{'id': k} for k in range(1, 5)]) 18 | self.app.data.insert('parents', [ 19 | {'id': 1, 'child': 1}, 20 | {'id': 2, 'child': 1}, 21 | {'id': 3}]) 22 | 23 | def test_get_related_children_with_where(self): 24 | response, status = self.get('children', '?where={"parents": 1}') 25 | self.assert200(status) 26 | children = response['_items'] 27 | self.assertEqual([c['id'] for c in children], [1]) 28 | 29 | def test_get_related_parents_with_where(self): 30 | response, status = self.get('parents', '?where={"child": 1}') 31 | self.assert200(status) 32 | parents = response['_items'] 33 | self.assertEqual([p['id'] for p in parents], [1, 2]) 34 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/nested_relations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from sqlalchemy import ( 5 | Column, DateTime, ForeignKey, Integer, String, Table, func, 6 | ) 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.orm import relationship 9 | 10 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 11 | from eve_sqlalchemy.tests import TestMinimal 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class BaseModel(Base): 17 | __abstract__ = True 18 | _created = Column(DateTime, default=func.now()) 19 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 20 | _etag = Column(String(40)) 21 | 22 | 23 | invoice_item = Table( 24 | 'invoice_item', Base.metadata, 25 | Column('invoice_id', Integer, ForeignKey('invoice.id')), 26 | Column('item_id', Integer, ForeignKey('item.id')), 27 | Column('quantity', Integer) 28 | ) 29 | 30 | 31 | class Address(BaseModel): 32 | __tablename__ = 'address' 33 | id = Column(Integer, primary_key=True) 34 | city = Column(String(64)) 35 | 36 | 37 | class Invoice(BaseModel): 38 | __tablename__ = 'invoice' 39 | id = Column(Integer, primary_key=True) 40 | recipient_address_id = Column(Integer, ForeignKey('address.id'), 41 | nullable=False) 42 | recipient_address = relationship('Address', uselist=False) 43 | items = relationship('Item', secondary=invoice_item, 44 | backref='invoices') 45 | 46 | 47 | class Item(BaseModel): 48 | __tablename__ = 'item' 49 | id = Column(Integer, primary_key=True) 50 | name = Column(String(256)) 51 | 52 | 53 | SETTINGS = { 54 | 'SQLALCHEMY_DATABASE_URI': 'sqlite:///', 55 | 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 56 | 'RESOURCE_METHODS': ['GET', 'POST', 'DELETE'], 57 | 'ITEM_METHODS': ['GET', 'PATCH', 'DELETE', 'PUT'], 58 | 'DOMAIN': DomainConfig({ 59 | 'addresses': ResourceConfig(Address), 60 | 'invoices': ResourceConfig(Invoice), 61 | 'items': ResourceConfig(Item), 62 | }).render() 63 | } 64 | 65 | 66 | class TestNestedRelations(TestMinimal): 67 | 68 | def setUp(self, url_converters=None): 69 | super(TestNestedRelations, self).setUp(SETTINGS, url_converters, Base) 70 | 71 | def bulk_insert(self): 72 | self.app.data.insert('addresses', [ 73 | {'id': 1, 'city': 'Berlin'}, 74 | {'id': 2, 'city': 'Paris'}, 75 | ]) 76 | self.app.data.insert('items', [ 77 | {'id': 1, 'name': 'Flux capacitor'}, 78 | {'id': 2, 'name': 'Excalibur'}, 79 | {'id': 3, 'name': 'Rice crackers'}, 80 | {'id': 4, 'name': 'Empty box'}, 81 | ]) 82 | self.app.data.insert('invoices', [ 83 | {'id': 1, 'recipient_address': 1, 'items': [1, 2]}, 84 | {'id': 2, 'recipient_address': 2, 'items': [2]}, 85 | {'id': 3, 'recipient_address': 1, 'items': [2, 3]}, 86 | {'id': 4, 'recipient_address': 2, 'items': [1]}, 87 | ]) 88 | 89 | def test_get_items_by_invoices_recipient_addresses(self): 90 | self._assert_queried_ids( 91 | 'items', '?where={"invoices.recipient_address": 1}', [1, 2, 3]) 92 | self._assert_queried_ids( 93 | 'items', '?where={"invoices.recipient_address": 2}', [2, 1]) 94 | 95 | def test_get_items_by_invoices_recipient_addresses_pythonic_syntax(self): 96 | self._assert_queried_ids( 97 | 'items', '?where=invoices.recipient_address==1', [1, 2, 3]) 98 | self._assert_queried_ids( 99 | 'items', '?where=invoices.recipient_address==2', [2, 1]) 100 | 101 | def test_get_items_by_invoices_recipient_addresses_city(self): 102 | self._assert_queried_ids( 103 | 'items', '?where={"invoices.recipient_address.city": "Berlin"}', 104 | [1, 2, 3]) 105 | self._assert_queried_ids( 106 | 'items', '?where={"invoices.recipient_address.city": "Paris"}', 107 | [2, 1]) 108 | 109 | def test_get_invoices_by_item_id(self): 110 | self._assert_queried_ids('invoices', '?where={"items": 1}', [1, 4]) 111 | 112 | def test_sort_invoices_by_recipient_address_city(self): 113 | self._assert_queried_ids( 114 | 'invoices', '?sort=recipient_address.city', [1, 3, 2, 4]) 115 | self._assert_queried_ids( 116 | 'invoices', '?sort=-recipient_address.city', [2, 4, 1, 3]) 117 | 118 | def _assert_queried_ids(self, resource, query, ids): 119 | response, status = self.get(resource, query) 120 | self.assert200(status) 121 | items = response['_items'] 122 | self.assertEqual([i['id'] for i in items], ids) 123 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/one_to_many.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from eve_sqlalchemy.examples.one_to_many import settings 5 | from eve_sqlalchemy.examples.one_to_many.domain import Base 6 | from eve_sqlalchemy.tests import TestMinimal 7 | 8 | SETTINGS = vars(settings) 9 | 10 | 11 | class TestOneToMany(TestMinimal): 12 | 13 | def setUp(self, url_converters=None): 14 | super(TestOneToMany, self).setUp(SETTINGS, url_converters, Base) 15 | 16 | def bulk_insert(self): 17 | self.app.data.insert('children', [{'id': k} for k in range(1, 5)]) 18 | self.app.data.insert('parents', [ 19 | {'id': 1, 'children': [1, 2]}, 20 | {'id': 2, 'children': []}, 21 | {'id': 3, 'children': [4]}]) 22 | 23 | def test_get_related_children_with_where(self): 24 | response, status = self.get('children', '?where={"parent": 1}') 25 | self.assert200(status) 26 | children = response['_items'] 27 | self.assertEqual([c['id'] for c in children], [1, 2]) 28 | 29 | def test_get_related_parents_with_where(self): 30 | response, status = self.get('parents', '?where={"children": 2}') 31 | self.assert200(status) 32 | parents = response['_items'] 33 | self.assertEqual([p['id'] for p in parents], [1]) 34 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/patch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import pytest 5 | from eve import ETAG 6 | from eve.tests.methods import patch as eve_patch_tests 7 | 8 | from eve_sqlalchemy.tests import TestBase 9 | 10 | 11 | class TestPatch(eve_patch_tests.TestPatch, TestBase): 12 | 13 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 14 | def test_patch_objectid(self): 15 | pass 16 | 17 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 18 | def test_patch_null_objectid(self): 19 | pass 20 | 21 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 22 | def test_patch_defaults(self): 23 | pass 24 | 25 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 26 | def test_patch_defaults_with_post_override(self): 27 | pass 28 | 29 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 30 | def test_patch_write_concern_fail(self): 31 | pass 32 | 33 | def test_patch_missing_standard_date_fields(self): 34 | # Eve test uses the Mongo layer directly. 35 | # TODO: Fix directly in Eve and remove this override 36 | 37 | with self.app.app_context(): 38 | contacts = self.random_contacts(1, False) 39 | ref = 'test_patch_missing_date_f' 40 | contacts[0]['ref'] = ref 41 | self.app.data.insert('contacts', contacts) 42 | 43 | # now retrieve same document via API and get its etag, which is 44 | # supposed to be computed on default DATE_CREATED and LAST_UPDATAED 45 | # values. 46 | response, status = self.get(self.known_resource, item=ref) 47 | etag = response[ETAG] 48 | _id = response['_id'] 49 | 50 | # attempt a PATCH with the new etag. 51 | field = "ref" 52 | test_value = "X234567890123456789012345" 53 | changes = {field: test_value} 54 | _, status = self.patch('%s/%s' % (self.known_resource_url, _id), 55 | data=changes, headers=[('If-Match', etag)]) 56 | self.assert200(status) 57 | 58 | def test_patch_subresource(self): 59 | # Eve test uses the Mongo layer directly. 60 | # TODO: Fix directly in Eve and remove this override 61 | 62 | with self.app.app_context(): 63 | # create random contact 64 | fake_contact = self.random_contacts(1) 65 | fake_contact_id = self.app.data.insert('contacts', fake_contact)[0] 66 | 67 | # update first invoice to reference the new contact 68 | self.app.data.update('invoices', self.invoice_id, 69 | {'person': fake_contact_id}, None) 70 | 71 | # GET all invoices by new contact 72 | response, status = self.get('users/%s/invoices/%s' % 73 | (fake_contact_id, self.invoice_id)) 74 | etag = response[ETAG] 75 | 76 | data = {"inv_number": "new_number"} 77 | headers = [('If-Match', etag)] 78 | response, status = self.patch('users/%s/invoices/%s' % 79 | (fake_contact_id, self.invoice_id), 80 | data=data, headers=headers) 81 | self.assert200(status) 82 | self.assertPatchResponse(response, self.invoice_id, 'peopleinvoices') 83 | 84 | @pytest.mark.xfail(True, run=False, reason='not implemented yet') 85 | def test_patch_nested_document_not_overwritten(self): 86 | pass 87 | 88 | @pytest.mark.xfail(True, run=False, reason='not implemented yet') 89 | def test_patch_nested_document_nullable_missing(self): 90 | pass 91 | 92 | def test_patch_dependent_field_on_origin_document(self): 93 | """ Test that when patching a field which is dependent on another and 94 | this other field is not provided with the patch but is still present 95 | on the target document, the patch will be accepted. See #363. 96 | """ 97 | # Eve remove the default-setting on 'dependency_field1', which we 98 | # cannot do easily with SQLAlchemy. 99 | # TODO: Fix directly in Eve and remove this override. 100 | 101 | # this will fail as dependent field is missing even in the 102 | # document we are trying to update. 103 | schema = self.domain['contacts']['schema'] 104 | schema['dependency_field2']['dependencies'] = \ 105 | ['dependency_field1_without_default'] 106 | changes = {'dependency_field2': 'value'} 107 | r, status = self.patch(self.item_id_url, data=changes, 108 | headers=[('If-Match', self.item_etag)]) 109 | self.assert422(status) 110 | 111 | # update the stored document by adding dependency field. 112 | changes = {'dependency_field1_without_default': 'value'} 113 | r, status = self.patch(self.item_id_url, data=changes, 114 | headers=[('If-Match', self.item_etag)]) 115 | self.assert200(status) 116 | 117 | # now the field2 update will be accepted as the dependency field is 118 | # present in the stored document already. 119 | etag = r['_etag'] 120 | changes = {'dependency_field2': 'value'} 121 | r, status = self.patch(self.item_id_url, data=changes, 122 | headers=[('If-Match', etag)]) 123 | self.assert200(status) 124 | 125 | def test_id_field_in_document_fails(self): 126 | # Eve test uses ObjectId as id. 127 | self.app.config['IF_MATCH'] = False 128 | id_field = self.domain[self.known_resource]['id_field'] 129 | data = {id_field: 424242} 130 | r, status = self.patch(self.item_id_url, data=data) 131 | self.assert400(status) 132 | self.assertTrue('immutable' in r['_error']['message']) 133 | 134 | 135 | class TestEvents(eve_patch_tests.TestEvents, TestBase): 136 | 137 | def before_update(self): 138 | # Eve test code uses mongo layer directly. 139 | # TODO: Fix directly in Eve and remove this override 140 | contact = self.app.data.find_one_raw(self.known_resource, self.item_id) 141 | return contact['ref'] == self.item_name 142 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/put.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import pytest 5 | import six 6 | from eve import ETAG, STATUS 7 | from eve.tests.methods import put as eve_put_tests 8 | 9 | from eve_sqlalchemy.tests import TestBase 10 | 11 | 12 | class TestPut(eve_put_tests.TestPut, TestBase): 13 | 14 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 15 | def test_put_dbref_subresource(self): 16 | pass 17 | 18 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 19 | def test_allow_unknown(self): 20 | pass 21 | 22 | def test_put_x_www_form_urlencoded_number_serialization(self): 23 | # Eve test manipulates schema and removes required constraint on 'ref'. 24 | # We decided to include 'ref' as it is not easy to manipulate 25 | # nullable-constraints during runtime. 26 | field = 'anumber' 27 | test_value = 41 28 | changes = {field: test_value, 29 | 'ref': 'test_put_x_www_num_ser_12'} 30 | headers = [('If-Match', self.item_etag)] 31 | r, status = self.parse_response(self.test_client.put( 32 | self.item_id_url, data=changes, headers=headers)) 33 | self.assert200(status) 34 | self.assertTrue('OK' in r[STATUS]) 35 | 36 | def test_put_referential_integrity_list(self): 37 | data = {"invoicing_contacts": [self.item_id, self.unknown_item_id]} 38 | headers = [('If-Match', self.invoice_etag)] 39 | r, status = self.put(self.invoice_id_url, data=data, headers=headers) 40 | self.assertValidationErrorStatus(status) 41 | expected = ("value '%s' must exist in resource '%s', field '%s'" % 42 | (self.unknown_item_id, 'contacts', 43 | self.domain['contacts']['id_field'])) 44 | self.assertValidationError(r, {'invoicing_contacts': expected}) 45 | 46 | # Eve test posts a list with self.item_id twice, which can't be handled 47 | # for our case because we use (invoice_id, contact_id) as primary key 48 | # in the association table. 49 | data = {"invoicing_contacts": [self.item_id]} 50 | r, status = self.put(self.invoice_id_url, data=data, headers=headers) 51 | self.assert200(status) 52 | self.assertPutResponse(r, self.invoice_id, 'invoices') 53 | 54 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 55 | def test_put_write_concern_fail(self): 56 | pass 57 | 58 | def test_put_subresource(self): 59 | # Eve test uses mongo layer directly. 60 | self.app.config['BANDWIDTH_SAVER'] = False 61 | 62 | with self.app.app_context(): 63 | # create random contact 64 | fake_contact = self.random_contacts(1) 65 | fake_contact_id = self.app.data.insert('contacts', fake_contact)[0] 66 | # update first invoice to reference the new contact 67 | self.app.data.update('invoices', self.invoice_id, 68 | {'person': fake_contact_id}, None) 69 | 70 | # GET all invoices by new contact 71 | response, status = self.get('users/%s/invoices/%s' % 72 | (fake_contact_id, self.invoice_id)) 73 | etag = response[ETAG] 74 | 75 | data = {"inv_number": "new_number"} 76 | headers = [('If-Match', etag)] 77 | response, status = self.put('users/%s/invoices/%s' % 78 | (fake_contact_id, self.invoice_id), 79 | data=data, headers=headers) 80 | self.assert200(status) 81 | self.assertPutResponse(response, self.invoice_id, 'peopleinvoices') 82 | self.assertEqual(response.get('person'), fake_contact_id) 83 | 84 | def test_put_dependency_fields_with_default(self): 85 | # Eve test manipulates schema and removes required constraint on 'ref'. 86 | # We decided to include 'ref' as it is not easy to manipulate 87 | # nullable-constraints during runtime. 88 | field = "dependency_field2" 89 | test_value = "a value" 90 | changes = {field: test_value, 91 | 'ref': 'test_post_dep_with_def_12'} 92 | r = self.perform_put(changes) 93 | db_value = self.compare_put_with_get(field, r) 94 | self.assertEqual(db_value, test_value) 95 | 96 | def test_put_dependency_fields_with_wrong_value(self): 97 | # Eve test manipulates schema and removes required constraint on 'ref'. 98 | # We decided to include 'ref' as it is not easy to manipulate 99 | # nullable-constraints during runtime. 100 | r, status = self.put(self.item_id_url, 101 | data={'dependency_field3': 'value', 102 | 'ref': 'test_post_dep_wrong_fiel1'}, 103 | headers=[('If-Match', self.item_etag)]) 104 | self.assert422(status) 105 | r, status = self.put(self.item_id_url, 106 | data={'dependency_field1': 'value', 107 | 'dependency_field3': 'value', 108 | 'ref': 'test_post_dep_wrong_fiel2'}, 109 | headers=[('If-Match', self.item_etag)]) 110 | self.assert200(status) 111 | 112 | def test_put_creates_unexisting_document(self): 113 | # Eve test uses ObjectId as id. 114 | id = 424242 115 | url = '%s/%s' % (self.known_resource_url, id) 116 | id_field = self.domain[self.known_resource]['id_field'] 117 | changes = {"ref": "1234567890123456789012345"} 118 | r, status = self.put(url, data=changes) 119 | # 201 is a creation (POST) response 120 | self.assert201(status) 121 | # new document has id_field matching the PUT endpoint 122 | self.assertEqual(r[id_field], id) 123 | 124 | def test_put_returns_404_on_unexisting_document(self): 125 | # Eve test uses ObjectId as id. 126 | self.app.config['UPSERT_ON_PUT'] = False 127 | id = 424242 128 | url = '%s/%s' % (self.known_resource_url, id) 129 | changes = {"ref": "1234567890123456789012345"} 130 | r, status = self.put(url, data=changes) 131 | self.assert404(status) 132 | 133 | def test_put_creates_unexisting_document_with_url_as_id(self): 134 | # Eve test uses ObjectId as id. 135 | id = 424242 136 | url = '%s/%s' % (self.known_resource_url, id) 137 | id_field = self.domain[self.known_resource]['id_field'] 138 | changes = {"ref": "1234567890123456789012345", 139 | id_field: 848484} # mismatching id 140 | r, status = self.put(url, data=changes) 141 | # 201 is a creation (POST) response 142 | self.assert201(status) 143 | # new document has id_field matching the PUT endpoint 144 | # (eventual mismatching id_field in the payload is ignored/replaced) 145 | self.assertEqual(r[id_field], id) 146 | 147 | def test_put_creates_unexisting_document_fails_on_mismatching_id(self): 148 | # Eve test uses ObjectId as id. 149 | id = 424243 150 | id_field = self.domain[self.known_resource]['id_field'] 151 | changes = {"ref": "1234567890123456789012345", id_field: id} 152 | r, status = self.put(self.item_id_url, 153 | data=changes, 154 | headers=[('If-Match', self.item_etag)]) 155 | self.assert400(status) 156 | self.assertTrue('immutable' in r['_error']['message']) 157 | 158 | def compare_put_with_get(self, fields, put_response): 159 | # Eve methods checks for instance of str, which could be unicode for 160 | # Python 2. We use six.string_types instead. 161 | raw_r = self.test_client.get(self.item_id_url) 162 | r, status = self.parse_response(raw_r) 163 | self.assert200(status) 164 | # Since v0.7, ETag conform to RFC 7232-2.3 (see Eve#794) 165 | self.assertEqual(raw_r.headers.get('ETag')[1:-1], 166 | put_response[ETAG]) 167 | if isinstance(fields, six.string_types): 168 | return r[fields] 169 | else: 170 | return [r[field] for field in fields] 171 | 172 | 173 | class TestEvents(eve_put_tests.TestEvents, TestBase): 174 | 175 | def before_replace(self): 176 | # Eve test code uses mongo layer directly. 177 | # TODO: Fix directly in Eve and remove this override 178 | contact = self.app.data.find_one_raw(self.known_resource, self.item_id) 179 | return contact['ref'] == self.item_name 180 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import copy 5 | 6 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 7 | from eve_sqlalchemy.tests.test_sql_tables import ( 8 | Contacts, DisabledBulk, Empty, InternalTransactions, Invoices, Login, 9 | Payments, Products, 10 | ) 11 | 12 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' # %s' % db_filename 13 | SQLALCHEMY_TRACK_MODIFICATIONS = False 14 | 15 | RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] 16 | ITEM_METHODS = ['GET', 'PATCH', 'DELETE', 'PUT'] 17 | 18 | DOMAIN = DomainConfig({ 19 | 'disabled_bulk': ResourceConfig(DisabledBulk), 20 | 'contacts': ResourceConfig(Contacts), 21 | 'invoices': ResourceConfig(Invoices), 22 | # 'versioned_invoices': versioned_invoices, 23 | 'payments': ResourceConfig(Payments), 24 | 'empty': ResourceConfig(Empty), 25 | # 'restricted': user_restricted_access, 26 | # 'peoplesearches': users_searches, 27 | # 'companies': ResourceConfig(Companies), 28 | # 'departments': ResourceConfig(Departments), 29 | 'internal_transactions': ResourceConfig(InternalTransactions), 30 | # 'ids': ids, 31 | 'login': ResourceConfig(Login), 32 | 'products': ResourceConfig(Products, id_field='sku') 33 | }).render() 34 | 35 | DOMAIN['disabled_bulk'].update({ 36 | 'url': 'somebulkurl', 37 | 'item_title': 'bulkdisabled', 38 | 'bulk_enabled': False 39 | }) 40 | 41 | DOMAIN['contacts'].update({ 42 | 'url': 'arbitraryurl', 43 | 'cache_control': 'max-age=20,must-revalidate', 44 | 'cache_expires': 20, 45 | 'item_title': 'contact', 46 | 'additional_lookup': { 47 | 'url': r'regex("[\w]+")', 48 | 'field': 'ref' 49 | } 50 | }) 51 | DOMAIN['contacts']['datasource']['filter'] = 'username == ""' 52 | DOMAIN['contacts']['schema']['ref']['minlength'] = 25 53 | DOMAIN['contacts']['schema']['role'].update({ 54 | 'type': 'list', 55 | 'allowed': ["agent", "client", "vendor"], 56 | }) 57 | DOMAIN['contacts']['schema']['rows'].update({ 58 | 'type': 'list', 59 | 'schema': { 60 | 'type': 'dict', 61 | 'schema': { 62 | 'sku': {'type': 'string', 'maxlength': 10}, 63 | 'price': {'type': 'integer'}, 64 | }, 65 | }, 66 | }) 67 | DOMAIN['contacts']['schema']['alist'].update({ 68 | 'type': 'list', 69 | 'items': [{'type': 'string'}, {'type': 'integer'}, ] 70 | }) 71 | DOMAIN['contacts']['schema']['location'].update({ 72 | 'type': 'dict', 73 | 'schema': { 74 | 'address': {'type': 'string'}, 75 | 'city': {'type': 'string', 'required': True} 76 | }, 77 | }) 78 | DOMAIN['contacts']['schema']['dependency_field2'].update({ 79 | 'dependencies': ['dependency_field1'] 80 | }) 81 | DOMAIN['contacts']['schema']['dependency_field3'].update({ 82 | 'dependencies': {'dependency_field1': 'value'} 83 | }) 84 | DOMAIN['contacts']['schema']['read_only_field'].update({ 85 | 'readonly': True 86 | }) 87 | DOMAIN['contacts']['schema']['propertyschema_dict'].update({ 88 | 'type': 'dict', 89 | 'propertyschema': {'type': 'string', 'regex': '[a-z]+'} 90 | }) 91 | DOMAIN['contacts']['schema']['valueschema_dict'].update({ 92 | 'type': 'dict', 93 | 'valueschema': {'type': 'integer'} 94 | }) 95 | DOMAIN['contacts']['schema']['anumber'].update({ 96 | 'type': 'number' 97 | }) 98 | 99 | # DOMAIN['companies']['schema']['departments'].update({ 100 | # 'type': 'list', 101 | # 'schema': { 102 | # 'type': 'integer', 103 | # 'data_relation': {'resource': 'departments', 'field': '_id'}, 104 | # 'required': False 105 | # } 106 | # }) 107 | # DOMAIN['departments'].update({ 108 | # 'internal_resource': True, 109 | # }) 110 | 111 | users = copy.deepcopy(DOMAIN['contacts']) 112 | users['url'] = 'users' 113 | users['datasource'] = {'source': 'Contacts', 114 | 'projection': {'username': 1, 'ref': 1}, 115 | 'filter': 'username != ""'} 116 | users['resource_methods'] = ['DELETE', 'POST', 'GET'] 117 | users['item_title'] = 'user' 118 | users['additional_lookup']['field'] = 'username' 119 | DOMAIN['users'] = users 120 | 121 | users_overseas = copy.deepcopy(DOMAIN['contacts']) 122 | users_overseas['url'] = 'users/overseas' 123 | users_overseas['datasource'] = {'source': 'Contacts'} 124 | DOMAIN['users_overseas'] = users_overseas 125 | 126 | required_invoices = copy.deepcopy(DOMAIN['invoices']) 127 | required_invoices['schema']['person'].update({ 128 | 'required': True 129 | }) 130 | DOMAIN['required_invoices'] = required_invoices 131 | 132 | users_invoices = copy.deepcopy(DOMAIN['invoices']) 133 | users_invoices['url'] = 'users//invoices' 134 | users_invoices['datasource'] = {'source': 'Invoices'} 135 | DOMAIN['peopleinvoices'] = users_invoices 136 | 137 | users_required_invoices = copy.deepcopy(required_invoices) 138 | users_required_invoices['url'] = \ 139 | 'users//required_invoices' 140 | DOMAIN['peoplerequiredinvoices'] = users_required_invoices 141 | 142 | DOMAIN['payments'].update({ 143 | 'resource_methods': ['GET'], 144 | 'item_methods': ['GET'], 145 | }) 146 | 147 | DOMAIN['internal_transactions'].update({ 148 | 'resource_methods': ['GET'], 149 | 'item_methods': ['GET'], 150 | 'internal_resource': True 151 | }) 152 | 153 | DOMAIN['login']['datasource']['projection'].update({ 154 | 'password': 0 155 | }) 156 | 157 | child_products = copy.deepcopy(DOMAIN['products']) 158 | child_products['url'] = \ 159 | 'products//children' 160 | child_products['datasource'] = {'source': 'Products'} 161 | DOMAIN['child_products'] = child_products 162 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/test_sql_tables.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import hashlib 5 | 6 | from sqlalchemy import ( 7 | Boolean, Column, DateTime, Float, ForeignKey, Integer, LargeBinary, 8 | PickleType, String, Table, func, 9 | ) 10 | from sqlalchemy.ext.declarative import declarative_base 11 | from sqlalchemy.orm import relationship 12 | 13 | Base = declarative_base() 14 | 15 | 16 | class CommonColumns(Base): 17 | """ 18 | Master SQLAlchemy Model. All the SQL tables defined for the application 19 | should inherit from this class. It provides common columns such as 20 | _created, _updated and _id. 21 | 22 | WARNING: the _id column name does not respect Eve's setting for custom 23 | ID_FIELD. 24 | """ 25 | __abstract__ = True 26 | _created = Column(DateTime, default=func.now()) 27 | _updated = Column(DateTime, default=func.now(), onupdate=func.now()) 28 | _etag = Column(String) 29 | 30 | def __init__(self, *args, **kwargs): 31 | h = hashlib.sha1() 32 | self._etag = h.hexdigest() 33 | super(CommonColumns, self).__init__(*args, **kwargs) 34 | 35 | 36 | class DisabledBulk(CommonColumns): 37 | __tablename__ = 'disabled_bulk' 38 | _id = Column(Integer, primary_key=True) 39 | string_field = Column(String(25)) 40 | 41 | 42 | InvoicingContacts = Table( 43 | 'invoicing_contacts', Base.metadata, 44 | Column('invoice_id', Integer, ForeignKey('invoices._id'), 45 | primary_key=True), 46 | Column('contact_id', Integer, ForeignKey('contacts._id'), 47 | primary_key=True) 48 | ) 49 | 50 | 51 | class Contacts(CommonColumns): 52 | __tablename__ = 'contacts' 53 | _id = Column(Integer, primary_key=True) 54 | ref = Column(String(25), unique=True, nullable=False) 55 | media = Column(LargeBinary) 56 | prog = Column(Integer) 57 | role = Column(PickleType) 58 | rows = Column(PickleType) 59 | alist = Column(PickleType) 60 | location = Column(PickleType) 61 | born = Column(DateTime) 62 | tid = Column(Integer) 63 | title = Column(String(20), default='Mr.') 64 | # id_list 65 | # id_list_of_dict 66 | # id_list_fixed_len 67 | dependency_field1 = Column(String(25), default='default') 68 | dependency_field1_without_default = Column(String(25)) 69 | dependency_field2 = Column(String(25)) 70 | dependency_field3 = Column(String(25)) 71 | read_only_field = Column(String(25), default='default') 72 | # dict_with_read_only 73 | key1 = Column(String(25)) 74 | propertyschema_dict = Column(PickleType) 75 | valueschema_dict = Column(PickleType) 76 | aninteger = Column(Integer) 77 | afloat = Column(Float) 78 | anumber = Column(Float) 79 | username = Column(String(25), default='') 80 | # additional fields for Eve-SQLAlchemy tests 81 | abool = Column(Boolean) 82 | 83 | 84 | class Invoices(CommonColumns): 85 | __tablename__ = 'invoices' 86 | _id = Column(Integer, primary_key=True) 87 | inv_number = Column(String(25)) 88 | person_id = Column(Integer, ForeignKey('contacts._id')) 89 | person = relationship(Contacts) 90 | invoicing_contacts = relationship('Contacts', secondary=InvoicingContacts) 91 | 92 | 93 | class Empty(CommonColumns): 94 | __tablename__ = 'empty' 95 | _id = Column(Integer, primary_key=True) 96 | inv_number = Column(String(25)) 97 | 98 | 99 | DepartmentsContacts = Table( 100 | 'department_contacts', Base.metadata, 101 | Column('department_id', Integer, ForeignKey('departments._id'), 102 | primary_key=True), 103 | Column('contact_id', Integer, ForeignKey('contacts._id'), 104 | primary_key=True) 105 | ) 106 | 107 | CompaniesDepartments = Table( 108 | 'companies_departments', Base.metadata, 109 | Column('company_id', Integer, ForeignKey('companies._id'), 110 | primary_key=True), 111 | Column('department_id', Integer, ForeignKey('departments._id'), 112 | primary_key=True) 113 | ) 114 | 115 | 116 | class Departments(CommonColumns): 117 | __tablename__ = 'departments' 118 | _id = Column(Integer, primary_key=True) 119 | title = Column(String(25)) 120 | members = relationship('Contacts', secondary=DepartmentsContacts) 121 | 122 | 123 | class Companies(CommonColumns): 124 | __tablename__ = 'companies' 125 | _id = Column(Integer, primary_key=True) 126 | holding_id = Column(String(16), ForeignKey('companies._id')) 127 | holding = relationship('Companies', remote_side=[_id]) 128 | departments = relationship('Departments', secondary=CompaniesDepartments) 129 | 130 | 131 | class Payments(CommonColumns): 132 | __tablename__ = 'payments' 133 | _id = Column(Integer, primary_key=True) 134 | a_string = Column(String(10)) 135 | a_number = Column(Integer) 136 | 137 | 138 | class InternalTransactions(CommonColumns): 139 | __tablename__ = 'internal_transactions' 140 | _id = Column(Integer, primary_key=True) 141 | internal_string = Column(String(10)) 142 | internal_number = Column(Integer) 143 | 144 | 145 | class Login(CommonColumns): 146 | __tablename__ = 'login' 147 | _id = Column(Integer, primary_key=True) 148 | email = Column(String(255), nullable=False, unique=True) 149 | password = Column(String(32), nullable=False) 150 | 151 | 152 | class Products(CommonColumns): 153 | __tablename__ = 'products' 154 | sku = Column(String(16), primary_key=True) 155 | title = Column(String(32)) 156 | parent_product_sku = Column(String(16), ForeignKey('products.sku')) 157 | parent_product = relationship('Products', remote_side=[sku]) 158 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/test_validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import unittest 5 | 6 | import eve_sqlalchemy.validation 7 | 8 | 9 | class TestValidator(unittest.TestCase): 10 | def setUp(self): 11 | schemas = { 12 | 'a_json': { 13 | 'type': 'json', 14 | }, 15 | 'a_objectid': { 16 | 'type': 'objectid', 17 | }, 18 | } 19 | self.validator = eve_sqlalchemy.validation.ValidatorSQL(schemas) 20 | 21 | def test_type_json(self): 22 | self.validator.validate_update( 23 | {'a_json': None}, None) 24 | 25 | def test_type_objectid(self): 26 | self.validator.validate_update( 27 | {'a_objectid': ''}, None) 28 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import unittest 5 | 6 | import mock 7 | 8 | from eve_sqlalchemy.utils import extract_sort_arg 9 | 10 | 11 | class TestUtils(unittest.TestCase): 12 | 13 | def test_extract_sort_arg_standard(self): 14 | req = mock.Mock() 15 | req.sort = 'created_at,-name' 16 | self.assertEqual(extract_sort_arg(req), [['created_at'], ['name', -1]]) 17 | 18 | def test_extract_sort_arg_sqlalchemy(self): 19 | req = mock.Mock() 20 | req.sort = '[("created_at", -1, "nullstart")]' 21 | self.assertEqual(extract_sort_arg(req), 22 | [('created_at', -1, 'nullstart')]) 23 | 24 | def test_extract_sort_arg_null(self): 25 | req = mock.Mock() 26 | req.sort = '' 27 | self.assertEqual(extract_sort_arg(req), None) 28 | -------------------------------------------------------------------------------- /eve_sqlalchemy/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Helpers and utils functions 4 | 5 | :copyright: (c) 2013 by Andrew Mleczko and Tomasz Jezierski (Tefnet) 6 | :license: BSD, see LICENSE for more details. 7 | 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | import ast 12 | import copy 13 | import re 14 | 15 | from eve.utils import config 16 | from sqlalchemy.ext.declarative.api import DeclarativeMeta 17 | 18 | try: 19 | from collections.abc import Mapping, MutableSequence, Set 20 | except ImportError: 21 | from collections import Mapping, MutableSequence, Set 22 | 23 | 24 | def merge_dicts(*dicts): 25 | """ 26 | Given any number of dicts, shallow copy and merge into a new dict, 27 | precedence goes to key value pairs in latter dicts. 28 | 29 | Source: https://stackoverflow.com/q/38987 30 | """ 31 | result = {} 32 | for dictionary in dicts: 33 | result.update(dictionary) 34 | return result 35 | 36 | 37 | def dict_update(d, u): 38 | for k, v in u.items(): 39 | if isinstance(v, Mapping) and k in d and isinstance(d[k], Mapping): 40 | dict_update(d[k], v) 41 | elif k not in d: 42 | d[k] = u[k] 43 | 44 | 45 | def remove_none_values(dict_): 46 | for k, v in list(dict_.items()): 47 | if v is None: 48 | del(dict_[k]) 49 | 50 | 51 | def validate_filters(where, resource): 52 | allowed = config.DOMAIN[resource]['allowed_filters'] 53 | if '*' not in allowed: 54 | for filt in where: 55 | key = filt.left.key 56 | if key not in allowed: 57 | return "filter on '%s' not allowed" % key 58 | return None 59 | 60 | 61 | def sqla_object_to_dict(obj, fields): 62 | """ Creates a dict containing copies of the requested fields from the 63 | SQLAlchemy query result """ 64 | if config.LAST_UPDATED not in fields: 65 | fields.append(config.LAST_UPDATED) 66 | if config.DATE_CREATED not in fields: 67 | fields.append(config.DATE_CREATED) 68 | if config.ETAG not in fields \ 69 | and getattr(config, 'IF_MATCH', True): 70 | fields.append(config.ETAG) 71 | 72 | result = {} 73 | for field in map(lambda f: f.split('.', 1)[0], fields): 74 | try: 75 | val = obj.__getattribute__(field) 76 | 77 | # If association proxies are embedded, their values must be copied 78 | # since they are garbage collected when Eve try to encode the 79 | # response. 80 | if hasattr(val, 'copy'): 81 | val = val.copy() 82 | 83 | result[field] = _sanitize_value(val) 84 | except AttributeError: 85 | # Ignore if the requested field does not exist 86 | # (may be wrong embedding parameter) 87 | pass 88 | 89 | # We have to remove the ETAG if it's None so Eve will add it later again. 90 | if result.get(config.ETAG, False) is None: 91 | del(result[config.ETAG]) 92 | 93 | return result 94 | 95 | 96 | def _sanitize_value(value): 97 | if isinstance(value.__class__, DeclarativeMeta): 98 | return _get_id(value) 99 | elif isinstance(value, Mapping): 100 | return dict([(k, _sanitize_value(v)) for k, v in value.items()]) 101 | elif isinstance(value, MutableSequence): 102 | return [_sanitize_value(v) for v in value] 103 | elif isinstance(value, Set): 104 | return set(_sanitize_value(v) for v in value) 105 | else: 106 | return copy.copy(value) 107 | 108 | 109 | def _get_id(obj): 110 | resource = _get_resource(obj) 111 | return getattr(obj, config.DOMAIN[resource]['id_field']) 112 | 113 | 114 | def extract_sort_arg(req): 115 | if req.sort: 116 | if re.match(r'^[-,\w.]+$', req.sort): 117 | arg = [] 118 | for s in req.sort.split(','): 119 | if s.startswith('-'): 120 | arg.append([s[1:], -1]) 121 | else: 122 | arg.append([s]) 123 | return arg 124 | else: 125 | return ast.literal_eval(req.sort) 126 | else: 127 | return None 128 | 129 | 130 | def rename_relationship_fields_in_sort_args(model, sort): 131 | result = [] 132 | for t in sort: 133 | t = list(t) 134 | t[0] = rename_relationship_fields_in_str(model, t[0]) 135 | t = tuple(t) 136 | result.append(t) 137 | return result 138 | 139 | 140 | def rename_relationship_fields_in_dict(model, dict_): 141 | result = {} 142 | rename_mapping = _get_relationship_to_id_field_rename_mapping(model) 143 | for k, v in dict_.items(): 144 | if k in rename_mapping: 145 | result[rename_mapping[k]] = v 146 | else: 147 | result[k] = v 148 | return result 149 | 150 | 151 | def rename_relationship_fields_in_str(model, str_): 152 | rename_mapping = _get_relationship_to_id_field_rename_mapping(model) 153 | for k, v in rename_mapping.items(): 154 | str_ = re.sub(r'\b%s\b(?!\.)' % k, v, str_) 155 | return str_ 156 | 157 | 158 | def _get_relationship_to_id_field_rename_mapping(model): 159 | result = {} 160 | resource = _get_resource(model) 161 | schema = config.DOMAIN[resource]['schema'] 162 | for field, field_schema in schema.items(): 163 | if 'local_id_field' in field_schema: 164 | result[field] = field_schema['local_id_field'] 165 | return result 166 | 167 | 168 | def _get_resource(model_or_obj): 169 | if isinstance(model_or_obj.__class__, DeclarativeMeta): 170 | model = model_or_obj.__class__ 171 | else: 172 | model = model_or_obj 173 | for resource, settings in config.DOMAIN.items(): 174 | if settings['datasource']['source'] == model.__name__: 175 | return resource 176 | -------------------------------------------------------------------------------- /eve_sqlalchemy/validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module implements the SQLAlchemy Validator class, 4 | used to validate that objects incoming via POST/PATCH requests 5 | conform to the API domain. 6 | An extension of Cerberus Validator. 7 | 8 | :copyright: (c) 2013 by Nicola Iarocci, Andrew Mleczko and 9 | Tomasz Jezierski (Tefnet) 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | from __future__ import unicode_literals 13 | 14 | import collections 15 | import copy 16 | 17 | from cerberus import Validator 18 | from eve.utils import config, str_type 19 | from eve.versioning import ( 20 | get_data_version_relation_document, missing_version_field, 21 | ) 22 | from flask import current_app as app 23 | 24 | from eve_sqlalchemy.utils import dict_update, remove_none_values 25 | 26 | 27 | class ValidatorSQL(Validator): 28 | """ A cerberus.Validator subclass adding the `unique` constraint to 29 | Cerberus standard validation. For documentation please refer to the 30 | Validator class of the eve.io.mongo package. 31 | """ 32 | 33 | def __init__(self, schema, resource=None, **kwargs): 34 | self.resource = resource 35 | self._id = None 36 | self._original_document = None 37 | kwargs['transparent_schema_rules'] = True 38 | super(ValidatorSQL, self).__init__(schema, **kwargs) 39 | if resource: 40 | self.allow_unknown = config.DOMAIN[resource]['allow_unknown'] 41 | 42 | def validate_update(self, document, _id, original_document=None): 43 | self._id = _id 44 | self._original_document = original_document 45 | return super(ValidatorSQL, self).validate_update(document) 46 | 47 | def validate_replace(self, document, _id, original_document=None): 48 | self._id = _id 49 | self._original_document = original_document 50 | return self.validate(document) 51 | 52 | def _validate_unique(self, unique, field, value): 53 | if unique: 54 | id_field = config.DOMAIN[self.resource]['id_field'] 55 | if field == id_field and value == self._id: 56 | return 57 | elif field != id_field and self._id is not None: 58 | query = {field: value, id_field: '!= \'%s\'' % self._id} 59 | else: 60 | query = {field: value} 61 | if app.data.find_one(self.resource, None, **query): 62 | self._error(field, "value '%s' is not unique" % value) 63 | 64 | def _validate_data_relation(self, data_relation, field, value): 65 | if 'version' in data_relation and data_relation['version'] is True: 66 | value_field = data_relation['field'] 67 | version_field = app.config['VERSION'] 68 | 69 | # check value format 70 | if isinstance(value, dict) and value_field in value and \ 71 | version_field in value: 72 | resource_def = config.DOMAIN[data_relation['resource']] 73 | if resource_def['versioning'] is False: 74 | self._error(field, ("can't save a version with " 75 | "data_relation if '%s' isn't " 76 | "versioned") % 77 | data_relation['resource']) 78 | else: 79 | # support late versioning 80 | if value[version_field] == 0: 81 | # there is a chance this document hasn't been saved 82 | # since versioning was turned on 83 | search = missing_version_field(data_relation, value) 84 | else: 85 | search = get_data_version_relation_document( 86 | data_relation, value) 87 | if not search: 88 | self._error(field, ("value '%s' must exist in resource" 89 | " '%s', field '%s' at version " 90 | "'%s'.") % ( 91 | value[value_field], 92 | data_relation['resource'], 93 | data_relation['field'], 94 | value[version_field])) 95 | else: 96 | self._error(field, ("versioned data_relation must be a dict " 97 | "with fields '%s' and '%s'") % 98 | (value_field, version_field)) 99 | else: 100 | query = {data_relation['field']: value} 101 | if not app.data.find_one(data_relation['resource'], None, **query): 102 | self._error(field, ("value '%s' must exist in resource '%s', " 103 | "field '%s'") % 104 | (value, data_relation['resource'], 105 | data_relation['field'])) 106 | 107 | def _validate_type_objectid(self, field, value): 108 | """ 109 | This field doesn't have a meaning in SQL 110 | """ 111 | pass 112 | 113 | def _validate_type_json(self, field, value): 114 | """ Enables validation for `json` schema attribute. 115 | 116 | :param field: field name. 117 | :param value: field value. 118 | """ 119 | pass 120 | 121 | def _validate_readonly(self, read_only, field, value): 122 | # Copied from eve/io/mongo/validation.py. 123 | original_value = self._original_document.get(field) \ 124 | if self._original_document else None 125 | if value != original_value: 126 | super(ValidatorSQL, self)._validate_readonly(read_only, field, 127 | value) 128 | 129 | def _validate_dependencies(self, document, dependencies, field, 130 | break_on_error=False): 131 | # Copied from eve/io/mongo/validation.py, with slight modifications. 132 | 133 | if dependencies is None: 134 | return True 135 | 136 | if isinstance(dependencies, str_type): 137 | dependencies = [dependencies] 138 | 139 | defaults = {} 140 | for d in dependencies: 141 | root = d.split('.')[0] 142 | default = self.schema[root].get('default') 143 | if default and root not in document: 144 | defaults[root] = default 145 | 146 | if isinstance(dependencies, collections.Mapping): 147 | # Only evaluate dependencies that don't have *valid* defaults 148 | for k, v in defaults.items(): 149 | if v in dependencies[k]: 150 | del(dependencies[k]) 151 | else: 152 | # Only evaluate dependencies that don't have defaults values 153 | dependencies = [d for d in dependencies if d not in 154 | defaults.keys()] 155 | 156 | dcopy = None 157 | if self._original_document: 158 | dcopy = copy.copy(document) 159 | # Use dict_update and remove_none_values from utils, so existing 160 | # values in document don't get overridden by the original document 161 | # and None values are removed. Otherwise handling in parent method 162 | # does not work as expected. 163 | dict_update(dcopy, self._original_document) 164 | remove_none_values(dcopy) 165 | return super(ValidatorSQL, self)._validate_dependencies( 166 | dcopy or document, dependencies, field, break_on_error) 167 | 168 | def _error(self, field, _error): 169 | # Copied from eve/io/mongo/validation.py. 170 | super(ValidatorSQL, self)._error(field, _error) 171 | if config.VALIDATION_ERROR_AS_LIST: 172 | err = self._errors[field] 173 | if not isinstance(err, list): 174 | self._errors[field] = [err] 175 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file includes all dependencies required for contributing to the 2 | # development of Eve-SQLAlchemy. If you just wish to install and use 3 | # Eve-SQLAlchemy, run `pip install .`. 4 | 5 | . 6 | Sphinx>=1.2.3 7 | alabaster 8 | zest.releaser[recommended] 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | exclude = build,docs,.git,.tox 6 | 7 | [isort] 8 | combine_as_imports = true 9 | default_section = THIRDPARTY 10 | include_trailing_comma = true 11 | line_length = 79 12 | multi_line_output = 5 13 | not_skip = __init__.py 14 | 15 | [tool:pytest] 16 | python_files = eve_sqlalchemy/tests/*.py 17 | addopts = -rf 18 | norecursedirs = testsuite .tox 19 | 20 | [zest.releaser] 21 | create-wheel = yes 22 | python-file-with-version = eve_sqlalchemy/__about__.py 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | def read(*parts): 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | return codecs.open(os.path.join(here, *parts), 'r', 'utf-8').read() 10 | 11 | 12 | # Import the project's metadata 13 | metadata = {} 14 | exec(read('eve_sqlalchemy', '__about__.py'), metadata) 15 | 16 | test_dependencies = [ 17 | 'mock', 18 | 'pytest', 19 | ] 20 | 21 | setup( 22 | name=metadata['__title__'], 23 | version=metadata['__version__'], 24 | description=(metadata['__summary__']), 25 | long_description=read('README.rst') + "\n\n" + read('CHANGES'), 26 | keywords='flask sqlalchemy rest', 27 | author=metadata['__author__'], 28 | author_email=metadata['__email__'], 29 | url=metadata['__url__'], 30 | license=metadata['__license__'], 31 | platforms=['any'], 32 | packages=find_packages(), 33 | test_suite='eve_sqlalchemy.tests', 34 | install_requires=[ 35 | 'Eve<0.8', 36 | 'Flask-SQLAlchemy>=2.4,<2.999', 37 | 'SQLAlchemy>=1.3', 38 | ], 39 | tests_require=test_dependencies, 40 | extras_require={ 41 | # This little hack allows us to reference our test dependencies within 42 | # tox.ini. For details see https://stackoverflow.com/a/41398850 . 43 | 'test': test_dependencies, 44 | }, 45 | zip_safe=True, 46 | classifiers=[ 47 | 'Development Status :: 4 - Beta', 48 | 'Environment :: Web Environment', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: BSD License', 51 | 'Operating System :: OS Independent', 52 | 'Programming Language :: Python', 53 | 'Programming Language :: Python :: 2', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Programming Language :: Python :: 3.7', 60 | 'Programming Language :: Python :: Implementation :: CPython', 61 | 'Programming Language :: Python :: Implementation :: PyPy', 62 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 63 | ], 64 | ) 65 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,34,35,36,37}, 4 | pypy, 5 | pypy3, 6 | flake8, 7 | isort, 8 | rstcheck, 9 | whitespace 10 | 11 | [testenv] 12 | deps = .[test] 13 | commands = py.test {posargs} 14 | 15 | [testenv:flake8] 16 | deps = flake8 17 | commands = flake8 eve_sqlalchemy *.py 18 | 19 | [testenv:isort] 20 | deps = isort 21 | commands = 22 | isort --recursive --check-only --diff -p eve_sqlalchemy eve_sqlalchemy 23 | 24 | [testenv:rstcheck] 25 | deps = rstcheck 26 | commands = /bin/sh -c "rstcheck docs/*.rst *.rst" 27 | 28 | [testenv:whitespace] 29 | deps = flake8 30 | commands = /bin/sh -c "flake8 --select=W1,W2,W3 docs/*.rst *" 31 | --------------------------------------------------------------------------------