├── examples ├── simple │ ├── simple │ │ ├── __init__.py │ │ ├── tables.py │ │ └── example_data.py │ ├── app.py │ └── settings.py ├── many_to_many │ ├── many_to_many │ │ ├── __init__.py │ │ └── domain.py │ ├── app.py │ └── settings.py ├── many_to_one │ ├── many_to_one │ │ ├── __init__.py │ │ └── domain.py │ ├── app.py │ └── settings.py ├── one_to_many │ ├── one_to_many │ │ ├── __init__.py │ │ └── domain.py │ ├── app.py │ └── settings.py ├── foreign_primary_key │ ├── foreign_primary_key │ │ ├── __init__.py │ │ └── domain.py │ ├── app.py │ └── settings.py └── trivial │ └── trivial.py ├── eve_sqlalchemy ├── tests │ ├── integration │ │ ├── __init__.py │ │ ├── get_none_values.py │ │ └── collection_class_set.py │ ├── config │ │ ├── domainconfig │ │ │ ├── __init__.py │ │ │ └── ambiguous_relations.py │ │ ├── __init__.py │ │ └── resourceconfig │ │ │ ├── __init__.py │ │ │ ├── item_url.py │ │ │ ├── column_property.py │ │ │ ├── hybrid_property.py │ │ │ ├── datasource.py │ │ │ ├── self_referential_relationship.py │ │ │ ├── inheritance.py │ │ │ ├── foreign_primary_key.py │ │ │ ├── item_lookup_field.py │ │ │ ├── many_to_one_relationship.py │ │ │ ├── one_to_many_relationship.py │ │ │ ├── one_to_one_relationship.py │ │ │ ├── many_to_many_relationship.py │ │ │ ├── id_field.py │ │ │ ├── association_proxy.py │ │ │ └── schema.py │ ├── test_validation.py │ ├── utils.py │ ├── test_sql_tables.py │ ├── test_settings.py │ ├── __init__.py │ ├── patch.py │ ├── delete.py │ └── put.py ├── config │ ├── __init__.py │ ├── domainconfig.py │ ├── fieldconfig.py │ └── resourceconfig.py ├── __about__.py ├── structures.py ├── media.py ├── utils.py ├── validation.py └── parser.py ├── docs ├── _themes │ ├── .gitignore │ ├── flask │ │ ├── theme.conf │ │ ├── relations.html │ │ └── layout.html │ ├── flask_small │ │ ├── theme.conf │ │ ├── layout.html │ │ └── static │ │ │ └── flasky.css_t │ ├── README │ ├── LICENSE │ └── flask_theme_support.py ├── contributing.rst ├── _static │ ├── favicon.ico │ ├── favicon.png │ ├── eve_leaf.png │ ├── eve-sidebar.png │ └── forkme_right_green_007200.png ├── _templates │ ├── artwork.html │ └── sidebarintro.html ├── index.rst ├── trivial.rst ├── upgrading.rst ├── Makefile └── conf.py ├── MANIFEST.in ├── .travis.yml ├── requirements.txt ├── setup.cfg ├── AUTHORS ├── .gitignore ├── tox.ini ├── LICENSE ├── setup.py ├── CHANGES ├── CONTRIBUTING.rst └── README.rst /examples/simple/simple/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/many_to_many/many_to_many/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/many_to_one/many_to_one/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/one_to_many/one_to_many/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eve_sqlalchemy/tests/config/domainconfig/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_themes/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /examples/foreign_primary_key/foreign_primary_key/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/eve-sqlalchemy/master/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/eve-sqlalchemy/master/docs/_static/favicon.png -------------------------------------------------------------------------------- /docs/_templates/artwork.html: -------------------------------------------------------------------------------- 1 |

2 | Artwork by Kalamun © 2013 3 |

4 | -------------------------------------------------------------------------------- /docs/_static/eve_leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/eve-sqlalchemy/master/docs/_static/eve_leaf.png -------------------------------------------------------------------------------- /docs/_static/eve-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/eve-sqlalchemy/master/docs/_static/eve-sidebar.png -------------------------------------------------------------------------------- /docs/_static/forkme_right_green_007200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrshll/eve-sqlalchemy/master/docs/_static/forkme_right_green_007200.png -------------------------------------------------------------------------------- /eve_sqlalchemy/config/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .domainconfig import DomainConfig # noqa 4 | from .resourceconfig import ResourceConfig # noqa 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS CHANGES CONTRIBUTING.rst LICENSE 2 | include requirements.txt tox.ini 3 | graft docs 4 | prune docs/_build 5 | graft examples 6 | 7 | global-exclude __pycache__ *.pyc *.pyo 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | cache: pip 4 | script: tox 5 | install: travis_retry pip install tox-travis 6 | python: 7 | - 2.7 8 | - 3.4 9 | - 3.5 10 | - 3.6 11 | - pypy 12 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.6.0.dev0' 7 | 8 | __author__ = 'Dominik Kellner' 9 | __email__ = 'dkellner@dkellner.de' 10 | 11 | __license__ = 'BSD' 12 | __copyright__ = '2017 %s' % __author__ 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Eve-SQLAlchemy 2 | ============== 3 | 4 | This tutorial will show how to use Eve with the `SQLAlchemy`_ support. Using 5 | `SQLAlchemy`_ instead MongoDB means that you can re-use your existing SQL data 6 | model and expose it via REST thanks to Eve with no hassle. 7 | 8 | Developer's Guide 9 | ----------------- 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | tutorial 14 | trivial 15 | upgrading 16 | contributing 17 | 18 | .. include:: ../CHANGES 19 | 20 | .. _SQLAlchemy: http://www.sqlalchemy.org/ 21 | -------------------------------------------------------------------------------- /examples/foreign_primary_key/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | from eve_sqlalchemy import SQL 3 | from eve_sqlalchemy.validation import ValidatorSQL 4 | 5 | from foreign_primary_key.domain import Base, Lock, Node 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 | -------------------------------------------------------------------------------- /examples/many_to_one/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | from eve_sqlalchemy import SQL 3 | from eve_sqlalchemy.validation import ValidatorSQL 4 | 5 | from many_to_one.domain import Base, Child, Parent 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 | -------------------------------------------------------------------------------- /examples/many_to_many/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | from eve_sqlalchemy import SQL 3 | from eve_sqlalchemy.validation import ValidatorSQL 4 | 5 | from many_to_many.domain import Base, Child, Parent 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 | -------------------------------------------------------------------------------- /examples/one_to_many/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | from eve_sqlalchemy import SQL 3 | from eve_sqlalchemy.validation import ValidatorSQL 4 | 5 | from one_to_many.domain import Base, Child, Parent 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 | -------------------------------------------------------------------------------- /docs/_themes/flask/relations.html: -------------------------------------------------------------------------------- 1 |

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/one_to_many/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | 3 | from one_to_many.domain import Child, Parent 4 | 5 | DEBUG = True 6 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 7 | SQLALCHEMY_TRACK_MODIFICATIONS = False 8 | RESOURCE_METHODS = ['GET', 'POST'] 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 | -------------------------------------------------------------------------------- /examples/foreign_primary_key/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | 3 | from foreign_primary_key.domain import Lock, Node 4 | 5 | DEBUG = True 6 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 7 | SQLALCHEMY_TRACK_MODIFICATIONS = False 8 | RESOURCE_METHODS = ['GET', 'POST'] 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 | 'nodes': ResourceConfig(Node), 20 | 'locks': ResourceConfig(Lock) 21 | }).render() 22 | -------------------------------------------------------------------------------- /examples/many_to_many/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | 3 | from many_to_many.domain import Child, Parent 4 | 5 | DEBUG = True 6 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 7 | SQLALCHEMY_TRACK_MODIFICATIONS = False 8 | RESOURCE_METHODS = ['GET', 'POST'] 9 | ITEM_METHODS = ['GET', 'PATCH'] 10 | 11 | # The following two lines will output the SQL statements executed by 12 | # SQLAlchemy. This is useful while debugging and in development, but is turned 13 | # off by default. 14 | # -------- 15 | # SQLALCHEMY_ECHO = True 16 | # SQLALCHEMY_RECORD_QUERIES = True 17 | 18 | # The default schema is generated using DomainConfig: 19 | DOMAIN = DomainConfig({ 20 | 'parents': ResourceConfig(Parent), 21 | 'children': ResourceConfig(Child) 22 | }).render() 23 | -------------------------------------------------------------------------------- /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 | - Mario Kralj 26 | - Nicola Iarocci 27 | - Peter Zinng 28 | - Tomasz Jezierski (Tefnet) 29 | - fubu 30 | - Øystein S. Haaland 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/simple/app.py: -------------------------------------------------------------------------------- 1 | from eve import Eve 2 | from eve_sqlalchemy import SQL 3 | from eve_sqlalchemy.validation import ValidatorSQL 4 | 5 | from simple.tables import Base, Invoices, People 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 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 | -------------------------------------------------------------------------------- /examples/many_to_one/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | 3 | from many_to_one.domain import Child, Parent 4 | 5 | DEBUG = True 6 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 7 | SQLALCHEMY_TRACK_MODIFICATIONS = False 8 | RESOURCE_METHODS = ['GET', 'POST'] 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 | 23 | DOMAIN['children']['datasource']['projection']['child_id'] = 1 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/foreign_primary_key/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/simple/settings.py: -------------------------------------------------------------------------------- 1 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 2 | 3 | from simple.tables import Invoices, People 4 | 5 | DEBUG = True 6 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 7 | SQLALCHEMY_TRACK_MODIFICATIONS = False 8 | RESOURCE_METHODS = ['GET', 'POST'] 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 | 'people': ResourceConfig(People), 20 | 'invoices': ResourceConfig(Invoices) 21 | }).render() 22 | 23 | # But you can always customize it: 24 | DOMAIN['people'].update({ 25 | 'item_title': 'person', 26 | 'cache_control': 'max-age=10,must-revalidate', 27 | 'cache_expires': 10, 28 | 'resource_methods': ['GET', 'POST', 'DELETE'] 29 | }) 30 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{27,34,35,36}, 4 | pypy, 5 | flake8, 6 | isort, 7 | rstcheck, 8 | whitespace 9 | 10 | [testenv] 11 | deps = .[test] 12 | commands = py.test {posargs} 13 | 14 | [testenv:flake8] 15 | deps = flake8 16 | commands = flake8 eve_sqlalchemy examples *.py 17 | 18 | [testenv:isort] 19 | deps = isort 20 | commands = 21 | isort --recursive --check-only --diff -p eve_sqlalchemy eve_sqlalchemy 22 | isort --recursive --check-only --diff -o eve_sqlalchemy \ 23 | -p foreign_primary_key -p one_to_many -p many_to_many \ 24 | -p many_to_one -p simple \ 25 | examples 26 | 27 | [testenv:rstcheck] 28 | deps = rstcheck 29 | commands = /bin/sh -c "rstcheck docs/*.rst *.rst" 30 | 31 | [testenv:whitespace] 32 | deps = flake8 33 | commands = /bin/sh -c "flake8 --select=W1,W2,W3 docs/*.rst *" 34 | 35 | [travis] 36 | python = 37 | 2.7: py27 38 | 3.4: py34 39 | 3.5: py35,flake8,isort,rstcheck,whitespace 40 | 3.6: py36 41 | pypy: pypy 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/many_to_one/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 | http://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") 26 | 27 | 28 | class Child(BaseModel): 29 | __tablename__ = 'child' 30 | id = Column(Integer, primary_key=True) 31 | -------------------------------------------------------------------------------- /examples/one_to_many/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 | http://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") 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 | -------------------------------------------------------------------------------- /examples/simple/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 | -------------------------------------------------------------------------------- /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/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

