├── 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 |
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 |
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 |
--------------------------------------------------------------------------------