Other Projects

2 | 3 | 13 | 14 |

Useful Links

15 | 23 | -------------------------------------------------------------------------------- /examples/many_to_many/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 | http://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 | -------------------------------------------------------------------------------- /docs/trivial.rst: -------------------------------------------------------------------------------- 1 | Simple example 2 | ============== 3 | 4 | Create a file, called trivial.py, and include the following: 5 | 6 | .. literalinclude:: ../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 | -------------------------------------------------------------------------------- /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 ResourceConfigTestCase 9 | from .. import BaseModel 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 | http://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/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 ResourceConfigTestCase 9 | from .. import BaseModel 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 | http://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/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 ResourceConfigTestCase 8 | from .. import BaseModel 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 | -------------------------------------------------------------------------------- /examples/trivial/trivial.py: -------------------------------------------------------------------------------- 1 | ''' Trivial Eve-SQLAlchemy example. ''' 2 | from eve import Eve 3 | from eve_sqlalchemy import SQL 4 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 5 | from eve_sqlalchemy.validation import ValidatorSQL 6 | from sqlalchemy import Column, Integer, String 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.orm import column_property 9 | 10 | Base = declarative_base() 11 | 12 | 13 | class People(Base): 14 | __tablename__ = 'people' 15 | id = Column(Integer, primary_key=True, autoincrement=True) 16 | firstname = Column(String(80)) 17 | lastname = Column(String(120)) 18 | fullname = column_property(firstname + " " + lastname) 19 | 20 | 21 | SETTINGS = { 22 | 'DEBUG': True, 23 | 'SQLALCHEMY_DATABASE_URI': 'sqlite://', 24 | 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 25 | 'DOMAIN': DomainConfig({ 26 | 'people': ResourceConfig(People) 27 | }).render() 28 | } 29 | 30 | app = Eve(auth=None, settings=SETTINGS, validator=ValidatorSQL, data=SQL) 31 | 32 | # bind SQLAlchemy 33 | db = app.data.driver 34 | Base.metadata.bind = db.engine 35 | db.Model = Base 36 | db.create_all() 37 | 38 | # Insert some example data in the db 39 | if not db.session.query(People).count(): 40 | db.session.add_all([ 41 | People(firstname=u'George', lastname=u'Washington'), 42 | People(firstname=u'John', lastname=u'Adams'), 43 | People(firstname=u'Thomas', lastname=u'Jefferson')]) 44 | db.session.commit() 45 | 46 | # using reloader will destroy in-memory sqlite db 47 | app.run(debug=True, use_reloader=False) 48 | -------------------------------------------------------------------------------- /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/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 ResourceConfigTestCase 11 | from .. import BaseModel 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ResourceConfigTestCase 11 | from .. import BaseModel 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 | -------------------------------------------------------------------------------- /examples/simple/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/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 ResourceConfigTestCase 11 | from .. import BaseModel 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.6,<0.7', 36 | 'Flask-SQLAlchemy>=1.0,<2.999', 37 | 'SQLAlchemy>=1.1', 38 | # keep until pip properly resolves dependencies: 39 | 'Flask<0.11', 40 | ], 41 | tests_require=test_dependencies, 42 | extras_require={ 43 | # This little hack allows us to reference our test dependencies within 44 | # tox.ini. For details see http://stackoverflow.com/a/41398850 . 45 | 'test': test_dependencies, 46 | }, 47 | zip_safe=True, 48 | classifiers=[ 49 | 'Development Status :: 4 - Beta', 50 | 'Environment :: Web Environment', 51 | 'Intended Audience :: Developers', 52 | 'License :: OSI Approved :: BSD License', 53 | 'Operating System :: OS Independent', 54 | 'Programming Language :: Python', 55 | 'Programming Language :: Python :: 2', 56 | 'Programming Language :: Python :: 2.7', 57 | 'Programming Language :: Python :: 3', 58 | 'Programming Language :: Python :: 3.4', 59 | 'Programming Language :: Python :: 3.5', 60 | 'Programming Language :: Python :: 3.6', 61 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /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/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 | self._query = self._query.order_by(*self._sort) 39 | 40 | # save the count of items to an internal variables before applying the 41 | # limit to the query as that screws the count returned by it 42 | self._count = self._query.count() 43 | if self._max_results: 44 | self._query = self._query.limit(self._max_results) 45 | if self._page: 46 | self._query = self._query.offset((self._page - 1) * 47 | self._max_results) 48 | 49 | def __iter__(self): 50 | for i in self._query: 51 | yield sqla_object_to_dict(i, self._fields) 52 | 53 | def count(self, **kwargs): 54 | return self._count 55 | -------------------------------------------------------------------------------- /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 ResourceConfigTestCase 11 | from .. import BaseModel 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 | http://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/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/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/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 ResourceConfigTestCase 12 | from .. import BaseModel 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 | http://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 ResourceConfigTestCase 11 | from .. import BaseModel 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 | http://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/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 ResourceConfigTestCase 11 | from .. import BaseModel 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 | http://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/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/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/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 ResourceConfigTestCase 12 | from .. import BaseModel 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 | http://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 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 0.6.0 (unreleased) 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | - Return None-values again (#155) [Cuong Manh Le] 8 | - Allow to supply own Flask-SQLAlchemy driver (#86) [fubu] 9 | - Support columns with server_default (#160) [Asif Mahmud Shimon] 10 | 11 | 12 | 0.5.0 (2017-10-22) 13 | ~~~~~~~~~~~~~~~~~~ 14 | 15 | - Add DomainConfig and ResourceConfig to ease configuration (#152) 16 | [Dominik Kellner] 17 | - Fixes in documentation (#151) [Alessandro De Angelis] 18 | - Fix deprecated import warning (#142) [Cuong Manh Le] 19 | - Configure `zest.releaser` for release management (#137) 20 | [Dominik Kellner, Øystein S. Haaland] 21 | - Leverage further automated syntax and formatting checks (#138) 22 | [Dominik Kellner] 23 | - Clean up specification of dependencies [Dominik Kellner] 24 | - Added 'Contributing' section to docs (#129) [Mario Kralj] 25 | - Fix trivial app output in documentation (#131) [Michal Vlasák] 26 | - Added dialect-specific PostgreSQL JSON type (#133) [Mario Kralj] 27 | - Fix url field in documentation about additional lookup (#110) [Killian Kemps] 28 | - Compatibility with Eve 0.6.4 and refactoring of tests (#92) [Dominik Kellner] 29 | 30 | 31 | 0.4.1 (2015-12-16) 32 | ~~~~~~~~~~~~~~~~~~ 33 | 34 | - improve query with null values [amleczko] 35 | 36 | 37 | 0.4.0a3 (2015-10-20) 38 | ~~~~~~~~~~~~~~~~~~~~ 39 | 40 | - `hybrid_properties` are now readonly in Eve schema [amleczko] 41 | 42 | 43 | 0.4.0a2 (2015-09-17) 44 | ~~~~~~~~~~~~~~~~~~~~ 45 | 46 | - PUT drops/recreates item in the same transaction [goneri] 47 | 48 | 49 | 0.4.0a1 (2015-06-18) 50 | ~~~~~~~~~~~~~~~~~~~~ 51 | 52 | - support the Python-Eve generic sorting syntax [Goneri Le Bouder] 53 | - add support for `and_` and `or_` conjunctions in sqla expressions [toxsick] 54 | - embedded table: use DOMAIN to look up the resource fields [Goneri Le Bouder] 55 | 56 | 57 | 0.3.4 (2015-05-18) 58 | ~~~~~~~~~~~~~~~~~~ 59 | 60 | - fix setup.py metadata 61 | - fix how embedded documents are resolved [amleczko] 62 | 63 | 64 | 0.3.3 (2015-05-13) 65 | ~~~~~~~~~~~~~~~~~~ 66 | 67 | - added support of SA association proxy [Kevin Roy] 68 | - make sure relationships are generated properly [amleczko] 69 | 70 | 71 | 0.3.2 (2015-05-01) 72 | ~~~~~~~~~~~~~~~~~~ 73 | 74 | - add fallback on attr.op if the operator doesn't exists in the 75 | `ColumnProperty` [Kevin Roy] 76 | - add support for PostgreSQL JSON type [Goneri Le Bouder] 77 | 78 | 79 | 0.3.1 (2015-04-29) 80 | ~~~~~~~~~~~~~~~~~~ 81 | 82 | - more flexible handling sqlalchemy operators [amleczko] 83 | 84 | 85 | 0.3 (2015-04-17) 86 | ~~~~~~~~~~~~~~~~ 87 | 88 | - return everything as dicts instead of SQLAResult, remove SQLAResult 89 | [Leonidaz0r] 90 | - fix update function, this closes #22 [David Durieux] 91 | - fixed replaced method, we are compatible with Eve>=0.5.1 [Kevin Roy] 92 | - fixed jsonify function [Leonidaz0r] 93 | - update documentation [Alex Kerney] 94 | - use id_field column from the config [Goneri Le Bouder] 95 | - add flake8 in tox [Goneri Le Bouder] 96 | 97 | 98 | 0.2.1 (2015-02-25) 99 | ~~~~~~~~~~~~~~~~~~ 100 | 101 | - always wrap embedded documents [amleczko] 102 | 103 | 104 | 0.2 (2015-01-27) 105 | ~~~~~~~~~~~~~~~~ 106 | 107 | - various bugfixing [Arie Brosztein, toxsick] 108 | - refactor sorting parser, add sql order by expresssions; please check 109 | http://eve-sqlalchemy.readthedocs.org/#sqlalchemy-sorting for more details 110 | [amleczko] 111 | 112 | 113 | 0.1 (2015-01-13) 114 | ~~~~~~~~~~~~~~~~ 115 | 116 | - First public preview release. [amleczko] 117 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 78 | .. _PEP8: http://www.python.org/dev/peps/pep-0008/ 79 | .. _flake8: http://flake8.readthedocs.org/en/latest/ 80 | .. _tox: http://tox.readthedocs.org/en/latest/ 81 | .. _help: https://help.github.com/ 82 | .. _`Effective Guide to Pull Requests`: http://codeinthehole.com/writing/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 | -------------------------------------------------------------------------------- /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 ResourceConfigTestCase 10 | from .. import BaseModel 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 | -------------------------------------------------------------------------------- /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 | Eve-SQLAlchemy is simple 12 | ------------------------ 13 | 14 | The following code blocks are excerpts of ``examples/one_to_many`` and should 15 | give you an idea of how Eve-SQLAlchemy is used. A complete working example can 16 | be found there. If you are not familiar with `Eve `_ 17 | and `SQLAlchemy `_, it is recommended to read up 18 | on them first. 19 | 20 | For this example, we declare two SQLAlchemy mappings (from ``domain.py``): 21 | 22 | .. code-block:: python 23 | 24 | class Parent(BaseModel): 25 | __tablename__ = 'parent' 26 | id = Column(Integer, primary_key=True) 27 | children = relationship("Child") 28 | 29 | class Child(BaseModel): 30 | __tablename__ = 'child' 31 | id = Column(Integer, primary_key=True) 32 | parent_id = Column(Integer, ForeignKey('parent.id')) 33 | 34 | As for Eve, a ``settings.py`` is used to configure our API. Eve-SQLAlchemy, 35 | having access to a lot of metadata from your models, can automatically generate 36 | a great deal of the `DOMAIN` dictionary for you: 37 | 38 | .. code-block:: python 39 | 40 | DEBUG = True 41 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 42 | SQLALCHEMY_TRACK_MODIFICATIONS = False 43 | RESOURCE_METHODS = ['GET', 'POST'] 44 | 45 | DOMAIN = DomainConfig({ 46 | 'parents': ResourceConfig(Parent), 47 | 'children': ResourceConfig(Child) 48 | }).render() 49 | 50 | Finally, running our application server is easy (from ``app.py``): 51 | 52 | .. code-block:: python 53 | 54 | app = Eve(validator=ValidatorSQL, data=SQL) 55 | 56 | db = app.data.driver 57 | Base.metadata.bind = db.engine 58 | db.Model = Base 59 | 60 | # create database schema on startup and populate some example data 61 | db.create_all() 62 | db.session.add_all([Parent(children=[Child() for k in range(n)]) 63 | for n in range(10)]) 64 | db.session.commit() 65 | 66 | # using reloader will destroy the in-memory sqlite db 67 | app.run(debug=True, use_reloader=False) 68 | 69 | The API is now live, ready to be consumed: 70 | 71 | .. code-block:: console 72 | 73 | $ curl -s http://localhost:5000/parents | python -m json.tool 74 | 75 | .. code-block:: json 76 | 77 | { 78 | "_items": [ 79 | { 80 | "_created": "Sun, 22 Oct 2017 07:58:28 GMT", 81 | "_etag": "f56d7cb013bf3d8449e11e8e1f0213f5efd0f07d", 82 | "_links": { 83 | "self": { 84 | "href": "parents/1", 85 | "title": "Parent" 86 | } 87 | }, 88 | "_updated": "Sun, 22 Oct 2017 07:58:28 GMT", 89 | "children": [], 90 | "id": 1 91 | }, 92 | { 93 | "_created": "Sun, 22 Oct 2017 07:58:28 GMT", 94 | "_etag": "dd1698161cb6beef04f564b2e18804d4a7c4330d", 95 | "_links": { 96 | "self": { 97 | "href": "parents/2", 98 | "title": "Parent" 99 | } 100 | }, 101 | "_updated": "Sun, 22 Oct 2017 07:58:28 GMT", 102 | "children": [ 103 | 1 104 | ], 105 | "id": 2 106 | }, 107 | "..." 108 | ], 109 | "_links": { 110 | "parent": { 111 | "href": "/", 112 | "title": "home" 113 | }, 114 | "self": { 115 | "href": "parents", 116 | "title": "parents" 117 | } 118 | }, 119 | "_meta": { 120 | "max_results": 25, 121 | "page": 1, 122 | "total": 10 123 | } 124 | } 125 | 126 | All you need to bring your API online is a database, a configuration 127 | file (defaults to ``settings.py``) and a launch script. Overall, you 128 | will find that configuring and fine-tuning your API is a very simple 129 | process. 130 | 131 | Eve-SQLAlchemy is thoroughly tested under Python 2.7-3.6 and PyPy. 132 | 133 | Documentation 134 | ------------- 135 | 136 | The offical project documentation can be accessed at 137 | `eve-sqlalchemy.readthedocs.org 138 | `_. For full working examples, 139 | especially regarding different relationship types, see the ``examples`` 140 | directory in this repository. 141 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_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': '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/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 collections 13 | import copy 14 | import re 15 | 16 | from eve.utils import config 17 | from sqlalchemy.ext.declarative.api import DeclarativeMeta 18 | 19 | 20 | def merge_dicts(*dicts): 21 | """ 22 | Given any number of dicts, shallow copy and merge into a new dict, 23 | precedence goes to key value pairs in latter dicts. 24 | 25 | Source: https://stackoverflow.com/q/38987 26 | """ 27 | result = {} 28 | for dictionary in dicts: 29 | result.update(dictionary) 30 | return result 31 | 32 | 33 | def dict_update(d, u): 34 | for k, v in u.items(): 35 | if isinstance(v, collections.Mapping) and \ 36 | k in d and isinstance(d[k], collections.Mapping): 37 | dict_update(d[k], v) 38 | elif k not in d: 39 | d[k] = u[k] 40 | 41 | 42 | def remove_none_values(dict_): 43 | for k, v in list(dict_.items()): 44 | if v is None: 45 | del(dict_[k]) 46 | 47 | 48 | def validate_filters(where, resource): 49 | allowed = config.DOMAIN[resource]['allowed_filters'] 50 | if '*' not in allowed: 51 | for filt in where: 52 | key = filt.left.key 53 | if key not in allowed: 54 | return "filter on '%s' not allowed" % key 55 | return None 56 | 57 | 58 | def sqla_object_to_dict(obj, fields): 59 | """ Creates a dict containing copies of the requested fields from the 60 | SQLAlchemy query result """ 61 | if config.LAST_UPDATED not in fields: 62 | fields.append(config.LAST_UPDATED) 63 | if config.DATE_CREATED not in fields: 64 | fields.append(config.DATE_CREATED) 65 | if config.ETAG not in fields \ 66 | and getattr(config, 'IF_MATCH', True): 67 | fields.append(config.ETAG) 68 | 69 | result = {} 70 | for field in map(lambda f: f.split('.', 1)[0], fields): 71 | try: 72 | val = obj.__getattribute__(field) 73 | 74 | # If association proxies are embedded, their values must be copied 75 | # since they are garbage collected when Eve try to encode the 76 | # response. 77 | if hasattr(val, 'copy'): 78 | val = val.copy() 79 | 80 | result[field] = _sanitize_value(val) 81 | except AttributeError: 82 | # Ignore if the requested field does not exist 83 | # (may be wrong embedding parameter) 84 | pass 85 | 86 | # We have to remove the ETAG if it's None so Eve will add it later again. 87 | if result.get(config.ETAG, False) is None: 88 | del(result[config.ETAG]) 89 | 90 | return result 91 | 92 | 93 | def _sanitize_value(value): 94 | if isinstance(value.__class__, DeclarativeMeta): 95 | return _get_id(value) 96 | elif isinstance(value, collections.Mapping): 97 | return dict([(k, _sanitize_value(v)) for k, v in value.items()]) 98 | elif isinstance(value, collections.MutableSequence): 99 | return [_sanitize_value(v) for v in value] 100 | elif isinstance(value, collections.Set): 101 | return set(_sanitize_value(v) for v in value) 102 | else: 103 | return copy.copy(value) 104 | 105 | 106 | def _get_id(obj): 107 | resource = _get_resource(obj) 108 | return getattr(obj, config.DOMAIN[resource]['id_field']) 109 | 110 | 111 | def extract_sort_arg(req): 112 | if req.sort: 113 | if re.match('^[-,\w]+$', req.sort): 114 | arg = [] 115 | for s in req.sort.split(','): 116 | if s.startswith('-'): 117 | arg.append([s[1:], -1]) 118 | else: 119 | arg.append([s]) 120 | return arg 121 | else: 122 | return ast.literal_eval(req.sort) 123 | else: 124 | return None 125 | 126 | 127 | def rename_relationship_fields_in_sort_args(model, sort): 128 | result = [] 129 | rename_mapping = _get_relationship_to_id_field_rename_mapping(model) 130 | for t in sort: 131 | if t[0] in rename_mapping: 132 | t = list(t) 133 | t[0] = rename_mapping[t[0]] 134 | t = tuple(t) 135 | result.append(t) 136 | return result 137 | 138 | 139 | def rename_relationship_fields_in_dict(model, dict_): 140 | result = {} 141 | rename_mapping = _get_relationship_to_id_field_rename_mapping(model) 142 | for k, v in dict_.items(): 143 | if k in rename_mapping: 144 | result[rename_mapping[k]] = v 145 | else: 146 | result[k] = v 147 | return result 148 | 149 | 150 | def rename_relationship_fields_in_str(model, str_): 151 | rename_mapping = _get_relationship_to_id_field_rename_mapping(model) 152 | for k, v in rename_mapping.items(): 153 | str_ = re.sub(r'\b%s\b' % k, v, str_) 154 | return str_ 155 | 156 | 157 | def _get_relationship_to_id_field_rename_mapping(model): 158 | result = {} 159 | resource = _get_resource(model) 160 | schema = config.DOMAIN[resource]['schema'] 161 | for field, field_schema in schema.items(): 162 | if 'local_id_field' in field_schema: 163 | result[field] = field_schema['local_id_field'] 164 | return result 165 | 166 | 167 | def _get_resource(model_or_obj): 168 | if isinstance(model_or_obj.__class__, DeclarativeMeta): 169 | model = model_or_obj.__class__ 170 | else: 171 | model = model_or_obj 172 | for resource, settings in config.DOMAIN.items(): 173 | if settings['datasource']['source'] == model.__name__: 174 | return resource 175 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/upgrading.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Upgrading 3 | ========= 4 | 5 | Upgrading from 0.5.0 to 0.6.0 6 | ============================= 7 | 8 | There is one potentially breaking change in 0.6.0: Due to a regression 0.5.0 9 | did not return `None`/`null` values anymore (as Eve does and 0.4.1 did). That 10 | means your API might return slightly different responses after upgrading to 11 | 0.6.0 than it did before. If it's really a breaking change for you depends on 12 | your API specification and your clients. 13 | 14 | Upgrading from 0.4.1 to 0.5.0 15 | ============================= 16 | 17 | There are two breaking changes in 0.5.0: 18 | 19 | 1. Eve-SQLAlchemy now handles related IDs and embedded objects with just one 20 | field in the payload, just as Eve does. This will most likely affect your 21 | consumers, too! 22 | 2. We introduced a new way to register your SQLAlchemy models with Eve. So far 23 | there is no backward compatible wrapper for the former ``registerSchema`` 24 | decorator. 25 | 26 | Let's look at the needed changes in more detail. To illustrate both changes, we 27 | will look at the following models (the full code is in the `examples` 28 | directory): 29 | 30 | .. code-block:: python 31 | 32 | class People(CommonColumns): 33 | __tablename__ = 'people' 34 | id = Column(Integer, primary_key=True, autoincrement=True) 35 | firstname = Column(String(80)) 36 | lastname = Column(String(120)) 37 | fullname = column_property(firstname + " " + lastname) 38 | 39 | 40 | class Invoices(CommonColumns): 41 | __tablename__ = 'invoices' 42 | id = Column(Integer, primary_key=True, autoincrement=True) 43 | number = Column(Integer) 44 | people_id = Column(Integer, ForeignKey('people.id')) 45 | people = relationship(People, uselist=False) 46 | 47 | 1. Related IDs and embedding 48 | ---------------------------- 49 | 50 | Getting an invoice in 0.4.1 will return the `people_id` in the payload: 51 | 52 | .. code-block:: json 53 | 54 | { 55 | "_created": "Sat, 15 Jul 2017 02:24:58 GMT", 56 | "_etag": null, 57 | "_id": 1, 58 | "_updated": "Sat, 15 Jul 2017 02:24:58 GMT", 59 | "id": 1, 60 | "number": 42, 61 | "people_id": 1 62 | } 63 | 64 | And, if you embed the related ``People`` object, you will get: 65 | 66 | .. code-block:: json 67 | 68 | { 69 | "_created": "Sat, 15 Jul 2017 02:39:25 GMT", 70 | "_etag": null, 71 | "_id": 1, 72 | "_updated": "Sat, 15 Jul 2017 02:39:25 GMT", 73 | "id": 1, 74 | "number": 42, 75 | "people": { 76 | "_created": "Sat, 15 Jul 2017 02:39:25 GMT", 77 | "_etag": null, 78 | "_id": 1, 79 | "_updated": "Sat, 15 Jul 2017 02:39:25 GMT", 80 | "firstname": "George", 81 | "fullname": "George Washington", 82 | "id": 1, 83 | "lastname": "Washington" 84 | }, 85 | "people_id": 1 86 | } 87 | 88 | But this was actually not how Eve itself is handling this. In order to follow 89 | the APIs generated by Eve more closely, we decided to adopt the way Eve is 90 | doing embedding and use the same field for both the related ID and the embedded 91 | document. Which means starting in 0.5.0, the first response looks like this: 92 | 93 | .. code-block:: json 94 | 95 | { 96 | "_created": "Sat, 15 Jul 2017 02:52:20 GMT", 97 | "_etag": "26abc30d70f57de186d9f99a7192444fcf538519", 98 | "_updated": "Sat, 15 Jul 2017 02:52:20 GMT", 99 | "id": 1, 100 | "number": 42, 101 | "people": 1 102 | } 103 | 104 | And the second one (with embedding): 105 | 106 | .. code-block:: json 107 | 108 | { 109 | "_created": "Sat, 15 Jul 2017 02:54:44 GMT", 110 | "_etag": "8a1121cacb77a21f9ff3b5a85cfba0a501a538ea", 111 | "_updated": "Sat, 15 Jul 2017 02:54:44 GMT", 112 | "id": 1, 113 | "number": 42, 114 | "people": { 115 | "_created": "Sat, 15 Jul 2017 02:54:44 GMT", 116 | "_updated": "Sat, 15 Jul 2017 02:54:44 GMT", 117 | "firstname": "George", 118 | "fullname": "George Washington", 119 | "id": 1, 120 | "lastname": "Washington" 121 | } 122 | } 123 | 124 | 2. Registering of SQLAlchemy models 125 | ----------------------------------- 126 | 127 | In 0.4.1, you were most likely doing something along the following lines in 128 | your `settings.py`: 129 | 130 | .. code-block:: python 131 | 132 | ID_FIELD = 'id' 133 | config.ID_FIELD = ID_FIELD 134 | 135 | registerSchema('people')(People) 136 | registerSchema('invoices')(Invoices) 137 | 138 | DOMAIN = { 139 | 'people': People._eve_schema['people'], 140 | 'invoices': Invoices._eve_schema['invoices'] 141 | } 142 | 143 | There are good news: manually (and globally) setting ``ID_FIELD``, including 144 | the workaround of setting ``config.ID_FIELD``, is not required anymore. The 145 | same applies to ``ITEM_LOOKUP_FIELD`` and ``ITEM_URL``. While you can still 146 | override them, they are now preconfigured at the resource level depending on 147 | your models' primary keys. 148 | 149 | The required configuration for the models above simplifies to: 150 | 151 | .. code-block:: python 152 | 153 | from eve_sqlalchemy.config import DomainConfig, ResourceConfig 154 | 155 | DOMAIN = DomainConfig({ 156 | 'people': ResourceConfig(People), 157 | 'invoices': ResourceConfig(Invoices) 158 | }).render() 159 | 160 | *Note:* If you've modified ``DATE_CREATED``, ``LAST_UPDATED`` or ``ETAG``, you 161 | have to pass their value to ``DomainConfig.render()``. They are needed during 162 | rendering the final ``DOMAIN`` configuration. 163 | 164 | .. code-block:: python 165 | 166 | DomainConfig(domain_dict).render(date_created=DATE_CREATED, 167 | last_updated=LAST_UPDATED, 168 | etag=ETAG) 169 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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_allow_unknown(self): 16 | pass 17 | 18 | def test_put_x_www_form_urlencoded_number_serialization(self): 19 | # Eve test manipulates schema and removes required constraint on 'ref'. 20 | # We decided to include 'ref' as it is not easy to manipulate 21 | # nullable-constraints during runtime. 22 | field = 'anumber' 23 | test_value = 41 24 | changes = {field: test_value, 25 | 'ref': 'test_put_x_www_num_ser_12'} 26 | headers = [('If-Match', self.item_etag)] 27 | r, status = self.parse_response(self.test_client.put( 28 | self.item_id_url, data=changes, headers=headers)) 29 | self.assert200(status) 30 | self.assertTrue('OK' in r[STATUS]) 31 | 32 | def test_put_referential_integrity_list(self): 33 | data = {"invoicing_contacts": [self.item_id, self.unknown_item_id]} 34 | headers = [('If-Match', self.invoice_etag)] 35 | r, status = self.put(self.invoice_id_url, data=data, headers=headers) 36 | self.assertValidationErrorStatus(status) 37 | expected = ("value '%s' must exist in resource '%s', field '%s'" % 38 | (self.unknown_item_id, 'contacts', 39 | self.domain['contacts']['id_field'])) 40 | self.assertValidationError(r, {'invoicing_contacts': expected}) 41 | 42 | # Eve test posts a list with self.item_id twice, which can't be handled 43 | # for our case because we use (invoice_id, contact_id) as primary key 44 | # in the association table. 45 | data = {"invoicing_contacts": [self.item_id]} 46 | r, status = self.put(self.invoice_id_url, data=data, headers=headers) 47 | self.assert200(status) 48 | self.assertPutResponse(r, self.invoice_id, 'invoices') 49 | 50 | @pytest.mark.xfail(True, run=False, reason='not applicable to SQLAlchemy') 51 | def test_put_write_concern_fail(self): 52 | pass 53 | 54 | def test_put_subresource(self): 55 | # Eve test uses mongo layer directly. 56 | self.app.config['BANDWIDTH_SAVER'] = False 57 | 58 | with self.app.app_context(): 59 | # create random contact 60 | fake_contact = self.random_contacts(1) 61 | fake_contact_id = self.app.data.insert('contacts', fake_contact)[0] 62 | # update first invoice to reference the new contact 63 | self.app.data.update('invoices', self.invoice_id, 64 | {'person': fake_contact_id}, None) 65 | 66 | # GET all invoices by new contact 67 | response, status = self.get('users/%s/invoices/%s' % 68 | (fake_contact_id, self.invoice_id)) 69 | etag = response[ETAG] 70 | 71 | data = {"inv_number": "new_number"} 72 | headers = [('If-Match', etag)] 73 | response, status = self.put('users/%s/invoices/%s' % 74 | (fake_contact_id, self.invoice_id), 75 | data=data, headers=headers) 76 | self.assert200(status) 77 | self.assertPutResponse(response, self.invoice_id, 'peopleinvoices') 78 | self.assertEqual(response.get('person'), fake_contact_id) 79 | 80 | def test_put_dependency_fields_with_default(self): 81 | # Eve test manipulates schema and removes required constraint on 'ref'. 82 | # We decided to include 'ref' as it is not easy to manipulate 83 | # nullable-constraints during runtime. 84 | field = "dependency_field2" 85 | test_value = "a value" 86 | changes = {field: test_value, 87 | 'ref': 'test_post_dep_with_def_12'} 88 | r = self.perform_put(changes) 89 | db_value = self.compare_put_with_get(field, r) 90 | self.assertEqual(db_value, test_value) 91 | 92 | def test_put_dependency_fields_with_wrong_value(self): 93 | # Eve test manipulates schema and removes required constraint on 'ref'. 94 | # We decided to include 'ref' as it is not easy to manipulate 95 | # nullable-constraints during runtime. 96 | r, status = self.put(self.item_id_url, 97 | data={'dependency_field3': 'value', 98 | 'ref': 'test_post_dep_wrong_fiel1'}, 99 | headers=[('If-Match', self.item_etag)]) 100 | self.assert422(status) 101 | r, status = self.put(self.item_id_url, 102 | data={'dependency_field1': 'value', 103 | 'dependency_field3': 'value', 104 | 'ref': 'test_post_dep_wrong_fiel2'}, 105 | headers=[('If-Match', self.item_etag)]) 106 | self.assert200(status) 107 | 108 | def test_put_creates_unexisting_document(self): 109 | # Eve test uses ObjectId as id. 110 | id = 424242 111 | url = '%s/%s' % (self.known_resource_url, id) 112 | id_field = self.domain[self.known_resource]['id_field'] 113 | changes = {"ref": "1234567890123456789012345"} 114 | r, status = self.put(url, data=changes) 115 | # 201 is a creation (POST) response 116 | self.assert201(status) 117 | # new document has id_field matching the PUT endpoint 118 | self.assertEqual(r[id_field], id) 119 | 120 | def test_put_returns_404_on_unexisting_document(self): 121 | # Eve test uses ObjectId as id. 122 | self.app.config['UPSERT_ON_PUT'] = False 123 | id = 424242 124 | url = '%s/%s' % (self.known_resource_url, id) 125 | changes = {"ref": "1234567890123456789012345"} 126 | r, status = self.put(url, data=changes) 127 | self.assert404(status) 128 | 129 | def test_put_creates_unexisting_document_with_url_as_id(self): 130 | # Eve test uses ObjectId as id. 131 | id = 424242 132 | url = '%s/%s' % (self.known_resource_url, id) 133 | id_field = self.domain[self.known_resource]['id_field'] 134 | changes = {"ref": "1234567890123456789012345", 135 | id_field: 848484} # mismatching id 136 | r, status = self.put(url, data=changes) 137 | # 201 is a creation (POST) response 138 | self.assert201(status) 139 | # new document has id_field matching the PUT endpoint 140 | # (eventual mismatching id_field in the payload is ignored/replaced) 141 | self.assertEqual(r[id_field], id) 142 | 143 | def test_put_creates_unexisting_document_fails_on_mismatching_id(self): 144 | # Eve test uses ObjectId as id. 145 | id = 424243 146 | id_field = self.domain[self.known_resource]['id_field'] 147 | changes = {"ref": "1234567890123456789012345", id_field: id} 148 | r, status = self.put(self.item_id_url, 149 | data=changes, 150 | headers=[('If-Match', self.item_etag)]) 151 | self.assert400(status) 152 | self.assertTrue('immutable' in r['_error']['message']) 153 | 154 | def compare_put_with_get(self, fields, put_response): 155 | # Eve methods checks for instance of str, which could be unicode for 156 | # Python 2. We use six.string_types instead. 157 | raw_r = self.test_client.get(self.item_id_url) 158 | r, status = self.parse_response(raw_r) 159 | self.assert200(status) 160 | self.assertEqual(raw_r.headers.get('ETag'), 161 | put_response[ETAG]) 162 | if isinstance(fields, six.string_types): 163 | return r[fields] 164 | else: 165 | return [r[field] for field in fields] 166 | 167 | 168 | class TestEvents(eve_put_tests.TestEvents, TestBase): 169 | 170 | def before_replace(self): 171 | # Eve test code uses mongo layer directly. 172 | # TODO: Fix directly in Eve and remove this override 173 | contact = self.app.data.find_one_raw(self.known_resource, self.item_id) 174 | return contact['ref'] == self.item_name 175 | -------------------------------------------------------------------------------- /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/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module implements a Python-to-SQLAlchemy syntax parser. 4 | Allows the SQLAlchemy data-layer to seamlessy respond to a 5 | Python-like query. 6 | 7 | :copyright: (c) 2013 by Andrew Mleczko and Tomasz Jezierski (Tefnet) 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from __future__ import unicode_literals 11 | 12 | import ast 13 | import itertools 14 | import json 15 | import operator as sqla_op 16 | import re 17 | 18 | import sqlalchemy 19 | from eve.utils import str_to_date 20 | from sqlalchemy.ext.associationproxy import AssociationProxy 21 | from sqlalchemy.sql import expression as sqla_exp 22 | 23 | 24 | class ParseError(ValueError): 25 | pass 26 | 27 | 28 | def parse_dictionary(filter_dict, model): 29 | """ 30 | Parse a dictionary into a list of SQLAlchemy BinaryExpressions to be used 31 | in query filters. 32 | 33 | :param filter_dict: Dictionary to convert 34 | :param model: SQLAlchemy model class used to create the BinaryExpressions 35 | :return list: List of conditions as SQLAlchemy BinaryExpressions 36 | """ 37 | if len(filter_dict) == 0: 38 | return [] 39 | 40 | conditions = [] 41 | 42 | for k, v in filter_dict.items(): 43 | # first let's check with the expression parser 44 | try: 45 | conditions += parse('{0}{1}'.format(k, v), model) 46 | except ParseError: 47 | pass 48 | else: 49 | continue 50 | 51 | if k in ['and_', 'or_']: 52 | try: 53 | if not isinstance(v, list): 54 | v = json.loads(v) 55 | operation = getattr(sqlalchemy, k) 56 | _conditions = list(itertools.chain.from_iterable( 57 | [parse_dictionary(sv, model) for sv in v])) 58 | conditions.append(operation(*_conditions)) 59 | continue 60 | except (TypeError, ValueError): 61 | raise ParseError("Can't parse expression '{0}'".format(v)) 62 | 63 | attr = getattr(model, k) 64 | 65 | if isinstance(attr, AssociationProxy): 66 | # If the condition is a dict, we must use 'any' method to match 67 | # objects' attributes. 68 | if isinstance(v, dict): 69 | conditions.append(attr.any(**v)) 70 | else: 71 | conditions.append(attr.contains(v)) 72 | 73 | elif hasattr(attr, 'property') and \ 74 | hasattr(attr.property, 'remote_side'): # a relation 75 | for fk in attr.property.remote_side: 76 | conditions.append(sqla_op.eq(fk, v)) 77 | 78 | else: 79 | try: 80 | new_op, v = parse_sqla_operators(v) 81 | attr_op = getattr(attr, new_op, None) 82 | if attr_op is not None: 83 | # try a direct call to named operator on attribute class. 84 | new_filter = attr_op(v) 85 | else: 86 | # try to call custom operator also called "generic" 87 | # operator in SQLAlchemy documentation. 88 | # cf. sqlalchemy.sql.operators.Operators.op() 89 | new_filter = attr.op(new_op)(v) 90 | except (TypeError, ValueError): # json/sql parse error 91 | if isinstance(v, list): # we have an array 92 | new_filter = attr.in_(v) 93 | else: 94 | new_filter = sqla_op.eq(attr, v) 95 | conditions.append(new_filter) 96 | return conditions 97 | 98 | 99 | def parse_sqla_operators(expression): 100 | """ 101 | Parse expressions like: 102 | like("%john%") 103 | ilike("john%") 104 | similar to("%(ohn|acob)") 105 | in("('a','b')") 106 | """ 107 | m = re.match(r"(?P[\w\s]+)\(+(?P.+)\)+", expression) 108 | if m: 109 | o = m.group('operator') 110 | v = json.loads(m.group('value')) 111 | return o, v 112 | 113 | 114 | def parse(expression, model): 115 | """ 116 | Given a python-like conditional statement, returns the equivalent 117 | SQLAlchemy-like query expression. Conditional and boolean operators 118 | (==, <=, >=, !=, >, <) are supported. 119 | """ 120 | v = SQLAVisitor(model) 121 | try: 122 | parsed_expr = ast.parse(expression) 123 | except SyntaxError: 124 | raise ParseError("Can't parse expression '{0}'".format(expression)) 125 | 126 | v.visit(parsed_expr) 127 | return v.sqla_query 128 | 129 | 130 | def parse_sorting(model, query, key, order=1, expression=None): 131 | """ 132 | Sorting parser that works with embedded resources and sql expressions 133 | Moved out from the query (find) method. 134 | """ 135 | if '.' in key: # sort by related mapper class 136 | rel, sort_attr = key.split('.') 137 | rel_class = getattr(model, rel).property.mapper.class_ 138 | query = query.outerjoin(rel_class) 139 | base_sort = getattr(rel_class, sort_attr) 140 | else: 141 | base_sort = getattr(model, key) 142 | 143 | if order == -1: 144 | base_sort = base_sort.desc() 145 | 146 | if expression: # sql expressions 147 | expression = getattr(base_sort, expression) 148 | base_sort = expression() 149 | return base_sort 150 | 151 | 152 | class SQLAVisitor(ast.NodeVisitor): 153 | """Implements the python-to-sql parser. Only Python conditional 154 | statements are supported, however nested, combined with most common compare 155 | and boolean operators (And and Or). 156 | 157 | Supported compare operators: ==, >, <, !=, >=, <= 158 | Supported boolean operators: And, Or 159 | """ 160 | op_mapper = { 161 | ast.Eq: sqla_op.eq, 162 | ast.Gt: sqla_op.gt, 163 | ast.GtE: sqla_op.ge, 164 | ast.Lt: sqla_op.lt, 165 | ast.LtE: sqla_op.le, 166 | ast.NotEq: sqla_op.ne, 167 | ast.Or: sqla_exp.or_, 168 | ast.And: sqla_exp.and_ 169 | } 170 | 171 | def __init__(self, model): 172 | super(SQLAVisitor, self).__init__() 173 | self.model = model 174 | self.sqla_query = [] 175 | self.ops = [] 176 | self.current_value = None 177 | 178 | def visit_Module(self, node): 179 | """ Module handler, our entry point. 180 | """ 181 | self.sqla_query = [] 182 | self.ops = [] 183 | self.current_value = None 184 | 185 | # perform the magic. 186 | self.generic_visit(node) 187 | 188 | # if we didn't obtain a query, it is likely that an unsupported 189 | # python expression has been passed. 190 | if not len(self.sqla_query): 191 | raise ParseError("Only conditional statements with boolean " 192 | "(and, or) and comparison operators are " 193 | "supported.") 194 | 195 | def visit_Expr(self, node): 196 | """ Make sure that we are parsing compare or boolean operators 197 | """ 198 | if not (isinstance(node.value, ast.Compare) or 199 | isinstance(node.value, ast.BoolOp)): 200 | raise ParseError("Will only parse conditional statements") 201 | self.generic_visit(node) 202 | 203 | def visit_Compare(self, node): 204 | """ Compare operator handler. 205 | """ 206 | 207 | self.visit(node.left) 208 | left = getattr(self.model, self.current_value) 209 | 210 | operation = self.op_mapper[node.ops[0].__class__] 211 | 212 | if node.comparators: 213 | comparator = node.comparators[0] 214 | self.visit(comparator) 215 | 216 | value = self.current_value 217 | 218 | if self.ops: 219 | self.ops[-1]['args'].append(operation(left, value)) 220 | else: 221 | self.sqla_query.append(operation(left, value)) 222 | 223 | def visit_BoolOp(self, node): 224 | """ Boolean operator handler. 225 | """ 226 | op = self.op_mapper[node.op.__class__] 227 | self.ops.append({'op': op, 'args': []}) 228 | for value in node.values: 229 | self.visit(value) 230 | 231 | tops = self.ops.pop() 232 | if self.ops: 233 | self.ops[-1]['args'].append(tops['op'](*tops['args'])) 234 | else: 235 | self.sqla_query.append(tops['op'](*tops['args'])) 236 | 237 | def visit_Call(self, node): 238 | # TODO ? 239 | pass 240 | 241 | def visit_Attribute(self, node): 242 | # FIXME ? 243 | self.visit(node.value) 244 | self.current_value += "." + node.attr 245 | 246 | def visit_Name(self, node): 247 | """ Names """ 248 | if node.id.lower() in ['none', 'null']: 249 | self.current_value = None 250 | else: 251 | self.current_value = node.id 252 | 253 | def visit_Num(self, node): 254 | """ Numbers """ 255 | self.current_value = node.n 256 | 257 | def visit_Str(self, node): 258 | """ Strings """ 259 | try: 260 | value = str_to_date(node.s) 261 | self.current_value = value if value is not None else node.s 262 | except ValueError: 263 | self.current_value = node.s 264 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Eve documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Mar 1 17:24:24 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import codecs 15 | import datetime 16 | import os 17 | import sys 18 | 19 | import alabaster 20 | 21 | metadata = {} 22 | with codecs.open(os.path.join('..', 'eve_sqlalchemy', '__about__.py'), 23 | 'r', 'utf-8') as f: 24 | exec(f.read(), metadata) 25 | 26 | # If extensions (or modules to document with autodoc) are in another directory, 27 | # add these directories to sys.path here. If the directory is relative to the 28 | # documentation root, use os.path.abspath to make it absolute, like shown here. 29 | sys.path.append(os.path.abspath('.')) 30 | sys.path.append(os.path.abspath('..')) 31 | sys.path.append(os.path.abspath('_themes')) 32 | 33 | # -- General configuration ----------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be extensions 39 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 40 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'alabaster'] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix of source filenames. 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | # source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = metadata['__title__'] 56 | copyright = u'%s. A RedTurtle Project' % datetime.datetime.now().year 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The full version, including alpha/beta/rc tags. 63 | release = metadata['__version__'] 64 | # The short X.Y version. 65 | version = '.'.join(metadata['__version__'].split('.')[:-1]) 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | # today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | # today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all documents. 82 | # default_role = None 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | # add_function_parentheses = True 86 | 87 | # If true, the current module name will be prepended to all description 88 | # unit titles (such as .. function::). 89 | # add_module_names = True 90 | 91 | # If true, sectionauthor and moduleauthor directives will be shown in the 92 | # output. They are ignored by default. 93 | # show_authors = False 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | # pygments_style = 'sphinx' 97 | 98 | # A list of ignored prefixes for module index sorting. 99 | # modindex_common_prefix = [] 100 | 101 | 102 | # -- Options for HTML output --------------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | # html_theme = 'default' 107 | # html_theme = 'flask' 108 | html_theme = 'alabaster' 109 | 110 | # Theme options are theme-specific and customize the look and feel of a theme 111 | # further. For a list of options available for each theme, see the 112 | # documentation. 113 | # html_theme_options = {'touch_icon': 'touch-icon.png'} 114 | 115 | # Add any paths that contain custom themes here, relative to this directory. 116 | html_theme_path = [alabaster.get_path()] 117 | 118 | # The name for this set of Sphinx documents. If None, it defaults to 119 | # " v documentation". 120 | # html_title = None 121 | 122 | # A shorter title for the navigation bar. Default is the same as html_title. 123 | # html_short_title = None 124 | 125 | # The name of an image file (relative to this directory) to place at the top 126 | # of the sidebar. 127 | # html_logo = "favicon.png" 128 | 129 | # The name of an image file (within the static path) to use as favicon of the 130 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 131 | # pixels large. 132 | html_favicon = "_static/favicon.ico" 133 | 134 | # Add any paths that contain custom static files (such as style sheets) here, 135 | # relative to this directory. They are copied after the builtin static files, 136 | # so a file named "default.css" will overwrite the builtin "default.css". 137 | html_static_path = ['_static'] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | # html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | # html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | # html_sidebars = {} 149 | # html_sidebars = { 150 | # 'index': ['sidebarintro.html', 'searchbox.html', 'sidebarfooter.html'], 151 | # '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', 152 | # 'sourcelink.html', 'searchbox.html'] 153 | # } 154 | html_sidebars = { 155 | '**': [ 156 | 'about.html', 157 | 'navigation.html', 158 | 'sidebarintro.html', 159 | 'searchbox.html', 160 | 'artwork.html', 161 | ] 162 | } 163 | 164 | html_theme_options = { 165 | 'logo': 'eve_leaf.png', 166 | 'github_user': 'pyeve', 167 | 'github_repo': 'eve-sqlalchemy', 168 | 'github_type': 'star', 169 | 'github_banner': 'forkme_right_green_007200.png', 170 | 'travis_button': True, 171 | 'show_powered_by': False, 172 | } 173 | # Additional templates that should be rendered to pages, maps page names to 174 | # template names. 175 | # html_additional_pages = {} 176 | 177 | # If false, no module index is generated. 178 | html_domain_indices = False 179 | # html_use_modindex = False 180 | 181 | # If false, no index is generated. 182 | # html_use_index = True 183 | 184 | # If true, the index is split into individual pages for each letter. 185 | # html_split_index = False 186 | 187 | # If true, links to the reST sources are added to the pages. 188 | html_show_sourcelink = False 189 | 190 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 191 | # html_show_sphinx = True 192 | 193 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 194 | # html_show_copyright = True 195 | 196 | # If true, an OpenSearch description file will be output, and all pages will 197 | # contain a tag referring to it. The value of this option must be the 198 | # base URL from which the finished HTML is served. 199 | # html_use_opensearch = '' 200 | 201 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 202 | # html_file_suffix = None 203 | 204 | # Output file base name for HTML help builder. 205 | htmlhelp_basename = 'EveSQLAlchemydoc' 206 | 207 | 208 | # -- Options for LaTeX output -------------------------------------------------- 209 | 210 | latex_elements = { 211 | # The paper size ('letterpaper' or 'a4paper'). 212 | # 'papersize': 'letterpaper', 213 | 214 | # The font size ('10pt', '11pt' or '12pt'). 215 | # 'pointsize': '10pt', 216 | 217 | # Additional stuff for the LaTeX preamble. 218 | # 'preamble': '', 219 | } 220 | 221 | # Grouping the document tree into LaTeX files. List of tuples 222 | # (source start file, target name, title, author, documentclass [howto/manual]). 223 | latex_documents = [ 224 | ('index', metadata['__title__'] + '.tex', 225 | metadata['__title__'] + ' Documentation', 226 | metadata['__author__'], 'manual'), 227 | ] 228 | 229 | # The name of an image file (relative to this directory) to place at the top of 230 | # the title page. 231 | # latex_logo = None 232 | 233 | # For "manual" documents, if this is true, then toplevel headings are parts, 234 | # not chapters. 235 | # latex_use_parts = False 236 | 237 | # If true, show page references after internal links. 238 | # latex_show_pagerefs = False 239 | 240 | # If true, show URL addresses after external links. 241 | # latex_show_urls = False 242 | 243 | # Documents to append as an appendix to all manuals. 244 | # latex_appendices = [] 245 | 246 | # If false, no module index is generated. 247 | # latex_domain_indices = True 248 | 249 | 250 | # -- Options for manual page output -------------------------------------------- 251 | 252 | # One entry per manual page. List of tuples 253 | # (source start file, name, description, authors, manual section). 254 | man_pages = [ 255 | ('index', metadata['__title__'], metadata['__title__'] + ' Documentation', 256 | [metadata['__author__']], 1) 257 | ] 258 | 259 | # If true, show URL addresses after external links. 260 | # man_show_urls = False 261 | 262 | 263 | # -- Options for Texinfo output ------------------------------------------------ 264 | 265 | # Grouping the document tree into Texinfo files. List of tuples 266 | # (source start file, target name, title, author, 267 | # dir menu entry, description, category) 268 | texinfo_documents = [ 269 | ('index', metadata['__title__'], metadata['__title__'] + ' Documentation', 270 | metadata['__author__'], metadata['__title__'], metadata['__summary__'], 271 | 'Miscellaneous'), 272 | ] 273 | 274 | # Documents to append as an appendix to all manuals. 275 | # texinfo_appendices = [] 276 | 277 | # If false, no module index is generated. 278 | # texinfo_domain_indices = True 279 | 280 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 281 | # texinfo_show_urls = 'footnote' 282 | 283 | 284 | # Example configuration for intersphinx: refer to the Python standard library. 285 | # intersphinx_mapping = {'http://docs.python.org/': None} 286 | # cerberus = 'http://cerberus.readthedocs.org/en/latest/' 287 | # intersphinx_mapping = {'cerberus': ('http://cerberus.readthedocs.org/en/latest/', None)} 288 | 289 | pygments_style = 'flask_theme_support.FlaskyStyle' 290 | 291 | # fall back if theme is not there 292 | try: 293 | __import__('flask_theme_support') 294 | except ImportError as e: 295 | print('-' * 74) 296 | print('Warning: Flask themes unavailable. Building with default theme') 297 | print('If you want the Flask themes, run this command and build again:') 298 | print() 299 | print(' git submodule update --init') 300 | print('-' * 74) 301 | 302 | pygments_style = 'tango' 303 | html_theme = 'default' 304 | html_theme_options = {} 305 | --------------------------------------------------------------------------------