├── .github ├── ISSUE_TEMPLATE.md ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS ├── CHANGES.rst ├── CONTRIBUTING.rst ├── FUNDING.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── SECURITY.md ├── artwork ├── LICENSE ├── eve-sidebar.png ├── eve_leaf.png ├── eve_logo.png ├── favicon.ico ├── favicon.png ├── forkme_right_green_007200.png ├── logo.ai └── logo.pdf ├── docs ├── Makefile ├── _static │ ├── backers │ │ └── blokt.png │ ├── eve-sidebar.png │ ├── eve_leaf.png │ ├── favicon.ico │ ├── favicon.png │ └── forkme_right_green_007200.png ├── _templates │ ├── artwork.html │ ├── layout.html │ └── sidebarintro.html ├── _themes │ ├── .gitignore │ ├── LICENSE │ ├── README │ ├── flask │ │ ├── layout.html │ │ ├── relations.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ ├── flask_small │ │ ├── layout.html │ │ ├── static │ │ │ └── flasky.css_t │ │ └── theme.conf │ └── flask_theme_support.py ├── authentication.rst ├── authors.rst ├── changelog.rst ├── conf.py ├── config.rst ├── contributing.rst ├── extensions.rst ├── features.rst ├── foreword.rst ├── funding.rst ├── index.rst ├── install.rst ├── license.rst ├── quickstart.rst ├── requirements.txt ├── rest_api_for_humans.rst ├── snippets │ ├── hooks_blueprints.rst │ ├── index.rst │ ├── list_of_items.rst │ └── template.rst ├── support.rst ├── tutorials │ ├── account_management.rst │ ├── custom_idfields.rst │ └── index.rst ├── updates.rst └── validation.rst ├── eve ├── __init__.py ├── auth.py ├── default_settings.py ├── endpoints.py ├── exceptions.py ├── flaskapp.py ├── io │ ├── __init__.py │ ├── base.py │ ├── media.py │ └── mongo │ │ ├── __init__.py │ │ ├── flask_pymongo.py │ │ ├── geo.py │ │ ├── media.py │ │ ├── mongo.py │ │ ├── parser.py │ │ └── validation.py ├── logging.py ├── methods │ ├── __init__.py │ ├── common.py │ ├── delete.py │ ├── get.py │ ├── patch.py │ ├── post.py │ └── put.py ├── render.py ├── utils.py ├── validation.py └── versioning.py ├── examples ├── README ├── notifications.py ├── notifications_settings.py └── security │ ├── README │ ├── bcrypt.py │ ├── hmac.py │ ├── roles.py │ ├── settings_security.py │ ├── sha1-hmac.py │ └── token.py ├── pyproject.toml ├── pytest.ini ├── setup.py ├── tests ├── __init__.py ├── auth.py ├── config.py ├── endpoints.py ├── methods │ ├── __init__.py │ ├── common.py │ ├── delete.py │ ├── get.py │ ├── patch.py │ ├── patch_atomic_concurrency.py │ ├── post.py │ ├── put.py │ └── ratelimit.py ├── renders.py ├── response.py ├── suite_generator.py ├── test.db ├── test_io │ ├── __init__.py │ ├── flask_pymongo.py │ ├── media.py │ ├── mongo.py │ └── multi_mongo.py ├── test_logging.py ├── test_prefix.py ├── test_prefix_version.py ├── test_settings.py ├── test_settings_env.py ├── test_version.py ├── utils.py └── versioning.py └── tox.ini /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **This issue tracker is a tool to address bugs in Eve itself. 2 | Please use Stack Overflow for general questions about using Eve or issues not 3 | related to Eve (see http://python-eve.org/support).** 4 | 5 | If you'd like to report a bug in Eve, fill out the template below. Provide 6 | any any extra information that may be useful / related to your problem. 7 | Ideally, create an [MCVE](http://stackoverflow.com/help/mcve), which helps us 8 | understand the problem and helps check that it is not caused by something in 9 | your code. 10 | 11 | --- 12 | 13 | ### Expected Behavior 14 | 15 | Tell us what should happen. 16 | 17 | ```python 18 | Paste a minimal example that causes the problem. 19 | ``` 20 | 21 | ### Actual Behavior 22 | 23 | Tell us what happens instead. 24 | 25 | ```pytb 26 | Paste the full traceback if there was an exception. 27 | ``` 28 | 29 | ### Environment 30 | 31 | * Python version: 32 | * Eve version: 33 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 180 2 | staleLabel: stale 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | tests: 7 | name: ${{ matrix.name }} 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | include: 13 | - { name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312, mongodb: '5.0', redis: '6' } 14 | - { name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311, mongodb: '5.0', redis: '6' } 15 | - { name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310, mongodb: '5.0', redis: '6' } 16 | - { name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39, mongodb: '5.0', redis: '6' } 17 | - { name: 'PyPy', python: 'pypy-3.10', os: ubuntu-latest, tox: pypy310, mongodb: '4.4', redis: '6' } 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python }} 24 | - uses: supercharge/mongodb-github-action@1.12.0 25 | with: 26 | mongodb-version: ${{ matrix.mongodb }} 27 | - uses: supercharge/redis-github-action@1.2.0 28 | with: 29 | redis-version: ${{ matrix.redis }} 30 | - name: Install dependencies 31 | run: | 32 | set -xe 33 | python -VV 34 | python -m site 35 | python -m pip install --upgrade pip setuptools wheel 36 | python -m pip install --upgrade virtualenv tox tox-gh-actions 37 | - name: 🍃 Install mongosh 38 | run: | 39 | sudo apt-get update 40 | sudo apt-get install -y wget gnupg 41 | wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add - 42 | echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list 43 | sudo apt-get update 44 | sudo apt-get install -y mongodb-mongosh 45 | - name: Start mongo ${{ matrix.mongodb }} 46 | run: | 47 | mongosh eve_test --eval 'db.createUser({user:"test_user", pwd:"test_pw", roles:["readWrite"]});' 48 | - name: Run tox targets for ${{ matrix.python }} 49 | run: tox -e ${{ matrix.tox }} 50 | -------------------------------------------------------------------------------- /.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 | 57 | #OSX 58 | .Python 59 | .DS_Store 60 | 61 | #Sphinx 62 | _build 63 | 64 | # PyCharm 65 | .idea 66 | 67 | .cache 68 | .vscode 69 | .pytest_cache 70 | pip-wheel-metadata/ 71 | !/.eggs/ 72 | .venv/ 73 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 23.3.0 4 | hooks: 5 | - id: black 6 | language_version: python3.9 7 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | 3 | version: 2 4 | 5 | build: 6 | os: ubuntu-22.04 7 | tools: 8 | python: "3.11" 9 | 10 | # Build documentation in the docs/ directory with Sphinx 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Eve is written and maintained by Nicola Iarocci and 2 | various contributors: 3 | 4 | Development Lead 5 | ```````````````` 6 | 7 | - Nicola Iarocci 8 | 9 | Patches and Contributions 10 | ````````````````````````` 11 | 12 | - Aayush Sarva 13 | - Adam Walsh 14 | - Adrian Cin 15 | - Alberto Marin 16 | - Alex Misk 17 | - Alexander Dietmüller 18 | - Alexander Hendorf 19 | - Alexander Miskaryan 20 | - Amedeo Bussi 21 | - Andreas Røssland 22 | - Andrés Martano 23 | - Antonio Lourenco 24 | - Arnau Orriols 25 | - Artem Kolesnikov 26 | - Arthur Burkart 27 | - Ashley Roach 28 | - Ben Demaree 29 | - Bjorn Andersson 30 | - Brad P. Crochet 31 | - Bret Curtis 32 | - Brian Mego 33 | - Bryan Cattle 34 | - Carl George 35 | - Carles Bruguera 36 | - Chen Rotem 37 | - Christian Henke 38 | - Christoph Witzany 39 | - Christopher Larsen 40 | - Chuck Turco 41 | - Conrad Burchert 42 | - Cyprien Pannier 43 | - Cyril Bonnard 44 | - DHuan 45 | - Daniel Lytkin 46 | - Daniele Pizzolli 47 | - Danse 48 | - David Arnold 49 | - David Booss 50 | - David Buchmann 51 | - David Murphy 52 | - David Wood 53 | - Dmitry Anoshin 54 | - Dominik Kellner 55 | - Dong Wei Ming 56 | - Dougal Matthews 57 | - Einar Huseby 58 | - Elias García 59 | - Emmanuel Leblond 60 | - Eugene Prikazchikov 61 | - Ewan Higgs 62 | - Felix Peppert 63 | - Florian Rathgeber 64 | - Fouad Chennou 65 | - Francisco Corrales Morales 66 | - Garrin Kimmell 67 | - George Lestaris 68 | - Gianfranco Palumbo 69 | - Gino Zhang 70 | - Giorgos Margaritis 71 | - Gonéri Le Bouder 72 | - Grisha K. 73 | - Guillaume Le Pape 74 | - Guillaume Royer 75 | - Gustavo Vargas 76 | - Hamdy 77 | - Hannes Tiede 78 | - Harro van der Klauw 79 | - Hasan Pekdemir 80 | - Henrique Barroso 81 | - Huan Di 82 | - Hugo Larcher 83 | - Hung Le 84 | - James Stewart 85 | - Jaroslav Semančík 86 | - Javier Gonel 87 | - Javier Jiménez 88 | - Jean Boussier 89 | - Jeff Zhang 90 | - Jen Montes 91 | - Jeremy Solbrig 92 | - Joakim Uddholm 93 | - Johan Bloemberg 94 | - John Chang 95 | - John Deng 96 | - Jorge Morales 97 | - Jorge Puente Sarrín 98 | - Joseph Heck 99 | - Josh Villbrandt 100 | - Juan Madurga 101 | - Julian Hille 102 | - Julien Barbot 103 | - Junior Vidotti 104 | - Kai Danielmeier 105 | - Kelly Caylor 106 | - Ken Carpenter 107 | - Kevin Bowrin 108 | - Kevin Funk 109 | - Kevin Roy 110 | - Kracekumar 111 | - Kris Lambrechts 112 | - Kurt Bonne 113 | - Kurt Doherty 114 | - Luca Di Gaspero 115 | - Luca Moretto 116 | - Luis Fernando Gomes 117 | - Magdas Adrian 118 | - Mamurjon Saitbaev 119 | - Mandar Vaze 120 | - Manquer 121 | - Marc Abramowitz 122 | - Marcelo Trylesinski 123 | - Marcin Puhacz 124 | - Marcus Cobden 125 | - Marica Odagaki 126 | - Mario Kralj 127 | - Mark Mayo 128 | - Marsch Huynh 129 | - Martin Fous 130 | - Massimo Scamarcia 131 | - Mateusz Łoskot 132 | - Matt Creenan 133 | - Matt Tucker 134 | - Matthew Ellison 135 | - Matthieu Prat 136 | - Mattias Lundberg 137 | - Mayur Dhamanwala 138 | - Michael Maxwell 139 | - Mikael Berg 140 | - Miroslav Šedivý 141 | - Moritz Schneider 142 | - Mugur Rus 143 | - Nathan Reynolds 144 | - Niall Donegan 145 | - Nick Park 146 | - Nicolas Bazire 147 | - Nicolas Carlier 148 | - Oleg Pshenichniy 149 | - Olivier Carrère 150 | - Olivier Poitrey 151 | - Olof Johansson 152 | - Ondrej Slinták 153 | - Or Neeman 154 | - Orange Tsai 155 | - Pablo Parada 156 | - Pahaz Blinov 157 | - Patricia Ramos 158 | - Patrick Decat 159 | - Pau Freixes 160 | - Paul Doucet 161 | - Pedro Rodrigues 162 | - Peter Darrow 163 | - Petr Jašek 164 | - Phone Myint Kyaw 165 | - Pieter De Clercq 166 | - Prajjwal Nijhara 167 | - Prayag Verma 168 | - Qiang Zhang 169 | - Raghuram Devarakonda 170 | - Rahul Salgare 171 | - Ralph Smith 172 | - Raychee 173 | - Robert Wlodarczyk 174 | - Roberto 'Kalamun' Pasini 175 | - Rodrigo Rodriguez 176 | - Roller Angel 177 | - Roman Gavrilov 178 | - Ronan Delacroix 179 | - Roy Smith 180 | - Ryan Shea 181 | - Sam Luu 182 | - Samuel Sutch 183 | - Samuli Tuomola 184 | - Saurabh Shandilya 185 | - Sebastien Estienne 186 | - Sebastián Magrí 187 | - Serge Kir 188 | - Shaoyu Meng 189 | - Simon Schönfeld 190 | - Sobolev Nikita 191 | - Stanislav Filin 192 | - Stanislav Heller 193 | - Stefaan Ghysels 194 | - Stratos Gerakakis 195 | - Svante Bengtson 196 | - Sybren A. Stüvel 197 | - Tadej Magajn 198 | - Tano Abeleyra 199 | - Taylor Brown 200 | - Thomas Sileo 201 | - Tim Gates 202 | - Tim Jacobi 203 | - Tomasz Jezierski 204 | - Tyler Kennedy 205 | - Valerie Coffman 206 | - Vasilis Lolis 207 | - Vincent Bisserie 208 | - Wael M. Nasreddine 209 | - Wan Bachtiar 210 | - Wei Guan 211 | - Wytamma Wirth 212 | - Xavi Cubillas 213 | - boosh 214 | - dccrazyboy 215 | - kinuax 216 | - kreynen 217 | - mmizotin 218 | - quentinpraz 219 | - smeng9 220 | - tgm 221 | - xgdgsc 222 | -------------------------------------------------------------------------------- /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 | Support questions 10 | ----------------- 11 | 12 | Please, don't use the issue tracker for this. Use one of the following 13 | resources for questions about your own code: 14 | 15 | * Ask on `Stack Overflow`_. Search with Google first using: ``site:stackoverflow.com eve {search term, exception message, etc.}`` 16 | * The `mailing list`_ is intended to be a low traffic resource for both developers/contributors and API maintainers looking for help or requesting feedback. 17 | * The IRC channel ``#python-eve`` on FreeNode. 18 | 19 | .. _Stack Overflow: https://stackoverflow.com/questions/tagged/eve?sort=linked 20 | .. _`mailing list`: https://groups.google.com/forum/#!forum/python-eve 21 | 22 | Reporting issues 23 | ---------------- 24 | 25 | - Describe what you expected to happen. 26 | - If possible, include a `minimal, complete, and verifiable example`_ to help 27 | us identify the issue. This also helps check that the issue is not with your 28 | own code. 29 | - Describe what actually happened. Include the full traceback if there was an 30 | exception. 31 | - List your Python and Eve versions. If possible, check if this issue is 32 | already fixed in the repository. 33 | 34 | .. _minimal, complete, and verifiable example: https://stackoverflow.com/help/mcve 35 | 36 | Submitting patches 37 | ------------------ 38 | 39 | - Include tests if your patch is supposed to solve a bug, and explain 40 | clearly under which circumstances the bug happens. Make sure the test fails 41 | without your patch. 42 | - Enable and install pre-commit_ to ensure styleguides and codechecks are 43 | followed. CI will reject a change that does not conform to the guidelines. 44 | 45 | .. _pre-commit: https://pre-commit.com/ 46 | 47 | First time setup 48 | ~~~~~~~~~~~~~~~~ 49 | 50 | - Download and install the `latest version of git`_. 51 | - Configure git with your `username`_ and `email`_:: 52 | 53 | git config --global user.name 'your name' 54 | git config --global user.email 'your email' 55 | 56 | - Make sure you have a `GitHub account`_. 57 | - Fork Eve to your GitHub account by clicking the `Fork`_ button. 58 | - `Clone`_ your GitHub fork locally:: 59 | 60 | git clone https://github.com/{username}/eve 61 | cd eve 62 | 63 | - Add the main repository as a remote to update later:: 64 | 65 | git remote add pyeve https://github.com/pyeve/eve 66 | git fetch pyeve 67 | 68 | - Create a virtualenv:: 69 | 70 | python3 -m venv env 71 | . env/bin/activate 72 | # or "env\Scripts\activate" on Windows 73 | 74 | - Install Eve in editable mode with development dependencies:: 75 | 76 | pip install -e ".[dev]" 77 | 78 | - Install pre-commit_ and then activate its hooks. pre-commit is a framework for managing and maintaining multi-language pre-commit hooks. Eve uses pre-commit to ensure code-style and code formatting is the same:: 79 | 80 | $ pip install --user pre-commit 81 | $ pre-commit install 82 | 83 | Afterwards, pre-commit will run whenever you commit. 84 | 85 | 86 | .. _GitHub account: https://github.com/join 87 | .. _latest version of git: https://git-scm.com/downloads 88 | .. _username: https://help.github.com/articles/setting-your-username-in-git/ 89 | .. _email: https://help.github.com/articles/setting-your-email-in-git/ 90 | .. _Fork: https://github.com/pyeve/eve/fork 91 | .. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork 92 | 93 | Start coding 94 | ~~~~~~~~~~~~ 95 | 96 | - Create a branch to identify the issue you would like to work on (e.g. 97 | ``fix_for_#1280``) 98 | - Using your favorite editor, make your changes, `committing as you go`_. 99 | - Follow `PEP8`_. 100 | - Include tests that cover any code changes you make. Make sure the test fails 101 | without your patch. `Run the tests. `_. 102 | - Push your commits to GitHub and `create a pull request`_. 103 | - Celebrate 🎉 104 | 105 | .. _committing as you go: http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes 106 | .. _PEP8: https://pep8.org/ 107 | .. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ 108 | 109 | .. _contributing-testsuite: 110 | 111 | Running the tests 112 | ~~~~~~~~~~~~~~~~~ 113 | 114 | You should have Python 3.9+ available in your system. Now 115 | running tests is as simple as issuing this command:: 116 | 117 | $ tox -e linting,py310,py39 118 | 119 | This command will run tests via the "tox" tool against Python 3.10 and 3.9 and 120 | also perform "lint" coding-style checks. 121 | 122 | You can pass different options to ``tox``. For example, to run tests on Python 123 | 3.10 and pass options to pytest (e.g. enter pdb on failure) to pytest you can 124 | do:: 125 | 126 | $ tox -e py310 -- --pdb 127 | 128 | Or to only run tests in a particular test module on Python 3.6:: 129 | 130 | $ tox -e py310 -- -k TestGet 131 | 132 | CI will run the full suite when you submit your pull request. The full 133 | test suite takes a long time to run because it tests multiple combinations of 134 | Python and dependencies. You need to have Python 3.9, 3.10, 3.11, 3.12 and PyPy 135 | installed to run all of the environments. Then run:: 136 | 137 | tox 138 | 139 | Please note that you need an active MongoDB instance running on localhost in 140 | order for the tests run. Save yourself some time and headache by creating a 141 | MongoDB user with the password defined in the `test_settings.py` file in the 142 | admin database (the pre-commit process is unforgiving if you don't want to 143 | commit your admin credentials but still have the file modified, which would be 144 | necessary for tox). If you want to run a local MongoDB instance along with an 145 | SSH tunnel to a remote instance, if you can, have the local use the default 146 | port and the remote use some other port. If you can't, fixing the tests that 147 | won't play nicely is probably more trouble than connecting to the remote and 148 | local instances one at a time. Also, be advised that in order to execute the 149 | :ref:`ratelimiting` tests you need a running Redis_ server. The Rate-Limiting 150 | tests are silently skipped if any of the two conditions are not met. 151 | 152 | Building the docs 153 | ~~~~~~~~~~~~~~~~~ 154 | Build the docs in the ``docs`` directory using Sphinx:: 155 | 156 | cd docs 157 | make html 158 | 159 | Open ``_build/html/index.html`` in your browser to view the docs. 160 | 161 | Read more about `Sphinx `_. 162 | 163 | make targets 164 | ~~~~~~~~~~~~ 165 | Eve provides a ``Makefile`` with various shortcuts. They will ensure that 166 | all dependencies are installed. 167 | 168 | - ``make test`` runs the basic test suite with ``pytest`` 169 | - ``make test-all`` runs the full test suite with ``tox`` 170 | - ``make docs`` builds the HTML documentation 171 | - ``make check`` performs some checks on the package 172 | - ``make install-dev`` install Eve in editable mode with all development dependencies. 173 | 174 | First time contributor? 175 | ----------------------- 176 | It's alright. We've all been there. See next chapter. 177 | 178 | Don't know where to start? 179 | -------------------------- 180 | There are usually several TODO comments scattered around the codebase, maybe 181 | check them out and see if you have ideas, or can help with them. Also, check 182 | the `open issues`_ in case there's something that sparks your interest. And 183 | what about documentation? I suck at English, so if you're fluent with it (or 184 | notice any typo and/or mistake), why not help with that? In any case, other 185 | than GitHub help_ pages, you might want to check this excellent `Effective 186 | Guide to Pull Requests`_ 187 | 188 | .. _`the repository`: http://github.com/pyeve/eve 189 | .. _AUTHORS: https://github.com/pyeve/eve/blob/master/AUTHORS 190 | .. _`open issues`: https://github.com/pyeve/eve/issues 191 | .. _`new issue`: https://github.com/pyeve/eve/issues/new 192 | .. _GitHub: https://github.com/ 193 | .. _`proper format`: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 194 | .. _flake8: http://flake8.readthedocs.org/en/latest/ 195 | .. _tox: http://tox.readthedocs.org/en/latest/ 196 | .. _help: https://help.github.com/ 197 | .. _`Effective Guide to Pull Requests`: http://codeinthehole.com/writing/pull-requests-and-other-good-practices-for-teams-using-github/ 198 | .. _`fork and edit`: https://github.com/blog/844-forking-with-the-edit-button 199 | .. _`Pull Request`: https://help.github.com/articles/creating-a-pull-request 200 | .. _`running the tests`: http://python-eve.org/testing#running-the-tests 201 | .. _Redis: https://redis.io 202 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nicolaiarocci 2 | patreon: nicolaiarocci 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 by Nicola Iarocci and contributors. See AUTHORS 2 | for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst LICENSE AUTHORS README.rst 2 | recursive-include tests * 3 | recursive-include docs * 4 | recursive-include examples * 5 | recursive-exclude docs *.pyc 6 | recursive-exclude docs *.pyo 7 | recursive-exclude tests *.pyc 8 | recursive-exclude tests *.pyo 9 | recursive-exclude examples *.pyo 10 | recursive-exclude examples *.pyc 11 | recursive-exclude * __pycache__ 12 | prune docs/_build 13 | prune docs/_themes/.git 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all install-dev test test-all tox docs audit clean-pyc docs-upload wheel 2 | 3 | install-dev: 4 | pip install -q -e .[dev] 5 | 6 | test: clean-pyc install-dev 7 | pytest 8 | 9 | test-all: clean-pyc install-dev 10 | tox 11 | 12 | tox: test-all 13 | 14 | wheel: 15 | python setup.py sdist bdist_wheel 16 | 17 | BUILDDIR = _build 18 | docs: install-dev 19 | $(MAKE) -C docs html BUILDDIR=$(BUILDDIR) 20 | 21 | check: 22 | python setup.py check -r -s 23 | 24 | clean-pyc: 25 | @find . -name '*.pyc' -exec rm -f {} + 26 | @find . -name '*.pyo' -exec rm -f {} + 27 | @find . -name '*~' -exec rm -f {} + 28 | 29 | # Only useful on Nicola's own machine :-) 30 | docs-upload: BUILDDIR = ~/code/eve.docs 31 | docs-upload: docs 32 | cd $(BUILDDIR)/html && \ 33 | git commit -am "rebuild docs" && \ 34 | git push 35 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Eve 2 | ==== 3 | .. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square 4 | :target: https://pypi.org/project/eve 5 | 6 | .. image:: https://github.com/pyeve/eve/workflows/CI/badge.svg 7 | :target: https://github.com/pyeve/eve/actions?query=workflow%3ACI 8 | 9 | .. image:: https://img.shields.io/pypi/pyversions/eve.svg?style=flat-square 10 | :target: https://pypi.org/project/eve 11 | 12 | .. image:: https://img.shields.io/badge/license-BSD-blue.svg?style=flat-square 13 | :target: https://en.wikipedia.org/wiki/BSD_License 14 | 15 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 16 | :target: https://github.com/ambv/black 17 | 18 | Eve is an open source Python REST API framework designed for human beings. It 19 | allows to effortlessly build and deploy highly customizable, fully featured 20 | RESTful Web Services. Eve offers native support for MongoDB, and SQL backends 21 | via community extensions. 22 | 23 | Eve is Simple 24 | ------------- 25 | .. code-block:: python 26 | 27 | from eve import Eve 28 | 29 | app = Eve() 30 | app.run() 31 | 32 | The API is now live, ready to be consumed: 33 | 34 | .. code-block:: console 35 | 36 | $ curl -i http://example.com/people 37 | HTTP/1.1 200 OK 38 | 39 | All you need to bring your API online is a database, a configuration file 40 | (defaults to ``settings.py``) and a launch script. Overall, you will find that 41 | configuring and fine-tuning your API is a very simple process. 42 | 43 | `Check out the Eve Website `_ 44 | 45 | Features 46 | -------- 47 | * Emphasis on REST 48 | * Full range of CRUD operations 49 | * Customizable resource endpoints 50 | * Customizable, multiple item endpoints 51 | * Filtering and Sorting 52 | * Pagination 53 | * HATEOAS 54 | * JSON and XML Rendering 55 | * Conditional Requests 56 | * Data Integrity and Concurrency Control 57 | * Bulk Inserts 58 | * Data Validation 59 | * Extensible Data Validation 60 | * Resource-level Cache Control 61 | * API Versioning 62 | * Document Versioning 63 | * Authentication 64 | * CORS Cross-Origin Resource Sharing 65 | * JSONP 66 | * Read-only by default 67 | * Default Values 68 | * Predefined Database Filters 69 | * Projections 70 | * Embedded Resource Serialization 71 | * Event Hooks 72 | * Rate Limiting 73 | * Custom ID Fields 74 | * File Storage 75 | * GeoJSON 76 | * Internal Resources 77 | * Enhanced Logging 78 | * Operations Log 79 | * MongoDB Aggregation Framework 80 | * MongoDB and SQL Support 81 | * Powered by Flask 82 | 83 | Funding 84 | ------- 85 | Eve REST framework is a open source, collaboratively funded project. If you run 86 | a business and are using Eve in a revenue-generating product, it would make 87 | business sense to sponsor Eve development: it ensures the project that your 88 | product relies on stays healthy and actively maintained. Individual users are 89 | also welcome to make a recurring pledge or a one time donation if Eve has 90 | helped you in your work or personal projects. 91 | 92 | Every single sign-up makes a significant impact towards making Eve possible. To 93 | learn more, check out our `funding page`_. 94 | 95 | License 96 | ------- 97 | Eve is a `Nicola Iarocci`_ open source project, 98 | distributed under the `BSD license 99 | `_. 100 | 101 | .. _`Nicola Iarocci`: http://nicolaiarocci.com 102 | .. _`funding page`: http://python-eve.org/funding.html 103 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please email pyeve at nicolaiarocci dot com any vulnerability you may find about this project. 6 | -------------------------------------------------------------------------------- /artwork/eve-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/eve-sidebar.png -------------------------------------------------------------------------------- /artwork/eve_leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/eve_leaf.png -------------------------------------------------------------------------------- /artwork/eve_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/eve_logo.png -------------------------------------------------------------------------------- /artwork/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/favicon.ico -------------------------------------------------------------------------------- /artwork/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/favicon.png -------------------------------------------------------------------------------- /artwork/forkme_right_green_007200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/forkme_right_green_007200.png -------------------------------------------------------------------------------- /artwork/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/logo.ai -------------------------------------------------------------------------------- /artwork/logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/artwork/logo.pdf -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | #BUILDDIR = ~/code/eve.docs 10 | 11 | # Internal variables. 12 | PAPEROPT_a4 = -D latex_paper_size=a4 13 | PAPEROPT_letter = -D latex_paper_size=letter 14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 15 | # the i18n builder cannot share the environment and doctrees with the others 16 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 17 | 18 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and a HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " texinfo to make Texinfo files" 36 | @echo " info to make Texinfo files and run them through makeinfo" 37 | @echo " gettext to make PO message catalogs" 38 | @echo " changes to make an overview of all changed/added/deprecated items" 39 | @echo " linkcheck to check all external links for integrity" 40 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 41 | 42 | clean: 43 | -rm -rf $(BUILDDIR)/* 44 | 45 | html: 46 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 47 | @echo 48 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 49 | 50 | dirhtml: 51 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 52 | @echo 53 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 54 | 55 | singlehtml: 56 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 57 | @echo 58 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 59 | 60 | pickle: 61 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 62 | @echo 63 | @echo "Build finished; now you can process the pickle files." 64 | 65 | json: 66 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 67 | @echo 68 | @echo "Build finished; now you can process the JSON files." 69 | 70 | htmlhelp: 71 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 72 | @echo 73 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 74 | ".hhp project file in $(BUILDDIR)/htmlhelp." 75 | 76 | qthelp: 77 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 78 | @echo 79 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 80 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 81 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Eve.qhcp" 82 | @echo "To view the help file:" 83 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Eve.qhc" 84 | 85 | devhelp: 86 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 87 | @echo 88 | @echo "Build finished." 89 | @echo "To view the help file:" 90 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Eve" 91 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Eve" 92 | @echo "# devhelp" 93 | 94 | epub: 95 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 96 | @echo 97 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 98 | 99 | latex: 100 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 101 | @echo 102 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 103 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 104 | "(use \`make latexpdf' here to do that automatically)." 105 | 106 | latexpdf: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo "Running LaTeX files through pdflatex..." 109 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 110 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 111 | 112 | text: 113 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 114 | @echo 115 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 116 | 117 | man: 118 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 119 | @echo 120 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 121 | 122 | texinfo: 123 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 124 | @echo 125 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 126 | @echo "Run \`make' in that directory to run these through makeinfo" \ 127 | "(use \`make info' here to do that automatically)." 128 | 129 | info: 130 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 131 | @echo "Running Texinfo files through makeinfo..." 132 | make -C $(BUILDDIR)/texinfo info 133 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 134 | 135 | gettext: 136 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 137 | @echo 138 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 139 | 140 | changes: 141 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 142 | @echo 143 | @echo "The overview file is in $(BUILDDIR)/changes." 144 | 145 | linkcheck: 146 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 147 | @echo 148 | @echo "Link check complete; look for any errors in the above output " \ 149 | "or in $(BUILDDIR)/linkcheck/output.txt." 150 | 151 | doctest: 152 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 153 | @echo "Testing of doctests in the sources finished, look at the " \ 154 | "results in $(BUILDDIR)/doctest/output.txt." 155 | -------------------------------------------------------------------------------- /docs/_static/backers/blokt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/docs/_static/backers/blokt.png -------------------------------------------------------------------------------- /docs/_static/eve-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/docs/_static/eve-sidebar.png -------------------------------------------------------------------------------- /docs/_static/eve_leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/docs/_static/eve_leaf.png -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/docs/_static/favicon.png -------------------------------------------------------------------------------- /docs/_static/forkme_right_green_007200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/docs/_static/forkme_right_green_007200.png -------------------------------------------------------------------------------- /docs/_templates/artwork.html: -------------------------------------------------------------------------------- 1 |

2 | Artwork by Kalamun © 2013 3 |

4 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {% block footer %} 4 | {{ super() }} 5 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /docs/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

Stay Informed

2 |

Receive updates on new releases and upcoming projects.

3 | 4 |

6 | 7 |

8 | 9 |

11 | 12 |

Join Mailing List.

13 | 14 |

Eve Course

15 |

This course will teach you how to build RESTful services with Eve and MongoDB.

16 |

The teacher is the project creator and maintainer.

17 | 21 | 22 |

Useful Links

23 | 32 | 33 |

Other Projects

34 | 35 |

More Nicola Iarocci projects:

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

Related Topics

2 | 20 | -------------------------------------------------------------------------------- /docs/_themes/flask/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | pygments_style = flask_theme_support.FlaskyStyle 5 | 6 | [options] 7 | index_logo = 8 | index_logo_height = 120px 9 | touch_icon = 10 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "basic/layout.html" %} 2 | {% block header %} 3 | {{ super() }} 4 | {% if pagename == 'index' %} 5 |
6 | {% endif %} 7 | {% endblock %} 8 | {% block footer %} 9 | {% if pagename == 'index' %} 10 |
11 | {% endif %} 12 | {% endblock %} 13 | {# do not display relbars #} 14 | {% block relbar1 %}{% endblock %} 15 | {% block relbar2 %} 16 | {% if theme_github_fork %} 17 | Fork me on GitHub 19 | {% endif %} 20 | {% endblock %} 21 | {% block sidebar1 %}{% endblock %} 22 | {% block sidebar2 %}{% endblock %} 23 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/static/flasky.css_t: -------------------------------------------------------------------------------- 1 | /* 2 | * flasky.css_t 3 | * ~~~~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- flasky theme based on nature theme. 6 | * 7 | * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | @import url("basic.css"); 13 | 14 | /* -- page layout ----------------------------------------------------------- */ 15 | 16 | body { 17 | font-family: 'Georgia', serif; 18 | font-size: 17px; 19 | color: #000; 20 | background: white; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | div.documentwrapper { 26 | float: left; 27 | width: 100%; 28 | } 29 | 30 | div.bodywrapper { 31 | margin: 40px auto 0 auto; 32 | width: 700px; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #ffffff; 41 | color: #3E4349; 42 | padding: 0 30px 30px 30px; 43 | } 44 | 45 | img.floatingflask { 46 | padding: 0 0 10px 10px; 47 | float: right; 48 | } 49 | 50 | div.footer { 51 | text-align: right; 52 | color: #888; 53 | padding: 10px; 54 | font-size: 14px; 55 | width: 650px; 56 | margin: 0 auto 40px auto; 57 | } 58 | 59 | div.footer a { 60 | color: #888; 61 | text-decoration: underline; 62 | } 63 | 64 | div.related { 65 | line-height: 32px; 66 | color: #888; 67 | } 68 | 69 | div.related ul { 70 | padding: 0 0 0 10px; 71 | } 72 | 73 | div.related a { 74 | color: #444; 75 | } 76 | 77 | /* -- body styles ----------------------------------------------------------- */ 78 | 79 | a { 80 | color: #004B6B; 81 | text-decoration: underline; 82 | } 83 | 84 | a:hover { 85 | color: #6D4100; 86 | text-decoration: underline; 87 | } 88 | 89 | div.body { 90 | padding-bottom: 40px; /* saved for footer */ 91 | } 92 | 93 | div.body h1, 94 | div.body h2, 95 | div.body h3, 96 | div.body h4, 97 | div.body h5, 98 | div.body h6 { 99 | font-family: 'Garamond', 'Georgia', serif; 100 | font-weight: normal; 101 | margin: 30px 0px 10px 0px; 102 | padding: 0; 103 | } 104 | 105 | {% if theme_index_logo %} 106 | div.indexwrapper h1 { 107 | text-indent: -999999px; 108 | background: url({{ theme_index_logo }}) no-repeat center center; 109 | height: {{ theme_index_logo_height }}; 110 | } 111 | {% endif %} 112 | 113 | div.body h2 { font-size: 180%; } 114 | div.body h3 { font-size: 150%; } 115 | div.body h4 { font-size: 130%; } 116 | div.body h5 { font-size: 100%; } 117 | div.body h6 { font-size: 100%; } 118 | 119 | a.headerlink { 120 | color: white; 121 | padding: 0 4px; 122 | text-decoration: none; 123 | } 124 | 125 | a.headerlink:hover { 126 | color: #444; 127 | background: #eaeaea; 128 | } 129 | 130 | div.body p, div.body dd, div.body li { 131 | line-height: 1.4em; 132 | } 133 | 134 | div.admonition { 135 | background: #fafafa; 136 | margin: 20px -30px; 137 | padding: 10px 30px; 138 | border-top: 1px solid #ccc; 139 | border-bottom: 1px solid #ccc; 140 | } 141 | 142 | div.admonition p.admonition-title { 143 | font-family: 'Garamond', 'Georgia', serif; 144 | font-weight: normal; 145 | font-size: 24px; 146 | margin: 0 0 10px 0; 147 | padding: 0; 148 | line-height: 1; 149 | } 150 | 151 | div.admonition p.last { 152 | margin-bottom: 0; 153 | } 154 | 155 | div.highlight{ 156 | background-color: white; 157 | } 158 | 159 | dt:target, .highlight { 160 | background: #FAF3E8; 161 | } 162 | 163 | div.note { 164 | background-color: #eee; 165 | border: 1px solid #ccc; 166 | } 167 | 168 | div.seealso { 169 | background-color: #ffc; 170 | border: 1px solid #ff6; 171 | } 172 | 173 | div.topic { 174 | background-color: #eee; 175 | } 176 | 177 | div.warning { 178 | background-color: #ffe4e4; 179 | border: 1px solid #f66; 180 | } 181 | 182 | p.admonition-title { 183 | display: inline; 184 | } 185 | 186 | p.admonition-title:after { 187 | content: ":"; 188 | } 189 | 190 | pre, tt { 191 | font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 192 | font-size: 0.85em; 193 | } 194 | 195 | img.screenshot { 196 | } 197 | 198 | tt.descname, tt.descclassname { 199 | font-size: 0.95em; 200 | } 201 | 202 | tt.descname { 203 | padding-right: 0.08em; 204 | } 205 | 206 | img.screenshot { 207 | -moz-box-shadow: 2px 2px 4px #eee; 208 | -webkit-box-shadow: 2px 2px 4px #eee; 209 | box-shadow: 2px 2px 4px #eee; 210 | } 211 | 212 | table.docutils { 213 | border: 1px solid #888; 214 | -moz-box-shadow: 2px 2px 4px #eee; 215 | -webkit-box-shadow: 2px 2px 4px #eee; 216 | box-shadow: 2px 2px 4px #eee; 217 | } 218 | 219 | table.docutils td, table.docutils th { 220 | border: 1px solid #888; 221 | padding: 0.25em 0.7em; 222 | } 223 | 224 | table.field-list, table.footnote { 225 | border: none; 226 | -moz-box-shadow: none; 227 | -webkit-box-shadow: none; 228 | box-shadow: none; 229 | } 230 | 231 | table.footnote { 232 | margin: 15px 0; 233 | width: 100%; 234 | border: 1px solid #eee; 235 | } 236 | 237 | table.field-list th { 238 | padding: 0 0.8em 0 0; 239 | } 240 | 241 | table.field-list td { 242 | padding: 0; 243 | } 244 | 245 | table.footnote td { 246 | padding: 0.5em; 247 | } 248 | 249 | dl { 250 | margin: 0; 251 | padding: 0; 252 | } 253 | 254 | dl dd { 255 | margin-left: 30px; 256 | } 257 | 258 | pre { 259 | padding: 0; 260 | margin: 15px -30px; 261 | padding: 8px; 262 | line-height: 1.3em; 263 | padding: 7px 30px; 264 | background: #eee; 265 | border-radius: 2px; 266 | -moz-border-radius: 2px; 267 | -webkit-border-radius: 2px; 268 | } 269 | 270 | dl pre { 271 | margin-left: -60px; 272 | padding-left: 60px; 273 | } 274 | 275 | tt { 276 | background-color: #ecf0f3; 277 | color: #222; 278 | /* padding: 1px 2px; */ 279 | } 280 | 281 | tt.xref, a tt { 282 | background-color: #FBFBFB; 283 | } 284 | 285 | a:hover tt { 286 | background: #EEE; 287 | } 288 | -------------------------------------------------------------------------------- /docs/_themes/flask_small/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = basic 3 | stylesheet = flasky.css 4 | nosidebar = true 5 | pygments_style = flask_theme_support.FlaskyStyle 6 | 7 | [options] 8 | index_logo = '' 9 | index_logo_height = 120px 10 | github_fork = '' 11 | -------------------------------------------------------------------------------- /docs/_themes/flask_theme_support.py: -------------------------------------------------------------------------------- 1 | # flasky extensions. flasky pygments style based on tango style 2 | from pygments.style import Style 3 | from pygments.token import (Comment, Error, Generic, Keyword, Literal, Name, 4 | Number, Operator, Other, Punctuation, String, 5 | Whitespace) 6 | 7 | 8 | class FlaskyStyle(Style): 9 | background_color = "#f8f8f8" 10 | default_style = "" 11 | 12 | styles = { 13 | # No corresponding class for the following: 14 | # Text: "", # class: '' 15 | Whitespace: "underline #f8f8f8", # class: 'w' 16 | Error: "#a40000 border:#ef2929", # class: 'err' 17 | Other: "#000000", # class 'x' 18 | Comment: "italic #8f5902", # class: 'c' 19 | Comment.Preproc: "noitalic", # class: 'cp' 20 | Keyword: "bold #004461", # class: 'k' 21 | Keyword.Constant: "bold #004461", # class: 'kc' 22 | Keyword.Declaration: "bold #004461", # class: 'kd' 23 | Keyword.Namespace: "bold #004461", # class: 'kn' 24 | Keyword.Pseudo: "bold #004461", # class: 'kp' 25 | Keyword.Reserved: "bold #004461", # class: 'kr' 26 | Keyword.Type: "bold #004461", # class: 'kt' 27 | Operator: "#582800", # class: 'o' 28 | Operator.Word: "bold #004461", # class: 'ow' - like keywords 29 | Punctuation: "bold #000000", # class: 'p' 30 | # because special names such as Name.Class, Name.Function, etc. 31 | # are not recognized as such later in the parsing, we choose them 32 | # to look the same as ordinary variables. 33 | Name: "#000000", # class: 'n' 34 | Name.Attribute: "#c4a000", # class: 'na' - to be revised 35 | Name.Builtin: "#004461", # class: 'nb' 36 | Name.Builtin.Pseudo: "#3465a4", # class: 'bp' 37 | Name.Class: "#000000", # class: 'nc' - to be revised 38 | Name.Constant: "#000000", # class: 'no' - to be revised 39 | Name.Decorator: "#888", # class: 'nd' - to be revised 40 | Name.Entity: "#ce5c00", # class: 'ni' 41 | Name.Exception: "bold #cc0000", # class: 'ne' 42 | Name.Function: "#000000", # class: 'nf' 43 | Name.Property: "#000000", # class: 'py' 44 | Name.Label: "#f57900", # class: 'nl' 45 | Name.Namespace: "#000000", # class: 'nn' - to be revised 46 | Name.Other: "#000000", # class: 'nx' 47 | Name.Tag: "bold #004461", # class: 'nt' - like a keyword 48 | Name.Variable: "#000000", # class: 'nv' - to be revised 49 | Name.Variable.Class: "#000000", # class: 'vc' - to be revised 50 | Name.Variable.Global: "#000000", # class: 'vg' - to be revised 51 | Name.Variable.Instance: "#000000", # class: 'vi' - to be revised 52 | Number: "#990000", # class: 'm' 53 | Literal: "#000000", # class: 'l' 54 | Literal.Date: "#000000", # class: 'ld' 55 | String: "#4e9a06", # class: 's' 56 | String.Backtick: "#4e9a06", # class: 'sb' 57 | String.Char: "#4e9a06", # class: 'sc' 58 | String.Doc: "italic #8f5902", # class: 'sd' - like a comment 59 | String.Double: "#4e9a06", # class: 's2' 60 | String.Escape: "#4e9a06", # class: 'se' 61 | String.Heredoc: "#4e9a06", # class: 'sh' 62 | String.Interpol: "#4e9a06", # class: 'si' 63 | String.Other: "#4e9a06", # class: 'sx' 64 | String.Regex: "#4e9a06", # class: 'sr' 65 | String.Single: "#4e9a06", # class: 's1' 66 | String.Symbol: "#4e9a06", # class: 'ss' 67 | Generic: "#000000", # class: 'g' 68 | Generic.Deleted: "#a40000", # class: 'gd' 69 | Generic.Emph: "italic #000000", # class: 'ge' 70 | Generic.Error: "#ef2929", # class: 'gr' 71 | Generic.Heading: "bold #000080", # class: 'gh' 72 | Generic.Inserted: "#00A000", # class: 'gi' 73 | Generic.Output: "#888", # class: 'go' 74 | Generic.Prompt: "#745334", # class: 'gp' 75 | Generic.Strong: "bold #000000", # class: 'gs' 76 | Generic.Subheading: "bold #800080", # class: 'gu' 77 | Generic.Traceback: "bold #a40000", # class: 'gt' 78 | } 79 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. _authors: 2 | 3 | Authors 4 | ======= 5 | .. include:: ../AUTHORS 6 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog: 2 | 3 | .. include:: ../CHANGES.rst 4 | -------------------------------------------------------------------------------- /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 datetime 15 | import os 16 | import sys 17 | 18 | import alabaster 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | sys.path.append(os.path.abspath(".")) 24 | sys.path.append(os.path.abspath("..")) 25 | sys.path.append(os.path.abspath("_themes")) 26 | 27 | # -- General configuration ----------------------------------------------------- 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be extensions 33 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 34 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx", "alabaster"] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = ".rst" 41 | 42 | # The encoding of source files. 43 | # source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = "index" 47 | 48 | # General information about the project. 49 | project = "Eve" 50 | copyright = ( 51 | '%s. Python-Eve is a Nicola Iarocci Project' 52 | % datetime.datetime.now().year 53 | ) 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The full version, including alpha/beta/rc tags. 60 | release = __import__("eve").__version__ 61 | # The short X.Y version. 62 | version = release.split(".dev")[0] 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # language = None 67 | 68 | # There are two options for replacing |today|: either, you set today to some 69 | # non-false value, then it is used: 70 | # today = '' 71 | # Else, today_fmt is used as the format for a strftime call. 72 | # today_fmt = '%B %d, %Y' 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | exclude_patterns = ["_build"] 77 | 78 | # The reST default role (used for this markup: `text`) to use for all documents. 79 | # default_role = None 80 | 81 | # If true, '()' will be appended to :func: etc. cross-reference text. 82 | # add_function_parentheses = True 83 | 84 | # If true, the current module name will be prepended to all description 85 | # unit titles (such as .. function::). 86 | # add_module_names = True 87 | 88 | # If true, sectionauthor and moduleauthor directives will be shown in the 89 | # output. They are ignored by default. 90 | # show_authors = False 91 | 92 | # The name of the Pygments (syntax highlighting) style to use. 93 | # pygments_style = 'sphinx' 94 | 95 | # A list of ignored prefixes for module index sorting. 96 | # modindex_common_prefix = [] 97 | 98 | 99 | # -- Options for HTML output --------------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | # html_theme = 'default' 104 | # html_theme = 'flask' 105 | html_theme = "alabaster" 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | # html_theme_options = {'touch_icon': 'touch-icon.png'} 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | html_theme_path = [alabaster.get_path()] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | # html_title = None 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | # html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | # html_logo = "favicon.png" 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | html_favicon = "_static/favicon.ico" 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ["_static"] 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | # html_last_updated_fmt = '%b %d, %Y' 139 | 140 | # If true, SmartyPants will be used to convert quotes and dashes to 141 | # typographically correct entities. 142 | # html_use_smartypants = True 143 | 144 | # Custom sidebar templates, maps document names to template names. 145 | # html_sidebars = {} 146 | # html_sidebars = { 147 | # 'index': ['sidebarintro.html', 'searchbox.html', 'sidebarfooter.html'], 148 | # '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', 149 | # 'sourcelink.html', 'searchbox.html'] 150 | # } 151 | html_sidebars = { 152 | "**": [ 153 | "about.html", 154 | "sidebarintro.html", 155 | "navigation.html", 156 | "searchbox.html", 157 | "artwork.html", 158 | ] 159 | } 160 | 161 | html_theme_options = { 162 | "logo": "eve_leaf.png", 163 | "github_user": "pyeve", 164 | "github_repo": "eve", 165 | "github_type": "star", 166 | "github_banner": "forkme_right_green_007200.png", 167 | "show_powered_by": False, 168 | } 169 | # Additional templates that should be rendered to pages, maps page names to 170 | # template names. 171 | # html_additional_pages = {} 172 | 173 | # If false, no module index is generated. 174 | html_domain_indices = False 175 | # html_use_modindex = False 176 | 177 | # If false, no index is generated. 178 | # html_use_index = True 179 | 180 | # If true, the index is split into individual pages for each letter. 181 | # html_split_index = False 182 | 183 | # If true, links to the reST sources are added to the pages. 184 | html_show_sourcelink = False 185 | 186 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 187 | # html_show_sphinx = True 188 | 189 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 190 | # html_show_copyright = True 191 | 192 | # If true, an OpenSearch description file will be output, and all pages will 193 | # contain a tag referring to it. The value of this option must be the 194 | # base URL from which the finished HTML is served. 195 | # html_use_opensearch = '' 196 | 197 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 198 | # html_file_suffix = None 199 | 200 | # Output file base name for HTML help builder. 201 | htmlhelp_basename = "Evedoc" 202 | 203 | 204 | # -- Options for LaTeX output -------------------------------------------------- 205 | 206 | latex_elements = { 207 | # The paper size ('letterpaper' or 'a4paper'). 208 | # 'papersize': 'letterpaper', 209 | # The font size ('10pt', '11pt' or '12pt'). 210 | # 'pointsize': '10pt', 211 | # Additional stuff for the LaTeX preamble. 212 | # 'preamble': '', 213 | } 214 | 215 | # Grouping the document tree into LaTeX files. List of tuples 216 | # (source start file, target name, title, author, documentclass [howto/manual]). 217 | latex_documents = [ 218 | ("index", "Eve.tex", "Eve Documentation", "Nicola Iarocci", "manual") 219 | ] 220 | 221 | # The name of an image file (relative to this directory) to place at the top of 222 | # the title page. 223 | # latex_logo = None 224 | 225 | # For "manual" documents, if this is true, then toplevel headings are parts, 226 | # not chapters. 227 | # latex_use_parts = False 228 | 229 | # If true, show page references after internal links. 230 | # latex_show_pagerefs = False 231 | 232 | # If true, show URL addresses after external links. 233 | # latex_show_urls = False 234 | 235 | # Documents to append as an appendix to all manuals. 236 | # latex_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | # latex_domain_indices = True 240 | 241 | 242 | # -- Options for manual page output -------------------------------------------- 243 | 244 | # One entry per manual page. List of tuples 245 | # (source start file, name, description, authors, manual section). 246 | man_pages = [("index", "eve", "Eve Documentation", ["Nicola Iarocci"], 1)] 247 | 248 | # If true, show URL addresses after external links. 249 | # man_show_urls = False 250 | 251 | 252 | # -- Options for Texinfo output ------------------------------------------------ 253 | 254 | # Grouping the document tree into Texinfo files. List of tuples 255 | # (source start file, target name, title, author, 256 | # dir menu entry, description, category) 257 | texinfo_documents = [ 258 | ( 259 | "index", 260 | "Eve", 261 | "Eve Documentation", 262 | "Nicola Iarocci", 263 | "Eve", 264 | "One line description of project.", 265 | "Miscellaneous", 266 | ) 267 | ] 268 | 269 | # Documents to append as an appendix to all manuals. 270 | # texinfo_appendices = [] 271 | 272 | # If false, no module index is generated. 273 | # texinfo_domain_indices = True 274 | 275 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 276 | # texinfo_show_urls = 'footnote' 277 | 278 | 279 | # Example configuration for intersphinx: refer to the Python standard library. 280 | # intersphinx_mapping = {'http://docs.python.org/': None} 281 | intersphinx_mapping = {"cerberus": ("http://docs.python-cerberus.org/en/latest/", None)} 282 | 283 | pygments_style = "flask_theme_support.FlaskyStyle" 284 | 285 | # fall back if theme is not there 286 | try: 287 | __import__("flask_theme_support") 288 | except ImportError: 289 | print("-" * 74) 290 | print("Warning: Flask themes unavailable. Building with default theme") 291 | print("If you want the Flask themes, run this command and build again:") 292 | print 293 | print(" git submodule update --init") 294 | print("-" * 74) 295 | 296 | pygments_style = "tango" 297 | html_theme = "default" 298 | html_theme_options = {} 299 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | .. include:: ../CONTRIBUTING.rst 4 | -------------------------------------------------------------------------------- /docs/extensions.rst: -------------------------------------------------------------------------------- 1 | Extensions 2 | ========== 3 | 4 | Welcome to the Eve extensions registry. Here you can find a list of packages 5 | that extend Eve. This list is moderated and updated on a regular basis. If you 6 | wrote a package for Eve and want it to show up here, just `get in touch`_ and 7 | show me your tool! 8 | 9 | - Eve-Auth-JWT_ 10 | - Eve-Elastic_ 11 | - Eve-Healthcheck_ 12 | - Eve-Mocker_ 13 | - Eve-Mongoengine_ 14 | - Eve-Neo4j_ 15 | - Eve-OAuth2_ and Flask-Sentinel_ 16 | - Eve-SQLAlchemy_ 17 | - Eve-Swagger_ 18 | - Eve.NET_ 19 | - EveGenie_ 20 | - `REST Layer for Golang`_ 21 | 22 | Eve-Auth-JWT 23 | ------------ 24 | 25 | | *by Olivier Poitrey* 26 | 27 | Eve-Auth-JWT_ is An OAuth 2 JWT token validation module for Eve. 28 | 29 | Eve-Elastic 30 | ----------- 31 | 32 | | *by Petr Jašek* 33 | 34 | Eve-Elastic_ is an elasticsearch data layer for the Eve REST framework. 35 | Features facets support and the generation of mapping for schema. 36 | 37 | Eve-Healthcheck 38 | --------------- 39 | 40 | | *by LuisComS* 41 | 42 | Eve-Healthcheck_ is project that servers healthcheck urls used to monitor your 43 | Eve application. 44 | 45 | Eve-Mocker 46 | ---------- 47 | *by Thomas Sileo* 48 | 49 | `Eve-Mocker`_ is a mocking tool for Eve powered REST APIs, based on the 50 | excellent HTTPretty, aimed to be used in your unit tests, when you rely on an 51 | Eve API. Eve-Mocker has been featured on the Eve blog: `Mocking tool for Eve 52 | APIs`_ 53 | 54 | Eve-Mongoengine 55 | --------------- 56 | 57 | | *by Stanislav Heller* 58 | 59 | Eve-Mongoengine_ is an Eve extension, which enables Mongoengine ORM models to 60 | be used as eve schema. If you use mongoengine in your application and 61 | simultaneously want to use Eve, instead of writing schema again in Cerberus 62 | format (DRY!), you can use this extension, which takes your mongoengine models 63 | and auto-transforms them into Cerberus schema under the hood. 64 | 65 | Eve-Neo4j 66 | --------- 67 | *by Abraxas Biosystems* 68 | 69 | Eve-Neo4j_ is an Eve extension aiming to enable it's users to build and 70 | deploy highly customizable, fully featured RESTful Web Services using Neo4j 71 | as backend. Powered by Eve, Py2neo, flask-neo4j and good intentions. 72 | 73 | Eve-OAuth2 74 | ---------- 75 | *by Nicola Iarocci* 76 | 77 | Eve-OAuth2_ is not an extension per-se, but rather an example of how you can 78 | leverage Flask-Sentinel_ to protect your API endpoints with OAuth2. 79 | 80 | Eve-SQLAlchemy 81 | -------------- 82 | *by Andrew Mleczko et al.* 83 | 84 | Powered by Eve, SQLAlchemy and good intentions Eve-SQLALchemy_ allows to 85 | effortlessly build and deploy highly customizable, fully featured RESTful Web 86 | Services with SQL-based backends. 87 | 88 | Eve-Swagger 89 | ----------- 90 | 91 | | *by Nicola Iarocci* 92 | 93 | Eve-Swagger_ is a swagger.io extension for Eve. With a Swagger-enabled API, you 94 | get interactive documentation, client SDK generation and discoverability. From 95 | Swagger website: 96 | 97 | Swagger is a simple yet powerful representation of your RESTful API. With 98 | the largest ecosystem of API tooling on the planet, thousands of developers 99 | are supporting Swagger in almost every modern programming language and 100 | deployment environment. With a Swagger-enabled API, you get interactive 101 | documentation, client SDK generation and discoverability. 102 | 103 | For more information, see also the `Meet Eve-Swagger`_ article. 104 | 105 | Eve.NET 106 | ------- 107 | *by Nicola Iarocci* 108 | 109 | `Eve.NET`_ is a simple HTTP and REST client for Web Services powered by the Eve 110 | Framework. It leverages both ``System.Net.HttpClient`` and ``Json.NET`` to 111 | provide the best possible Eve experience on the .NET platform. Written and 112 | maintained by the same author of the Eve Framework itself, Eve.NET is delivered 113 | as a portable library (PCL) and runs seamlessly on .NET4, Mono, Xamarin.iOS, 114 | Xamarin.Android, Windows Phone 8 and Windows 8. We use Eve.NET internally to 115 | power our iOS, Web and Windows applications. 116 | 117 | EveGenie 118 | -------- 119 | *by Erin Corson and Matt Tucker, maintained by David Zisky.* 120 | 121 | EveGenie_ is a tool for generating Eve schemas. It accepts a json document of 122 | one or more resources and provides you with a starting schema definition. 123 | 124 | REST Layer for Golang 125 | --------------------- 126 | If you are into Golang, you should also check `REST Layer`_. Developed by 127 | Olivier Poitrey, a long time Eve contributor and sustainer. REST Layer is 128 | 129 | a REST API framework heavily inspired by the excellent Python 130 | Eve. It lets you automatically generate a comprehensive, customizable, and 131 | secure REST API on top of any backend storage with no boiler plate code. 132 | You can focus on your business logic now. 133 | 134 | 135 | .. _Eve-Healthcheck: https://github.com/ateliedocodigo/eve-healthcheck 136 | .. _`Mocking tool for Eve APIs`: http://blog.python-eve.org/eve-mocker 137 | .. _`Auto generate API docs`: http://blog.python-eve.org/eve-docs 138 | .. _charlesflynn/eve-docs: https://github.com/charlesflynn/eve-docs 139 | .. _eve-mocker: https://github.com/tsileo/eve-mocker 140 | .. _`get in touch`: mailto:eve@nicolaiarocci.com 141 | .. _Eve-Mongoengine: https://github.com/hellerstanislav/eve-mongoengine 142 | .. _Eve-Elastic: https://github.com/petrjasek/eve-elastic 143 | .. _Eve.NET: https://github.com/pyeve/Eve.NET 144 | .. _Eve-SQLAlchemy: https://github.com/RedTurtle/eve-sqlalchemy 145 | .. _Eve-OAuth2: https://github.com/pyeve/eve-oauth2 146 | .. _Flask-Sentinel: https://github.com/pyeve/flask-sentinel 147 | .. _Eve-Auth-JWT: https://github.com/rs/eve-auth-jwt 148 | .. _`REST Layer`: https://github.com/rs/rest-layer 149 | .. _EveGenie: https://github.com/DavidZisky/evegenie 150 | .. _Eve-Swagger: https://github.com/pyeve/eve-swagger 151 | .. _`Meet Eve-Swagger`: http://nicolaiarocci.com/announcing-eve-swagger/ 152 | .. _Eve-Neo4j: https://github.com/Abraxas-Biosystems/eve-neo4j 153 | -------------------------------------------------------------------------------- /docs/foreword.rst: -------------------------------------------------------------------------------- 1 | .. _foreword: 2 | 3 | Foreword 4 | ======== 5 | 6 | Read this before you get started with Eve. This hopefully answers some 7 | questions about the purpose and goals of the project, and when you should or 8 | should not be using it. 9 | 10 | Philosophy 11 | ---------- 12 | You have data stored somewhere and you want to expose it to your users 13 | through a RESTful Web API. Eve is the tool that allows you to do so. 14 | 15 | Eve provides a robust, feature rich, REST-centered API implementation, 16 | and you just need to configure your API settings and behavior, plug in your 17 | datasource, and you're good to go. See :doc:`features` for a list 18 | of features available to Eve-powered APIs. You might want to check the 19 | :doc:`rest_api_for_humans` slide deck too. 20 | 21 | API settings are stored in a standard Python module (defaults to 22 | ``settings.py``), which makes customization quite a trivial task. It is also 23 | possible to extend some key features, namely :ref:`auth`, :ref:`validation` and 24 | Data Access, by providing the Eve engine with custom objects. 25 | 26 | A little context 27 | ---------------- 28 | At `Gestionale Amica `_ we had been working hard on 29 | a full featured, Python powered, RESTful Web API. We learned quite a few things 30 | on REST best patterns, and we had a chance to put Python's renowned web 31 | capabilities to the test. Then, at EuroPython 2012, I had the opportunity to share 32 | what we learned. My talk sparked quite a bit of interest, and even after a few 33 | months had passed, the slides were still receiving a lot of hits every day. 34 | I kept receiving emails asking for source code examples and whatnot. After all, 35 | a REST API lies in the future of every web-oriented developer, and who isn't 36 | one these days? 37 | 38 | So, I thought, perhaps I could take the proprietary, closed code (codenamed 39 | 'Adam') and refactor it "just a little bit", so that it could fit a much wider 40 | number of use cases. I could then release it as an open source project. Well 41 | it turned out to be slightly more complex than that but finally here it is, and 42 | of course it's called Eve. 43 | 44 | REST, Flask and MongoDB 45 | ----------------------- 46 | The slides from my EuroPython talk, *Developing RESTful Web APIs with Flask and 47 | MongoDB*, are `available online`_. You might want to check them out to understand 48 | why and how certain design decisions were made, especially with regards to REST 49 | implementation. 50 | 51 | BSD License 52 | ----------- 53 | A large number of open source projects you find today are GPL Licensed. While 54 | the GPL has its time and place, it should most certainly not be your go-to 55 | license for your next open source project. 56 | 57 | A project that is released as GPL cannot be used in any commercial product 58 | without the product itself also being offered as open source. 59 | 60 | The MIT, BSD, ISC, and Apache2 licenses are great alternatives to the GPL that 61 | allow your open-source software to be used freely in proprietary, closed-source 62 | software. 63 | 64 | Eve is released under terms of the BSD License. See :ref:`license`. 65 | 66 | .. _available online: https://speakerdeck.com/u/nicola/p/developing-restful-web-apis-with-python-flask-and-mongodb 67 | -------------------------------------------------------------------------------- /docs/funding.rst: -------------------------------------------------------------------------------- 1 | Funding 2 | ======= 3 | We believe that collaboratively funded software can offer outstanding returns 4 | on investment, by encouraging users to collectively share the cost of 5 | development. 6 | 7 | The Eve REST framework continues to be open-source and permissively licensed, 8 | but we firmly believe it is in the commercial best-interest for users of the 9 | project to invest in its ongoing development. 10 | 11 | Signing up as a Backer or Sponsor will: 12 | 13 | - Directly contribute to faster releases, more features, and higher quality software. 14 | - Allow more time to be invested in documentation, issue triage, and community support. 15 | - Safeguard the future development of the Eve REST framework. 16 | 17 | If you run a business and is using Eve in a revenue-generating product, it 18 | would make business sense to sponsor Eve development: it ensures the project 19 | that your product relies on stays healthy and actively maintained. It can also 20 | help your exposure in the Eve community and makes it easier to attract Eve 21 | developers. 22 | 23 | Of course, individual users are also welcome to make a recurring pledge if Eve 24 | has helped you in your work or personal projects. Alternatively, consider 25 | donating as a sign of appreciation - like buying me coffee once in a while :) 26 | 27 | Support Eve development 28 | ----------------------- 29 | You can support Eve development by pledging on GitHub, Patreon, or PayPal. 30 | 31 | - `Become a Backer on GitHub `_ 32 | - `Become a Backer on Patreon `_ 33 | - `Donate via PayPal `_ (one time) 34 | 35 | Eve Course at TalkPython Training 36 | --------------------------------- 37 | There is a 5 hours-long Eve course available for you at the fine TalkPython 38 | Training website. The teacher is Nicola, Eve author and maintainer. Taking this 39 | course will directly support the project. 40 | 41 | - `Take the Eve Course at TalkPython Training `_ 42 | 43 | Custom Sponsorship and Consulting 44 | --------------------------------- 45 | If you are a business that is building core products using Eve, I am also 46 | open to conversations regarding custom sponsorship / consulting arrangements. 47 | Just `get in touch`_ with me. 48 | 49 | .. _`get in touch`: mailto:nicola@nicolaiarocci.com 50 | .. _`Eve course`: https://training.talkpython.fm/courses/explore_eve/eve-building-restful-mongodb-backed-apis-course 51 | 52 | Backers 53 | ~~~~~~~ 54 | Backers who actively support Eve and Cerberus development: 55 | 56 | - Gabriel Wainer 57 | - Jon Kelled 58 | 59 | Generous Backers 60 | ~~~~~~~~~~~~~~~~ 61 | Generous backers who actively support Eve and Cerberus development: 62 | 63 | .. image:: _static/backers/blokt.png 64 | :target: http://blokt.com/guides/best-vpn 65 | :alt: Blokt Crypto & Privacy 66 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :description: Python REST API Framework to effortlessly build and deploy full featured, highly customizable RESTful Web Services. 3 | 4 | .. title:: Python REST API Framework: Eve, the Simple Way to REST. 5 | 6 | Eve. The Simple Way to REST 7 | =========================== 8 | 9 | Version |release|. 10 | 11 | .. image:: https://img.shields.io/pypi/v/eve.svg?style=flat-square 12 | :target: https://pypi.org/project/eve 13 | 14 | .. image:: https://github.com/pyeve/eve/workflows/CI/badge.svg 15 | :target: https://github.com/pyeve/eve/actions?query=workflow%3ACI 16 | 17 | .. image:: https://img.shields.io/pypi/pyversions/eve.svg?style=flat-square 18 | :target: https://pypi.org/project/eve 19 | 20 | .. image:: https://img.shields.io/badge/license-BSD-blue.svg?style=flat-square 21 | :target: https://en.wikipedia.org/wiki/BSD_License 22 | 23 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 24 | :target: https://github.com/ambv/black 25 | 26 | ----- 27 | 28 | Eve is an :doc:`open source ` Python REST API framework designed for 29 | human beings. It allows to effortlessly build and deploy highly customizable, 30 | fully featured RESTful Web Services. 31 | 32 | Eve is powered by Flask_ and Cerberus_ and it offers native support for 33 | MongoDB_ data stores. Support for SQL, Elasticsearch and Neo4js backends is 34 | provided by community extensions_. 35 | 36 | The codebase is thoroughly tested under Python 3.9+, and PyPy. 37 | 38 | Eve is Simple 39 | ------------- 40 | .. code-block:: python 41 | 42 | from eve import Eve 43 | 44 | settings = {'DOMAIN': {'people': {}}} 45 | 46 | app = Eve(settings=settings) 47 | app.run() 48 | 49 | The API is now live, ready to be consumed: 50 | 51 | .. code-block:: console 52 | 53 | $ curl -i http://example.com/people 54 | HTTP/1.1 200 OK 55 | 56 | All you need to bring your API online is a database, a configuration file 57 | (defaults to ``settings.py``) or dictionary, and a launch script. Overall, you 58 | will find that configuring and fine-tuning your API is a very simple process. 59 | 60 | Funding Eve 61 | ----------- 62 | Eve REST framework is a :doc:`collaboratively funded project `. If you 63 | run a business and are using Eve in a revenue-generating product, it would make 64 | business sense to sponsor Eve development: it ensures the project that your 65 | product relies on stays healthy and actively maintained. Individual users are 66 | also welcome to make either a recurring pledge or a one time donation if Eve 67 | has helped you in your work or personal projects. Every single sign-up makes 68 | a significant impact towards making Eve possible. 69 | 70 | You can support Eve development by pledging on GitHub, Patreon, or PayPal. 71 | 72 | - `Become a Backer on GitHub `_ 73 | - `Become a Backer on Patreon `_ 74 | - `Donate via PayPal `_ (one time) 75 | 76 | .. toctree:: 77 | :hidden: 78 | 79 | foreword 80 | rest_api_for_humans 81 | install 82 | quickstart 83 | features 84 | config 85 | validation 86 | authentication 87 | funding 88 | tutorials/index 89 | snippets/index 90 | extensions 91 | contributing 92 | support 93 | updates 94 | authors 95 | license 96 | changelog 97 | 98 | .. note:: 99 | This documentation is under constant development. Please refer to the links 100 | on the sidebar for more information. 101 | 102 | 103 | .. _python-eve.org: http://python-eve.org 104 | .. _Postman: https://www.getpostman.com 105 | .. _Flask: http://flask.pocoo.org/ 106 | .. _eve-sqlalchemy: https://github.com/RedTurtle/eve-sqlalchemy 107 | .. _MongoDB: https://mongodb.org 108 | .. _Redis: http://redis.io 109 | .. _Cerberus: http://python-cerberus.org 110 | .. _events: https://github.com/pyeve/events 111 | .. _extensions: http://python-eve.org/extensions.html 112 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation 4 | ============ 5 | This part of the documentation covers the installation of Eve. The first step 6 | to using any software package is getting it properly installed. 7 | 8 | Installing Eve is simple with `pip `_: 9 | 10 | .. code-block:: console 11 | 12 | $ pip install eve 13 | 14 | Development Version 15 | -------------------- 16 | Eve is actively developed on GitHub, where the code is `always available 17 | `_. If you want to work with the 18 | development version of Eve, there are two ways: you can either let `pip` pull 19 | in the development version, or you can tell it to operate on a git checkout. 20 | Either way, virtualenv is recommended. 21 | 22 | Get the git checkout in a new virtualenv and run in development mode. 23 | 24 | .. code-block:: console 25 | 26 | $ git clone https://github.com/pyeve/eve.git 27 | Cloning into 'eve'... 28 | ... 29 | 30 | $ cd eve 31 | $ virtualenv venv 32 | ... 33 | Installing setuptools, pip, wheel... 34 | done. 35 | 36 | $ . venv/bin/activate 37 | $ pip install . 38 | ... 39 | Successfully installed ... 40 | 41 | This will pull in the dependencies and activate the git head as the current 42 | version inside the virtualenv. Then all you have to do is run ``git pull 43 | origin`` to update to the latest version. 44 | 45 | To just get the development version without git, do this instead: 46 | 47 | .. code-block:: console 48 | 49 | $ mkdir eve 50 | $ cd eve 51 | $ virtualenv venv 52 | $ . venv/bin/activate 53 | $ pip install git+https://github.com/pyeve/eve.git 54 | ... 55 | Successfully installed ... 56 | 57 | And you're done! 58 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | .. _license: 2 | 3 | Licensing 4 | ========= 5 | 6 | Also see :ref:`authors`. 7 | 8 | BSD License 9 | ----------- 10 | 11 | .. include:: ../LICENSE 12 | 13 | Artwork License 14 | --------------- 15 | Eve artwork 2013 by Roberto Pasini "Kalamun" released under the `Creative 16 | Commons BY-SA`_ license. 17 | 18 | .. _`Creative Commons BY-SA`: https://github.com/pyeve/eve/blob/master/artwork/LICENSE 19 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | Eager to get started? This page gives a first introduction to Eve. 7 | 8 | Prerequisites 9 | ------------- 10 | - You already have Eve installed. If you do not, head over to the 11 | :ref:`install` section. 12 | - MongoDB is installed_. 13 | - An instance of MongoDB is running_. 14 | 15 | A Minimal Application 16 | --------------------- 17 | 18 | A minimal Eve application looks something like this:: 19 | 20 | from eve import Eve 21 | app = Eve() 22 | 23 | if __name__ == '__main__': 24 | app.run() 25 | 26 | Just save it as run.py. Next, create a new text file with the following 27 | content: 28 | 29 | :: 30 | 31 | DOMAIN = {'people': {}} 32 | 33 | Save it as settings.py in the same directory where run.py is stored. This 34 | is the Eve configuration file, a standard Python module, and it is telling Eve 35 | that your API is comprised of just one accessible resource, ``people``. 36 | 37 | Now your are ready to launch your API. 38 | 39 | .. code-block:: console 40 | 41 | $ python run.py 42 | * Running on http://127.0.0.1:5000/ 43 | 44 | Now you can consume the API: 45 | 46 | .. code-block:: console 47 | 48 | $ curl -i http://127.0.0.1:5000 49 | HTTP/1.0 200 OK 50 | Content-Type: application/json 51 | Content-Length: 82 52 | Server: Eve/0.0.5-dev Werkzeug/0.8.3 Python/2.7.3 53 | Date: Wed, 27 Mar 2013 16:06:44 GMT 54 | 55 | Congratulations, your GET request got a nice response back. Let's look at the 56 | payload: 57 | 58 | :: 59 | 60 | { 61 | "_links": { 62 | "child": [ 63 | { 64 | "href": "people", 65 | "title": "people" 66 | } 67 | ] 68 | } 69 | } 70 | 71 | API entry points adhere to the :ref:`hateoas_feature` principle and provide 72 | information about the resources accessible through the API. In our case 73 | there's only one child resource available, that being ``people``. 74 | 75 | Try requesting ``people`` now: 76 | 77 | .. code-block:: console 78 | 79 | $ curl http://127.0.0.1:5000/people 80 | 81 | :: 82 | 83 | { 84 | "_items": [], 85 | "_links": { 86 | "self": { 87 | "href": "people", 88 | "title": "people" 89 | }, 90 | "parent": { 91 | "href": "/", 92 | "title": "home" 93 | } 94 | }, 95 | "_meta": { 96 | "max_results": 25, 97 | "page": 1, 98 | "total": 0 99 | } 100 | } 101 | 102 | This time we also got an ``_items`` list. The ``_links`` are relative to the 103 | resource being accessed, so you get a link to the parent resource (the home 104 | page) and to the resource itself. If you got a timeout error from pymongo, make 105 | sure the prerequisites are met. Chances are that the ``mongod`` server process 106 | is not running. 107 | 108 | By default Eve APIs are read-only: 109 | 110 | .. code-block:: console 111 | 112 | $ curl -X DELETE http://127.0.0.1:5000/people 113 | 114 | 405 Method Not Allowed 115 |

Method Not Allowed

116 |

The method DELETE is not allowed for the requested URL.

117 | 118 | Since we didn't provide any database detail in settings.py, Eve has no clue 119 | about the real content of the ``people`` collection (it might even be 120 | non-existent) and seamlessly serves an empty resource, as we don't want to let 121 | API users down. 122 | 123 | Database Interlude 124 | ------------------ 125 | Let's connect to a database by adding the following lines to settings.py: 126 | 127 | :: 128 | 129 | # Let's just use the local mongod instance. Edit as needed. 130 | 131 | # Please note that MONGO_HOST and MONGO_PORT could very well be left 132 | # out as they already default to a bare bones local 'mongod' instance. 133 | MONGO_HOST = 'localhost' 134 | MONGO_PORT = 27017 135 | 136 | # Skip this block if your db has no auth. But it really should. 137 | MONGO_USERNAME = '' 138 | MONGO_PASSWORD = '' 139 | # Name of the database on which the user can be authenticated, 140 | # needed if --auth mode is enabled. 141 | MONGO_AUTH_SOURCE = '' 142 | 143 | MONGO_DBNAME = 'apitest' 144 | 145 | Due to MongoDB *laziness*, we don't really need to create the database 146 | collections. Actually we don't even need to create the database: GET requests 147 | on an empty/non-existent DB will be served correctly (``200 OK`` with an empty 148 | collection); DELETE/PATCH/PUT will receive appropriate responses (``404 Not 149 | Found`` ), and POST requests will create database and collections as needed. 150 | However, such an auto-managed database will perform very poorly since it lacks 151 | indexes and any sort of optimization. 152 | 153 | A More Complex Application 154 | -------------------------- 155 | So far our API has been read-only. Let's enable the full spectrum of CRUD 156 | operations: 157 | 158 | :: 159 | 160 | # Enable reads (GET), inserts (POST) and DELETE for resources/collections 161 | # (if you omit this line, the API will default to ['GET'] and provide 162 | # read-only access to the endpoint). 163 | RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] 164 | 165 | # Enable reads (GET), edits (PATCH), replacements (PUT) and deletes of 166 | # individual items (defaults to read-only item access). 167 | ITEM_METHODS = ['GET', 'PATCH', 'PUT', 'DELETE'] 168 | 169 | ``RESOURCE_METHODS`` lists methods allowed at resource endpoints (``/people``) 170 | while ``ITEM_METHODS`` lists the methods enabled at item endpoints 171 | (``/people/``). Both settings have a global scope and will apply to 172 | all endpoints. You can then enable or disable HTTP methods at individual 173 | endpoint level, as we will soon see. 174 | 175 | Since we are enabling editing we also want to enable proper data validation. 176 | Let's define a schema for our ``people`` resource. 177 | 178 | :: 179 | 180 | schema = { 181 | # Schema definition, based on Cerberus grammar. Check the Cerberus project 182 | # (https://github.com/pyeve/cerberus) for details. 183 | 'firstname': { 184 | 'type': 'string', 185 | 'minlength': 1, 186 | 'maxlength': 10, 187 | }, 188 | 'lastname': { 189 | 'type': 'string', 190 | 'minlength': 1, 191 | 'maxlength': 15, 192 | 'required': True, 193 | # talk about hard constraints! For the purpose of the demo 194 | # 'lastname' is an API entry-point, so we need it to be unique. 195 | 'unique': True, 196 | }, 197 | # 'role' is a list, and can only contain values from 'allowed'. 198 | 'role': { 199 | 'type': 'list', 200 | 'allowed': ["author", "contributor", "copy"], 201 | }, 202 | # An embedded 'strongly-typed' dictionary. 203 | 'location': { 204 | 'type': 'dict', 205 | 'schema': { 206 | 'address': {'type': 'string'}, 207 | 'city': {'type': 'string'} 208 | }, 209 | }, 210 | 'born': { 211 | 'type': 'datetime', 212 | }, 213 | } 214 | 215 | For more information on validation see :ref:`validation`. 216 | 217 | Now let's say that we want to further customize the ``people`` endpoint. We want 218 | to: 219 | 220 | - set the item title to ``person`` 221 | - add an extra :ref:`custom item endpoint ` at ``/people/`` 222 | - override the default :ref:`cache control directives ` 223 | - disable DELETE for the ``/people`` endpoint (we enabled it globally) 224 | 225 | Here is how the complete ``people`` definition looks in our updated settings.py 226 | file: 227 | 228 | :: 229 | 230 | people = { 231 | # 'title' tag used in item links. Defaults to the resource title minus 232 | # the final, plural 's' (works fine in most cases but not for 'people') 233 | 'item_title': 'person', 234 | 235 | # by default the standard item entry point is defined as 236 | # '/people/'. We leave it untouched, and we also enable an 237 | # additional read-only entry point. This way consumers can also perform 238 | # GET requests at '/people/'. 239 | 'additional_lookup': { 240 | 'url': 'regex("[\w]+")', 241 | 'field': 'lastname' 242 | }, 243 | 244 | # We choose to override global cache-control directives for this resource. 245 | 'cache_control': 'max-age=10,must-revalidate', 246 | 'cache_expires': 10, 247 | 248 | # most global settings can be overridden at resource level 249 | 'resource_methods': ['GET', 'POST'], 250 | 251 | 'schema': schema 252 | } 253 | 254 | Finally we update our domain definition: 255 | 256 | :: 257 | 258 | DOMAIN = { 259 | 'people': people, 260 | } 261 | 262 | Save settings.py and launch run.py. We can now insert documents at the 263 | ``people`` endpoint: 264 | 265 | .. code-block:: console 266 | 267 | $ curl -d '[{"firstname": "barack", "lastname": "obama"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://127.0.0.1:5000/people 268 | HTTP/1.0 201 OK 269 | 270 | We can also update and delete items (but not the whole resource since we 271 | disabled that). We can also perform GET requests against the new ``lastname`` 272 | endpoint: 273 | 274 | .. code-block:: console 275 | 276 | $ curl -i http://127.0.0.1:5000/people/obama 277 | HTTP/1.0 200 OK 278 | Etag: 28995829ee85d69c4c18d597a0f68ae606a266cc 279 | Last-Modified: Wed, 21 Nov 2012 16:04:56 GMT 280 | Cache-Control: 'max-age=10,must-revalidate' 281 | Expires: 10 282 | ... 283 | 284 | .. code-block:: javascript 285 | 286 | { 287 | "firstname": "barack", 288 | "lastname": "obama", 289 | "_id": "50acfba938345b0978fccad7" 290 | "updated": "Wed, 21 Nov 2012 16:04:56 GMT", 291 | "created": "Wed, 21 Nov 2012 16:04:56 GMT", 292 | "_links": { 293 | "self": {"href": "people/50acfba938345b0978fccad7", "title": "person"}, 294 | "parent": {"href": "/", "title": "home"}, 295 | "collection": {"href": "people", "title": "people"} 296 | } 297 | } 298 | 299 | Cache directives and item title match our new settings. See :doc:`features` for 300 | a complete list of features available and more usage examples. 301 | 302 | .. _`installed`: http://docs.mongodb.org/manual/installation/ 303 | .. _running: http://docs.mongodb.org/manual/tutorial/manage-mongodb-processes/ 304 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # requirements to build documentation 2 | Sphinx<2.0 3 | sphinxcontrib-issuetracker 4 | alabaster==0.7.13 5 | doc8 6 | eve 7 | jinja2<3.1.0 8 | -------------------------------------------------------------------------------- /docs/rest_api_for_humans.rst: -------------------------------------------------------------------------------- 1 | REST API for Humans 2 | =================== 3 | I have been introducing Eve at several conferences and meetups. A few people 4 | suggested that I post the slides on the Eve website, so here it is: a quick 5 | rundown on Eve features, along with a few code snippets and examples. Hopefully 6 | it will do a good job in letting you decide whether Eve is valid solution for 7 | your use case. 8 | 9 | - `REST API for Humans @ SpeakerDeck `_ 10 | 11 | 12 | Conferences 13 | ------------ 14 | Eve REST API for Humans™ has been presented at the following events so far: 15 | 16 | - PyConWeb 2018, Munich 17 | - PyCon Belarus 2018, Kiev 18 | - Codemotion 2017, Rome 19 | - PiterPy 2016, St. Petersburg 20 | - Percona Live 2015, Amsterdam 21 | - EuroPython 2014, Berlin 22 | - Python Meetup, Helsinki 23 | - PyCon Italy 2014, Florence 24 | - PyCon Sweden 2014, Stockholm 25 | - FOSDEM 2014, Brussels 26 | 27 | Want this talk delivered at your conference? Get in touch_! 28 | 29 | 30 | .. _touch: mailto:nicola@nicolaiarocci.com 31 | -------------------------------------------------------------------------------- /docs/snippets/hooks_blueprints.rst: -------------------------------------------------------------------------------- 1 | Using Eve Event Hooks from your Blueprint 2 | ========================================= 3 | by Pau Freixes 4 | 5 | The use of Flask Blueprints_ helps us to extend our Eve applications with new 6 | endpoints that do not fit as a typical Eve resource. Pulling these endpoints 7 | out of the Eve scope allows us to write specific code in order to handle 8 | specific situations. 9 | 10 | In the context of a Blueprint we could expect Eve features not be available, 11 | but often that is not the case. We can continue to use a bunch of features, 12 | such as :ref:`eventhooks`. 13 | 14 | Next snippet displays how the ``users`` module has a blueprint which performs 15 | some custom actions and then uses the ``users_deleted`` signal to notify and 16 | invoke all callback functions which are registered to the Eve application. 17 | 18 | .. code-block:: python 19 | 20 | from flask import Blueprint, current_app as app 21 | 22 | blueprint = Blueprint('prefix_uri', __name__) 23 | 24 | @blueprint.route('/users/', methods=['DELETE']) 25 | def del_user(username): 26 | # some specific code goes here 27 | # ... 28 | 29 | # call Eve-hooks consumers for this event 30 | getattr(app, "users_deleted")(username) 31 | 32 | Next snippet displays how the blueprint is binded over our main Eve application 33 | and how the specific ``set_username_as_none`` function is registered to be 34 | called each time an user is deleted using the Eve events, to update the 35 | properly MongoDB collection. 36 | 37 | .. code-block:: python 38 | 39 | from eve import Eve 40 | from users import blueprint 41 | from flask import current_app, request 42 | 43 | def set_username_as_none(username): 44 | resource = request.endpoint.split('|')[0] 45 | return current_app.data.driver.db[resource].update( 46 | {"user" : username}, 47 | {"$set": {"user": None}}, 48 | multi=True 49 | ) 50 | 51 | app = Eve() 52 | # register the blueprint to the main Eve application 53 | app.register_blueprint(blueprint) 54 | # bind the callback function so it is invoked at each user deletion 55 | app.users_deleted += set_username_as_none 56 | app.run() 57 | 58 | .. _Blueprints: http://flask.pocoo.org/docs/blueprints/ 59 | .. _`eve event-hooks`: http://python-eve.org/features.html#event-hooks 60 | -------------------------------------------------------------------------------- /docs/snippets/index.rst: -------------------------------------------------------------------------------- 1 | .. _snippets: 2 | 3 | Snippets 4 | ======== 5 | 6 | Welcome to the Eve snippet archive. This is the place where anyone can drop 7 | helpful pieces of code for others to use. 8 | 9 | Available Snippets 10 | ------------------ 11 | 12 | .. toctree:: 13 | :maxdepth: 1 14 | 15 | hooks_blueprints 16 | list_of_items 17 | 18 | 19 | Add your snippet 20 | ---------------- 21 | Want to add your snippet? Just add your own .rst file to the `snippets folder`_ 22 | (see the template below for reference), update the TOC in this page (see 23 | source_), and then submit a `pull request`_. 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | template 29 | 30 | .. _template: https://raw.githubusercontent.com/pyeve/eve/master/docs/snippets/template.rst 31 | .. _`pull request`: https://github.com/pyeve/eve/pulls 32 | .. _`snippets folder`: https://github.com/pyeve/eve/tree/master/docs/snippets 33 | .. _source: https://raw.githubusercontent.com/pyeve/eve/master/docs/snippets/index.rst 34 | -------------------------------------------------------------------------------- /docs/snippets/list_of_items.rst: -------------------------------------------------------------------------------- 1 | Supporting both list-level and item-level CRUD operations 2 | ========================================================= 3 | by John Chang 4 | 5 | This is an example of how to implement a simple list of items that supports both list-level and item-level CRUD operations. 6 | 7 | Specifically, it should be possible to use a single GET to get the entire list (including all items) but also a single POST to append an item (rather than PATCHing the list). 8 | 9 | The solution was to database event hooks to inject the embedded child documents (``items``) into the parent list before it's returned to the client and also delete the child items when the parent list is deleted. This works, although it results in two DB queries. 10 | 11 | main.py 12 | ------- 13 | .. code-block:: python 14 | 15 | from eve import Eve 16 | from bson.objectid import ObjectId 17 | 18 | app = Eve() 19 | mongo = app.data.driver 20 | 21 | 22 | def after_fetching_lists(response): 23 | list_id = response['_id'] 24 | f = {'list_id': ObjectId(list_id)} 25 | response['items'] = list(mongo.db.items.find(f)) 26 | 27 | 28 | def after_deleting_lists(item): 29 | list_id = item['_id'] 30 | f = {'list_id': ObjectId(list_id)} 31 | mongo.db.items.delete_many(f) 32 | 33 | app.on_fetched_item_lists += after_fetching_lists 34 | app.on_deleted_item_lists += after_deleting_lists 35 | 36 | app.run() 37 | 38 | settings.py 39 | ----------- 40 | .. code-block:: python 41 | 42 | import os 43 | 44 | DEBUG = True 45 | 46 | MONGO_HOST = os.environ.get('MONGO_HOST', 'localhost') 47 | MONGO_PORT = os.environ.get('MONGO_PORT', 27017) 48 | MONGO_USERNAME = os.environ.get('MONGO_USERNAME', 'user') 49 | MONGO_PASSWORD = os.environ.get('MONGO_PASSWORD', 'user') 50 | MONGO_DBNAME = os.environ.get('MONGO_DBNAME', 'listtest') 51 | 52 | RESOURCE_METHODS = ['GET', 'POST', 'DELETE'] 53 | ITEM_METHODS = ['GET', 'PUT', 'PATCH', 'DELETE'] 54 | 55 | DOMAIN = { 56 | 'lists': { 57 | 'schema': { 58 | 'title': { 59 | 'type': 'string' 60 | } 61 | } 62 | }, 63 | 'items': { 64 | 'url': 'lists//items', 65 | 'schema': { 66 | 'list_id': { 67 | 'type': 'objectid', 68 | 'data_relation': { 69 | 'resource': 'lists', 70 | 'field': '_id' 71 | } 72 | }, 73 | 'name': { 74 | 'type': 'string', 75 | 'required': True 76 | } 77 | } 78 | } 79 | } 80 | 81 | Usage 82 | ----- 83 | .. code-block:: bash 84 | 85 | $ curl -i -X POST http://127.0.0.1:5000/lists -d title="My List" 86 | HTTP/1.0 201 CREATED 87 | 88 | { 89 | "_id": "58960f83a663e2e6746dfa6a", 90 | : 91 | } 92 | 93 | $ curl -i -X POST http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a/items -d 'name=Alice' 94 | HTTP/1.0 201 CREATED 95 | 96 | $ curl -i -X POST http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a/items -d 'name=Bob' 97 | HTTP/1.0 201 CREATED 98 | 99 | $ curl -i -X GET http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a 100 | HTTP/1.0 200 OK 101 | 102 | { 103 | "_created": "Sat, 04 Feb 2017 17:29:39 GMT", 104 | "_etag": "01799f6be25a044ab95cfeb2dc0f834d11b796d8", 105 | "_id": "58960f83a663e2e6746dfa6a", 106 | "_updated": "Sat, 04 Feb 2017 17:29:39 GMT", 107 | "items": [ 108 | { 109 | "_created": "Sat, 04 Feb 2017 17:30:06 GMT", 110 | "_etag": "72ad9248ad5bf45c7bfe3e03a1b9bc384d94572f", 111 | "_id": "58960f9ea663e2e6746dfa6b", 112 | "_updated": "Sat, 04 Feb 2017 17:30:06 GMT", 113 | "list_id": "58960f83a663e2e6746dfa6a", 114 | "name": "Alice", 115 | "quantity": 1 116 | }, 117 | { 118 | "_created": "Sat, 04 Feb 2017 17:30:13 GMT", 119 | "_etag": "447f51b057fb5e0a70472e96ff883c64b5e2e308", 120 | "_id": "58960fa5a663e2e6746dfa6c", 121 | "_updated": "Sat, 04 Feb 2017 17:30:13 GMT", 122 | "list_id": "58960f83a663e2e6746dfa6a", 123 | "name": "Bob", 124 | "quantity": 1 125 | } 126 | ], 127 | "title": "My List" 128 | } 129 | 130 | $ curl -i -X DELETE http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a/items/58960f9ea663e2e6746dfa6b -H "If-Match: 72ad9248ad5bf45c7bfe3e03a1b9bc384d94572f" 131 | HTTP/1.0 204 NO CONTENT 132 | 133 | $ curl -i -X GET http://127.0.0.1:5000/lists/58960f83a663e2e6746dfa6a 134 | HTTP/1.0 200 OK 135 | 136 | { 137 | "_created": "Sat, 04 Feb 2017 17:29:39 GMT", 138 | "_etag": "01799f6be25a044ab95cfeb2dc0f834d11b796d8", 139 | "_id": "58960f83a663e2e6746dfa6a", 140 | "_updated": "Sat, 04 Feb 2017 17:29:39 GMT", 141 | "items": [ 142 | { 143 | "_created": "Sat, 04 Feb 2017 17:30:13 GMT", 144 | "_etag": "447f51b057fb5e0a70472e96ff883c64b5e2e308", 145 | "_id": "58960fa5a663e2e6746dfa6c", 146 | "_updated": "Sat, 04 Feb 2017 17:30:13 GMT", 147 | "list_id": "58960f83a663e2e6746dfa6a", 148 | "name": "Bob", 149 | "quantity": 1 150 | } 151 | ], 152 | "title": "My List" 153 | } 154 | -------------------------------------------------------------------------------- /docs/snippets/template.rst: -------------------------------------------------------------------------------- 1 | Snippet Template 2 | ================ 3 | by Firstname Lastname 4 | 5 | This is a snippet template. Put your snippet explanation here. If this is going 6 | to be long, make sure to split it into paragraphs for enhanced reading 7 | experience. Make your code snippet follow, like so: 8 | 9 | .. code-block:: python 10 | 11 | from eve import Eve 12 | 13 | # just an example of a code snippet 14 | app = Eve() 15 | app.run() 16 | 17 | Add closing comments as needed. 18 | -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | .. _support: 2 | 3 | Support 4 | ======= 5 | Please keep in mind that the issues on GitHub are reserved for bugs and 6 | feature requests. If you have general or usage questions about Eve, there 7 | are several options: 8 | 9 | Stack Overflow 10 | -------------- 11 | `Stack Overflow`_ has a eve tag. It is generally followed by Eve developers 12 | and users. 13 | 14 | Mailing List 15 | ------------ 16 | The `mailing list`_ is intended to be a low traffic resource for both 17 | developers/contributors and API maintainers looking for help or requesting 18 | feedback. 19 | 20 | IRC 21 | --- 22 | There is an official Freenode channel for Eve at `#python-eve 23 | `_. 24 | 25 | File an Issue 26 | ------------- 27 | If you notice some unexpected behavior in Eve, or want to see support for a new 28 | feature, `file an issue on GitHub 29 | `_. 30 | 31 | .. _`mailing list`: https://groups.google.com/forum/#!forum/python-eve 32 | .. _`Stack Overflow`: https://stackoverflow.com/questions/tagged/eve 33 | -------------------------------------------------------------------------------- /docs/tutorials/custom_idfields.rst: -------------------------------------------------------------------------------- 1 | .. _custom_ids: 2 | 3 | Handling custom ID fields 4 | ========================= 5 | 6 | When it comes to individual document endpoints, in most cases you don't have 7 | anything to do besides defining the parent resource endpoint. So let's say that 8 | you configure a ``/invoices`` endpoint, which will allow clients to query the 9 | underlying `invoices` database collection. The ``/invoices/`` 10 | endpoint will be made available by the framework, and will be used by clients to 11 | retrieve and/or edit individual documents. By default, Eve provides this feature 12 | seamlessly when ``ID_FIELD`` fields are of ``ObjectId`` type. 13 | 14 | However, you might have collections where your unique identifier is not an 15 | ``ObjectId``, and you still want individual document endpoints to work 16 | properly. Don't worry, it's doable, it only requires a little tinkering. 17 | 18 | Handling ``UUID`` fields 19 | ------------------------ 20 | In this tutorial we will consider a scenario in which one of our database 21 | collections (invoices) uses UUID fields as unique identifiers. We want our API to 22 | expose a document endpoint like ``/invoices/uuid``, which translates to something like: 23 | 24 | ``/invoices/48c00ee9-4dbe-413f-9fc3-d5f12a91de1c``. 25 | 26 | These are the steps we need to follow: 27 | 28 | 1. Craft a custom JSONEncoder that is capable of serializing UUIDs as strings 29 | and pass it to our Eve application. 30 | 2. Add support for a new ``uuid`` data type so we can properly validate 31 | incoming uuid values. 32 | 3. Configure our invoices endpoint so Eve knows how to properly parse UUID 33 | urls. 34 | 35 | Custom JSONEncoder 36 | ~~~~~~~~~~~~~~~~~~ 37 | The Eve default JSON serializer is perfectly capable of serializing common data 38 | types like ``datetime`` (serialized to a RFC1123 string, like ``Sat, 23 Feb 1985 39 | 12:00:00 GMT``) and ``ObjectId`` values (also serialized to strings). 40 | 41 | Since we are adding support for an unknown data type, we also need to instruct 42 | our Eve instance on how to properly serialize it. This is as easy as 43 | subclassing a standard ``JSONEncoder`` or, even better, Eve's own 44 | ``BaseJSONEncoder``, so our custom serializer will preserve all of Eve's 45 | serialization magic: 46 | 47 | .. code-block:: python 48 | 49 | from eve.io.base import BaseJSONEncoder 50 | from uuid import UUID 51 | 52 | class UUIDEncoder(BaseJSONEncoder): 53 | """ JSONEconder subclass used by the json render function. 54 | This is different from BaseJSONEoncoder since it also addresses 55 | encoding of UUID 56 | """ 57 | 58 | def default(self, obj): 59 | if isinstance(obj, UUID): 60 | return str(obj) 61 | else: 62 | # delegate rendering to base class method (the base class 63 | # will properly render ObjectIds, datetimes, etc.) 64 | return super(UUIDEncoder, self).default(obj) 65 | 66 | 67 | ``UUID`` Validation 68 | ~~~~~~~~~~~~~~~~~~~ 69 | By default Eve creates a unique identifier for each newly inserted document, 70 | and that is of ``ObjectId`` type. This is not what we want to happen at this 71 | endpoint. Here we want the client itself to provide the unique identifiers, and 72 | we also want to validate that they are of UUID type. In order to achieve that, 73 | we first need to extend our data validation layer (see :ref:`validation` for 74 | details on custom validation): 75 | 76 | .. code-block:: python 77 | 78 | from eve.io.mongo import Validator 79 | from uuid import UUID 80 | 81 | class UUIDValidator(Validator): 82 | """ 83 | Extends the base mongo validator adding support for the uuid data-type 84 | """ 85 | def _validate_type_uuid(self, value): 86 | try: 87 | UUID(value) 88 | return True 89 | except ValueError: 90 | pass 91 | 92 | ``UUID`` URLs 93 | ~~~~~~~~~~~~~ 94 | Now Eve is capable of rendering and validating UUID values but it still doesn't know 95 | which resources are going to use these features. We also need to set 96 | ``item_url`` so uuid formed urls can be properly parsed. Let's pick our 97 | ``settings.py`` module and update the API domain accordingly: 98 | 99 | .. code-block:: python 100 | 101 | invoices = { 102 | # this resource item endpoint (/invoices/) will match a UUID regex. 103 | 'item_url': 'regex("[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}")', 104 | 'schema': { 105 | # set our _id field of our custom uuid type. 106 | '_id': {'type': 'uuid'}, 107 | }, 108 | } 109 | 110 | DOMAIN = { 111 | 'invoices': invoices 112 | } 113 | 114 | If all your API resources are going to support uuid as unique document 115 | identifiers then you might just want to set the global ``ITEM_URL`` to the uuid 116 | regex in order to avoid setting it for every single resource endpoint. 117 | 118 | Passing the ``UUID`` juice to Eve 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | Now all the missing pieces are there we only need to instruct Eve on how to 121 | use them. Eve needs to know about the new data type when its building the 122 | URL map, so we need to pass our custom classes right at the beginning, when we 123 | are instancing the application: 124 | 125 | .. code-block:: python 126 | 127 | app = Eve(json_encoder=UUIDEncoder, validator=UUIDValidator) 128 | 129 | 130 | Remember, if you are using custom ``ID_FIELD`` values then you should not rely 131 | on MongoDB (and Eve) to auto-generate the ``ID_FIELD`` for you. You are 132 | supposed to pass the value, like so: 133 | 134 | :: 135 | 136 | POST 137 | {"name":"bill", "_id":"48c00ee9-4dbe-413f-9fc3-d5f12a91de1c"} 138 | 139 | .. note:: 140 | By default, Eve sets PyMongo's ``UuidRepresentation`` to ``standard``. 141 | This allows for seamlessly handling of modern Python-generated UUID values. You 142 | can change the default by setting the ``uuidRepresentation`` value of ``MONGO_OPTIONS`` 143 | as desired. For more informations, see `PyMongo documentation`_. 144 | 145 | .. _`custom url converters`: http://werkzeug.pocoo.org/docs/routing/#custom-converters 146 | .. _Flask: http://flask.pocoo.org/ 147 | .. _Werkzeug: http://werkzeug.pocoo.org/ 148 | .. _PyMongo documentation: https://pymongo.readthedocs.io/en/stable/examples/uuid.html#configuring-uuid-representation 149 | -------------------------------------------------------------------------------- /docs/tutorials/index.rst: -------------------------------------------------------------------------------- 1 | .. _tutorials: 2 | 3 | Tutorials 4 | ========= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | account_management 10 | custom_idfields 11 | 12 | Learn Eve at TalkPython Training 13 | -------------------------------- 14 | There is a 5 hours-long Eve course available for you at the fine TalkPython 15 | Training website. The teacher is Nicola, Eve author and maintainer. Taking this 16 | course will directly support the project. 17 | 18 | - `Take the Eve Course at TalkPython Training `_ 19 | -------------------------------------------------------------------------------- /docs/updates.rst: -------------------------------------------------------------------------------- 1 | .. _updates: 2 | 3 | Updates 4 | ======= 5 | If you'd like to stay up to date on the community and development of Eve, 6 | there are several options: 7 | 8 | Blog 9 | ---- 10 | `Eve News `_ is the official blog of the Eve project. 11 | 12 | Twitter 13 | ------- 14 | I often tweet about new features and releases of Eve. Follow `@nicolaiarocci 15 | `_. 16 | 17 | Mailing List 18 | ------------ 19 | The `mailing list`_ is intended to be a low traffic resource for both 20 | developers/contributors and API maintainers looking for help or requesting 21 | feedback. 22 | 23 | GitHub 24 | ------ 25 | Of course the best way to track the development of Eve is through 26 | `the GitHub repo `_. 27 | 28 | .. _`mailing list`: https://groups.google.com/forum/#!forum/python-eve 29 | -------------------------------------------------------------------------------- /docs/validation.rst: -------------------------------------------------------------------------------- 1 | .. _validation: 2 | 3 | Data Validation 4 | =============== 5 | Data validation is provided out-of-the-box. Your configuration includes 6 | a schema definition for every resource managed by the API. Data sent to the API 7 | to be inserted/updated will be validated against the schema, and a resource 8 | will only be updated if validation passes. 9 | 10 | .. code-block:: console 11 | 12 | $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "mitt", "lastname": "romney"}]' -H 'Content-Type: application/json' http://myapi/people 13 | HTTP/1.1 201 OK 14 | 15 | The response will contain a success/error state for each item provided in the 16 | request: 17 | 18 | .. code-block:: javascript 19 | 20 | { 21 | "_status": "ERR", 22 | "_error": "Some documents contains errors", 23 | "_items": [ 24 | { 25 | "_status": "ERR", 26 | "_issues": {"lastname": "value 'clinton' not unique"} 27 | }, 28 | { 29 | "_status": "OK", 30 | } 31 | ] 32 | ] 33 | 34 | In the example above, the first document did not validate so the whole request 35 | has been rejected. 36 | 37 | When all documents pass validation and are inserted correctly the response 38 | status is ``201 Created``. If any document fails validation the response status 39 | is ``422 Unprocessable Entity``, or any other error code defined by 40 | ``VALIDATION_ERROR_STATUS`` configuration. 41 | 42 | For information on how to define documents schema and standard validation 43 | rules, see :ref:`schema`. 44 | 45 | Extending Data Validation 46 | ------------------------- 47 | Data validation is based on the Cerberus_ validation system and it is therefore 48 | extensible. As a matter of fact, Eve's MongoDB data-layer itself extends 49 | Cerberus validation, implementing the ``unique`` and ``data_relation`` 50 | constraints, the ``ObjectId`` data type and the ``decimal128`` on top of 51 | the standard rules. 52 | 53 | .. _custom_validation_rules: 54 | 55 | Custom Validation Rules 56 | ------------------------ 57 | Suppose that in your specific and very peculiar use case, a certain value can 58 | only be expressed as an odd integer. You decide to add support for a new 59 | ``isodd`` rule to our validation schema. This is how you would implement 60 | that: 61 | 62 | .. code-block:: python 63 | 64 | from eve.io.mongo import Validator 65 | 66 | class MyValidator(Validator): 67 | def _validate_isodd(self, isodd, field, value): 68 | if isodd and not bool(value & 1): 69 | self._error(field, "Value must be an odd number") 70 | 71 | app = Eve(validator=MyValidator) 72 | 73 | if __name__ == '__main__': 74 | app.run() 75 | 76 | By subclassing the base Mongo validator class and then adding a custom 77 | ``_validate_`` method, you extended the available :ref:`schema` 78 | grammar and now the new custom rule ``isodd`` is available in your schema. You 79 | can now do something like: 80 | 81 | .. code-block:: python 82 | 83 | 'schema': { 84 | 'oddity': { 85 | 'isodd': True, 86 | 'type': 'integer' 87 | } 88 | } 89 | 90 | Cerberus and Eve also offer `function-based validation`_ and `type coercion`_, 91 | lightweight alternatives to class-based custom validation. 92 | 93 | Custom Data Types 94 | ----------------- 95 | You can also add new data types by simply adding ``_validate_type_`` 96 | methods to your subclass. Consider the following snippet from the Eve source 97 | code. 98 | 99 | .. code-block:: python 100 | 101 | def _validate_type_objectid(self, value): 102 | """ Enables validation for `objectid` schema attribute. 103 | 104 | :param value: field value. 105 | """ 106 | if isinstance(value, ObjectId): 107 | return True 108 | 109 | This method enables support for MongoDB ``ObjectId`` type in your schema, 110 | allowing something like this: 111 | 112 | .. code-block:: python 113 | 114 | 'schema': { 115 | 'owner': { 116 | 'type': 'objectid', 117 | 'required': True, 118 | }, 119 | } 120 | 121 | You can also check the `source code`_ for Eve custom validation, where you will 122 | find more advanced use cases, such as the implementation of the ``unique`` and 123 | ``data_relation`` constraints. 124 | 125 | For more information on 126 | 127 | .. note:: 128 | 129 | We have only scratched the surface of data validation. Please make sure 130 | to check the Cerberus_ documentation for a complete list of available 131 | validation rules and data types. 132 | 133 | Also note that Cerberus requirement is pinned to version 0.9.2, which still 134 | supports the ``validate_update`` method used for ``PATCH`` requests. 135 | Upgrade to Cerberus 1.0+ is scheduled for Eve version 0.8. 136 | 137 | .. _unknown: 138 | 139 | Allowing the Unknown 140 | -------------------- 141 | Normally you don't want clients to inject unknown fields in your documents. 142 | However, there might be circumstances where this is desirable. During the 143 | development cycle, for example, or when you are dealing with very heterogeneous 144 | data. After all, not forcing normalized information is one of the selling 145 | points of MongoDB and many other NoSQL data stores. 146 | 147 | In Eve, you achieve this by setting the ``ALLOW_UNKNOWN`` option to ``True``. 148 | Once this option is enabled, fields matching the schema will be validated 149 | normally, while unknown fields will be quietly stored without a glitch. You 150 | can also enable this feature only for certain endpoints by setting the 151 | ``allow_unknown`` local option. 152 | 153 | Consider the following domain: 154 | 155 | .. code-block:: python 156 | 157 | DOMAIN: { 158 | 'people': { 159 | 'allow_unknown': True, 160 | 'schema': { 161 | 'firstname': {'type': 'string'}, 162 | } 163 | } 164 | } 165 | 166 | Normally you can only add (POST) or edit (PATCH) `firstnames` to the 167 | ``/people`` endpoint. However, since ``allow_unknown`` has been enabled, even 168 | a payload like this will be accepted: 169 | 170 | .. code-block:: console 171 | 172 | $ curl -d '[{"firstname": "bill", "lastname": "clinton"}, {"firstname": "bill", "age":70}]' -H 'Content-Type: application/json' http://myapi/people 173 | HTTP/1.1 201 OK 174 | 175 | .. admonition:: Please note 176 | 177 | Use this feature with extreme caution. Also be aware that, when this 178 | option is enabled, clients will be capable of actually `adding` fields via 179 | PATCH (edit). 180 | 181 | ``ALLOW_UNKNOWN`` is also useful for read-only APIs or endpoints that 182 | need to return the whole document, as found in the underlying database. In this 183 | scenario you don't want to bother with validation schemas. For the whole API 184 | just set ``ALLOW_UNKNOWN`` to ``True``, then ``schema: {}`` at every endpoint. 185 | For a single endpoint, use ``allow_unknown: True`` instead. 186 | 187 | .. _schema_validation: 188 | 189 | Schema validation 190 | ----------------- 191 | 192 | By default, schemas are validated to ensure they conform to the structure 193 | documented in :ref:`schema`. 194 | 195 | In order to deal with non-conforming schemas, add 196 | :ref:`custom_validation_rules` for non-conforming keys used in the schema. 197 | 198 | .. _Cerberus: http://python-cerberus.org 199 | .. _`source code`: https://github.com/pyeve/eve/blob/master/eve/io/mongo/validation.py 200 | .. _`function-based validation`: http://docs.python-cerberus.org/en/latest/customize.html#function-validator 201 | .. _`type coercion`: http://docs.python-cerberus.org/en/latest/usage.html#type-coercion 202 | -------------------------------------------------------------------------------- /eve/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Eve 5 | ~~~ 6 | 7 | An out-of-the-box REST Web API that's as dangerous as you want it to be. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | 12 | .. versionchanged:: 0.5 13 | 'SERVER_NAME' removed. 14 | 'QUERY_WHERE' added. 15 | 'QUERY_SORT' added. 16 | 'QUERY_PAGE' added. 17 | 'QUERY_MAX_RESULTS' added. 18 | 'QUERY_PROJECTION' added. 19 | 'QUERY_EMBEDDED' added. 20 | 'RFC1123_DATE_FORMAT' added. 21 | 22 | .. versionchanged:: 0.4 23 | 'META' defaults to '_meta'. 24 | 'ERROR' defaults to '_error'. 25 | Remove unnecessary commented code. 26 | 27 | .. versionchanged:: 0.2 28 | 'LINKS' defaults to '_links'. 29 | 'ITEMS' defaults to '_items'. 30 | 'STATUS' defaults to 'status'. 31 | 'ISSUES' defaults to 'issues'. 32 | 33 | .. versionchanged:: 0.1.1 34 | 'SERVER_NAME' defaults to None. 35 | 36 | .. versionchagned:: 0.0.9 37 | 'DATE_FORMAT now using GMT instead of UTC. 38 | 39 | """ 40 | 41 | __version__ = "2.2.0" 42 | 43 | # RFC 1123 (ex RFC 822) 44 | DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" 45 | RFC1123_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" 46 | 47 | URL_PREFIX = "" 48 | API_VERSION = "" 49 | PAGINATION = True 50 | PAGINATION_LIMIT = 50 51 | PAGINATION_DEFAULT = 25 52 | ID_FIELD = "_id" 53 | CACHE_CONTROL = "max-age=10,must-revalidate" # TODO confirm this value 54 | CACHE_EXPIRES = 10 55 | 56 | ALLOW_CUSTOM_FIELDS_IN_GEOJSON = False 57 | 58 | RESOURCE_METHODS = ["GET"] 59 | ITEM_METHODS = ["GET"] 60 | ITEM_LOOKUP = True 61 | ITEM_LOOKUP_FIELD = ID_FIELD 62 | ITEM_URL = 'regex("[a-f0-9]{24}")' 63 | 64 | STATUS_OK = "OK" 65 | STATUS_ERR = "ERR" 66 | LAST_UPDATED = "_updated" 67 | DATE_CREATED = "_created" 68 | ISSUES = "_issues" 69 | STATUS = "_status" 70 | ERROR = "_error" 71 | ITEMS = "_items" 72 | LINKS = "_links" 73 | ETAG = "_etag" 74 | VERSION = "_version" 75 | META = "_meta" 76 | INFO = None 77 | 78 | QUERY_WHERE = "where" 79 | QUERY_SORT = "sort" 80 | QUERY_PAGE = "page" 81 | QUERY_MAX_RESULTS = "max_results" 82 | QUERY_EMBEDDED = "embedded" 83 | QUERY_PROJECTION = "projection" 84 | 85 | VALIDATION_ERROR_STATUS = 422 86 | VALIDATION_ERROR_AS_LIST = False 87 | 88 | # must be the last line (will raise W402 on pyflakes) 89 | from eve.flaskapp import Eve # noqa 90 | -------------------------------------------------------------------------------- /eve/endpoints.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.endpoints 5 | ~~~~~~~~~~~~~ 6 | 7 | This module implements the API endpoints. Each endpoint (resource, item, 8 | home) invokes the appropriate method handler, returning its response 9 | to the client, properly rendered. 10 | 11 | :copyright: (c) 2017 by Nicola Iarocci. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | import re 15 | 16 | from bson import tz_util 17 | from flask import Response, abort 18 | from flask import current_app as app 19 | from flask import request 20 | 21 | import eve 22 | from eve.auth import requires_auth, resource_auth 23 | from eve.methods import delete, deleteitem, get, getitem, patch, post, put 24 | from eve.methods.common import ratelimit 25 | from eve.render import send_response 26 | from eve.utils import config, date_to_rfc1123, weak_date 27 | 28 | 29 | def collections_endpoint(**lookup): 30 | """Resource endpoint handler 31 | 32 | :param url: the url that led here 33 | 34 | .. versionchanged:: 0.3 35 | Pass lookup query down to delete_resource, so it can properly process 36 | sub-resources. 37 | 38 | .. versionchanged:: 0.2 39 | Relying on request.endpoint to retrieve the resource being consumed. 40 | 41 | .. versionchanged:: 0.1.1 42 | Relying on request.path for determining the current endpoint url. 43 | 44 | .. versionchanged:: 0.0.7 45 | Using 'utils.request_method' helper function now. 46 | 47 | .. versionchanged:: 0.0.6 48 | Support for HEAD requests 49 | 50 | .. versionchanged:: 0.0.2 51 | Support for DELETE resource method. 52 | """ 53 | 54 | resource = _resource() 55 | response = None 56 | method = request.method 57 | if method in ("GET", "HEAD"): 58 | response = get(resource, lookup) 59 | elif method == "POST": 60 | response = post(resource) 61 | elif method == "DELETE": 62 | response = delete(resource, lookup) 63 | elif method == "OPTIONS": 64 | send_response(resource, response) 65 | else: 66 | abort(405) 67 | return send_response(resource, response) 68 | 69 | 70 | def item_endpoint(**lookup): 71 | """Item endpoint handler 72 | 73 | :param url: the url that led here 74 | :param lookup: sub resource query 75 | 76 | .. versionchanged:: 0.2 77 | Support for sub-resources. 78 | Relying on request.endpoint to retrieve the resource being consumed. 79 | 80 | .. versionchanged:: 0.1.1 81 | Relying on request.path for determining the current endpoint url. 82 | 83 | .. versionchanged:: 0.1.0 84 | Support for PUT method. 85 | 86 | .. versionchanged:: 0.0.7 87 | Using 'utils.request_method' helper function now. 88 | 89 | .. versionchanged:: 0.0.6 90 | Support for HEAD requests 91 | """ 92 | resource = _resource() 93 | response = None 94 | method = request.method 95 | if method in ("GET", "HEAD"): 96 | response = getitem(resource, **lookup) 97 | elif method == "PATCH": 98 | response = patch(resource, **lookup) 99 | elif method == "PUT": 100 | response = put(resource, **lookup) 101 | elif method == "DELETE": 102 | response = deleteitem(resource, **lookup) 103 | elif method == "OPTIONS": 104 | send_response(resource, response) 105 | else: 106 | abort(405) 107 | return send_response(resource, response) 108 | 109 | 110 | @ratelimit() 111 | @requires_auth("home") 112 | def home_endpoint(): 113 | """Home/API entry point. Will provide links to each available resource 114 | 115 | .. versionchanged:: 0.5 116 | Resource URLs are relative to API root. 117 | Don't list internal resources. 118 | 119 | .. versionchanged:: 0.4 120 | Prevent versioning collections from being added in links. 121 | 122 | .. versionchanged:: 0.2 123 | Use new 'resource_title' setting for link titles. 124 | 125 | .. versionchanged:: 0.1.0 126 | Support for optional HATEOAS. 127 | """ 128 | response = {} 129 | if config.INFO: 130 | info = {} 131 | info["server"] = "Eve" 132 | info["version"] = eve.__version__ 133 | if config.API_VERSION: 134 | info["api_version"] = config.API_VERSION 135 | response[config.INFO] = info 136 | 137 | if config.HATEOAS: 138 | links = [] 139 | for resource in config.DOMAIN.keys(): 140 | internal = config.DOMAIN[resource]["internal_resource"] 141 | if not resource.endswith(config.VERSIONS): 142 | if not bool(internal): 143 | links.append( 144 | { 145 | "href": "%s" % config.URLS[resource], 146 | "title": "%s" % config.DOMAIN[resource]["resource_title"], 147 | } 148 | ) 149 | if config.SCHEMA_ENDPOINT is not None: 150 | links.append( 151 | { 152 | "href": "%s" % config.SCHEMA_ENDPOINT, 153 | "title": "%s" % config.SCHEMA_ENDPOINT, 154 | } 155 | ) 156 | 157 | response[config.LINKS] = {"child": links} 158 | return send_response(None, (response,)) 159 | return send_response(None, (response,)) 160 | 161 | 162 | def error_endpoint(error): 163 | """Response returned when an error is raised by the API (e.g. my means of 164 | an abort(4xx). 165 | """ 166 | headers = [] 167 | 168 | try: 169 | headers.append(error.response.headers) 170 | except AttributeError: 171 | pass 172 | 173 | try: 174 | if error.www_authenticate is not None: 175 | headers.append(error.www_authenticate) 176 | except AttributeError: 177 | pass 178 | 179 | response = { 180 | config.STATUS: config.STATUS_ERR, 181 | config.ERROR: {"code": error.code, "message": error.description}, 182 | } 183 | return send_response(None, (response, None, None, error.code, headers)) 184 | 185 | 186 | def _resource(): 187 | return request.endpoint.split("|")[0] 188 | 189 | 190 | @requires_auth("media") 191 | def media_endpoint(_id): 192 | """This endpoint is active when RETURN_MEDIA_AS_URL is True. It retrieves 193 | a media file and streams it to the client. 194 | 195 | .. versionadded:: 0.6 196 | """ 197 | if request.method == "OPTIONS": 198 | return send_response(None, (None)) 199 | 200 | file_ = app.media.get(_id) 201 | if file_ is None: 202 | return abort(404) 203 | 204 | headers = { 205 | "Last-Modified": date_to_rfc1123(file_.upload_date), 206 | "Content-Length": file_.length, 207 | "Accept-Ranges": "bytes", 208 | } 209 | 210 | range_header = request.headers.get("Range") 211 | if range_header: 212 | status = 206 213 | 214 | size = file_.length 215 | try: 216 | m = re.search(r"(\d+)-(\d*)", range_header) 217 | begin, end = m.groups() 218 | begin = int(begin) 219 | end = int(end) 220 | except Exception: 221 | begin, end = 0, None 222 | 223 | length = size - begin 224 | if end is not None: 225 | length = end - begin + 1 226 | 227 | file_.seek(begin) 228 | 229 | data = file_.read(length) 230 | headers["Content-Range"] = "bytes {0}-{1}/{2}".format( 231 | begin, begin + length - 1, size 232 | ) 233 | else: 234 | if_modified_since = weak_date(request.headers.get("If-Modified-Since")) 235 | if if_modified_since: 236 | if not if_modified_since.tzinfo: 237 | if_modified_since = if_modified_since.replace(tzinfo=tz_util.utc) 238 | 239 | if if_modified_since > file_.upload_date: 240 | return Response(status=304) 241 | 242 | data = file_ 243 | status = 200 244 | 245 | response = Response( 246 | data, 247 | status=status, 248 | headers=headers, 249 | mimetype=file_.content_type, 250 | direct_passthrough=True, 251 | ) 252 | 253 | return send_response(None, (response,)) 254 | 255 | 256 | @requires_auth("resource") 257 | def schema_item_endpoint(resource): 258 | """This endpoint is active when SCHEMA_ENDPOINT != None. It returns the 259 | requested resource's schema definition in JSON format. 260 | """ 261 | resource_config = app.config["DOMAIN"].get(resource) 262 | if not resource_config or resource_config.get("internal_resource") is True: 263 | return abort(404) 264 | 265 | return send_response(None, (resource_config["schema"],)) 266 | 267 | 268 | @requires_auth("home") 269 | def schema_collection_endpoint(): 270 | """This endpoint is active when SCHEMA_ENDPOINT != None. It returns the 271 | schema definition for all public or request authenticated resources in 272 | JSON format. 273 | """ 274 | schemas = {} 275 | for resource_name, resource_config in app.config["DOMAIN"].items(): 276 | # skip versioned shadow collections 277 | if resource_name.endswith(config.VERSIONS): 278 | continue 279 | # skip internal resources 280 | internal = resource_config.get("internal_resource", False) 281 | if internal: 282 | continue 283 | # skip resources for which request does not have read authorization 284 | auth = resource_auth(resource_name) 285 | if auth and request.method not in resource_config["public_methods"]: 286 | roles = list(resource_config["allowed_roles"]) 287 | roles += resource_config["allowed_read_roles"] 288 | if not auth.authorized(roles, resource_name, request.method): 289 | continue 290 | # otherwise include this resource in domain wide schema response 291 | schemas[resource_name] = resource_config["schema"] 292 | 293 | return send_response(None, (schemas,)) 294 | -------------------------------------------------------------------------------- /eve/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.exceptions 5 | ~~~~~~~~~~~~~~ 6 | 7 | This module implements Eve custom exceptions. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | 14 | class ConfigException(Exception): 15 | """Raised when errors are found in the configuration settings (usually 16 | `settings.py`). 17 | """ 18 | 19 | pass 20 | 21 | 22 | class SchemaException(ConfigException): 23 | """Raised when errors are found in a field schema definition""" 24 | 25 | pass 26 | -------------------------------------------------------------------------------- /eve/io/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.io 5 | ~~~~~~ 6 | 7 | This package implements the data layers supported by Eve. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | # flake8: noqa 14 | from eve.io.base import ConnectionException, DataLayer 15 | -------------------------------------------------------------------------------- /eve/io/media.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.io.media 5 | ~~~~~~~~~~~~ 6 | 7 | Media storage for Eve-powered APIs. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | 14 | class MediaStorage(): 15 | """The MediaStorage class provides a standardized API for storing files, 16 | along with a set of default behaviors that all other storage systems can 17 | inherit or override as necessary. 18 | 19 | ..versionadded:: 0.3 20 | """ 21 | 22 | def __init__(self, app=None): 23 | """ 24 | :param app: the flask application (eve itself). This can be used by 25 | the class to access, amongst other things, the app.config object to 26 | retrieve class-specific settings. 27 | """ 28 | self.app = app 29 | 30 | def get(self, id_or_filename, resource=None): 31 | """Opens the file given by name or unique id. Note that although the 32 | returned file is guaranteed to be a File object, it might actually be 33 | some subclass. Returns None if no file was found. 34 | """ 35 | raise NotImplementedError 36 | 37 | def put(self, content, filename=None, content_type=None, resource=None): 38 | """Saves a new file using the storage system, preferably with the name 39 | specified. If there already exists a file with this name name, the 40 | storage system may modify the filename as necessary to get a unique 41 | name. Depending on the storage system, a unique id or the actual name 42 | of the stored file will be returned. The content type argument is used 43 | to appropriately identify the file when it is retrieved. 44 | 45 | .. versionchanged:: 0.5 46 | Allow filename to be optional (#414). 47 | """ 48 | raise NotImplementedError 49 | 50 | def delete(self, id_or_filename, resource=None): 51 | """Deletes the file referenced by name or unique id. If deletion is 52 | not supported on the target storage system this will raise 53 | NotImplementedError instead 54 | """ 55 | raise NotImplementedError 56 | 57 | def exists(self, id_or_filename, resource=None): 58 | """Returns True if a file referenced by the given name or unique id 59 | already exists in the storage system, or False if the name is available 60 | for a new file. 61 | """ 62 | raise NotImplementedError 63 | -------------------------------------------------------------------------------- /eve/io/mongo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.io.mongo 5 | ~~~~~~~~~~~~ 6 | 7 | This package implements the MongoDB data layer. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | # flake8: noqa 14 | from eve.io.mongo.mongo import Mongo, MongoJSONEncoder, ensure_mongo_indexes 15 | from eve.io.mongo.media import GridFSMediaStorage 16 | from eve.io.mongo.validation import Validator 17 | -------------------------------------------------------------------------------- /eve/io/mongo/flask_pymongo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.io.mongo.flask_pymongo 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | Flask extension to create Mongo connection and database based on 8 | configuration. 9 | 10 | :copyright: (c) 2017 by Nicola Iarocci. 11 | :license: BSD, see LICENSE for more details. 12 | """ 13 | from bson import UuidRepresentation 14 | from flask import current_app 15 | from pymongo import MongoClient, uri_parser 16 | 17 | 18 | class PyMongo(): 19 | """ 20 | Creates Mongo connection and database based on Flask configuration. 21 | """ 22 | 23 | def __init__(self, app, config_prefix="MONGO"): 24 | if "pymongo" not in app.extensions: 25 | app.extensions["pymongo"] = {} 26 | 27 | if config_prefix in app.extensions["pymongo"]: 28 | raise Exception('duplicate config_prefix "%s"' % config_prefix) 29 | 30 | self.config_prefix = config_prefix 31 | 32 | def key(suffix): 33 | return "%s_%s" % (config_prefix, suffix) 34 | 35 | def config_to_kwargs(mapping): 36 | """ 37 | Convert config options to kwargs according to provided mapping 38 | information. 39 | """ 40 | kwargs = {} 41 | for option, arg in mapping.items(): 42 | if key(option) in app.config: 43 | kwargs[arg] = app.config[key(option)] 44 | return kwargs 45 | 46 | app.config.setdefault(key("HOST"), "localhost") 47 | app.config.setdefault(key("PORT"), 27017) 48 | app.config.setdefault(key("DBNAME"), app.name) 49 | app.config.setdefault(key("WRITE_CONCERN"), {"w": 1}) 50 | client_kwargs = {"appname": app.name, "connect": True, "tz_aware": True} 51 | if key("OPTIONS") in app.config: 52 | client_kwargs.update(app.config[key("OPTIONS")]) 53 | 54 | if key("WRITE_CONCERN") in app.config: 55 | # w, wtimeout, j and fsync 56 | client_kwargs.update(app.config[key("WRITE_CONCERN")]) 57 | 58 | if key("REPLICA_SET") in app.config: 59 | client_kwargs["replicaset"] = app.config[key("REPLICA_SET")] 60 | 61 | uri_parser.validate_options(client_kwargs) 62 | 63 | if key("URI") in app.config: 64 | host = app.config[key("URI")] 65 | # raises an exception if uri is invalid 66 | mongo_settings = uri_parser.parse_uri(host) 67 | 68 | # extract username and password from uri 69 | if mongo_settings.get("username"): 70 | client_kwargs["username"] = mongo_settings["username"] 71 | client_kwargs["password"] = mongo_settings["password"] 72 | 73 | # extract default database from uri 74 | dbname = mongo_settings.get("database") 75 | if not dbname: 76 | dbname = app.config[key("DBNAME")] 77 | 78 | # extract auth source from uri 79 | auth_source = mongo_settings["options"].get("authSource") 80 | if not auth_source: 81 | auth_source = dbname 82 | else: 83 | dbname = app.config[key("DBNAME")] 84 | auth_source = dbname 85 | host = app.config[key("HOST")] 86 | client_kwargs["port"] = app.config[key("PORT")] 87 | 88 | client_kwargs["host"] = host 89 | client_kwargs["authSource"] = auth_source 90 | 91 | if key("DOCUMENT_CLASS") in app.config: 92 | client_kwargs["document_class"] = app.config[key("DOCUMENT_CLASS")] 93 | 94 | auth_kwargs = {} 95 | if key("USERNAME") in app.config: 96 | app.config.setdefault(key("PASSWORD"), None) 97 | username = app.config[key("USERNAME")] 98 | password = app.config[key("PASSWORD")] 99 | auth = (username, password) 100 | if any(auth) and not all(auth): 101 | raise Exception("Must set both USERNAME and PASSWORD or neither") 102 | client_kwargs["username"] = username 103 | client_kwargs["password"] = password 104 | if any(auth): 105 | auth_mapping = { 106 | "AUTH_MECHANISM": "authMechanism", 107 | "AUTH_SOURCE": "authSource", 108 | "AUTH_MECHANISM_PROPERTIES": "authMechanismProperties", 109 | } 110 | auth_kwargs = config_to_kwargs(auth_mapping) 111 | 112 | cx = MongoClient(**{**client_kwargs, **auth_kwargs}) 113 | db = cx[dbname] 114 | 115 | app.extensions["pymongo"][config_prefix] = (cx, db) 116 | 117 | @property 118 | def cx(self): 119 | """ 120 | Automatically created :class:`~pymongo.Connection` object corresponding 121 | to the provided configuration parameters. 122 | """ 123 | if self.config_prefix not in current_app.extensions["pymongo"]: 124 | raise Exception("flask_pymongo extensions is not initialized") 125 | return current_app.extensions["pymongo"][self.config_prefix][0] 126 | 127 | @property 128 | def db(self): 129 | """ 130 | Automatically created :class:`~pymongo.Database` object 131 | corresponding to the provided configuration parameters. 132 | """ 133 | if self.config_prefix not in current_app.extensions["pymongo"]: 134 | raise Exception("flask_pymongo extensions is not initialized") 135 | return current_app.extensions["pymongo"][self.config_prefix][1] 136 | -------------------------------------------------------------------------------- /eve/io/mongo/geo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.io.mongo.geo 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | Geospatial functions and classes for mongo IO layer 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | from eve.utils import config 13 | 14 | 15 | class GeoJSON(dict): 16 | def __init__(self, json): 17 | try: 18 | self["type"] = json["type"] 19 | except KeyError: 20 | raise TypeError("Not compliant to GeoJSON") 21 | self.update(json) 22 | if not config.ALLOW_CUSTOM_FIELDS_IN_GEOJSON and len(self.keys()) != 2: 23 | raise TypeError("Not compliant to GeoJSON") 24 | 25 | def _correct_position(self, position): 26 | return ( 27 | isinstance(position, list) 28 | and len(position) > 1 29 | and all(isinstance(pos, (int, float)) for pos in position) 30 | ) 31 | 32 | 33 | class Geometry(GeoJSON): 34 | def __init__(self, json): 35 | super().__init__(json) 36 | try: 37 | if ( 38 | not isinstance(self["coordinates"], list) 39 | or self["type"] != self.__class__.__name__ 40 | ): 41 | raise TypeError 42 | except (KeyError, TypeError): 43 | raise TypeError("Geometry not compliant to GeoJSON") 44 | 45 | 46 | class GeometryCollection(GeoJSON): 47 | def __init__(self, json): 48 | super().__init__(json) 49 | try: 50 | if not isinstance(self["geometries"], list): 51 | raise TypeError 52 | for geometry in self["geometries"]: 53 | factory = factories[geometry["type"]] 54 | factory(geometry) 55 | except (KeyError, TypeError, AttributeError): 56 | raise TypeError("Geometry not compliant to GeoJSON") 57 | 58 | 59 | class Point(Geometry): 60 | def __init__(self, json): 61 | super().__init__(json) 62 | if not self._correct_position(self["coordinates"]): 63 | raise TypeError 64 | 65 | 66 | class MultiPoint(GeoJSON): 67 | def __init__(self, json): 68 | super().__init__(json) 69 | for position in self["coordinates"]: 70 | if not self._correct_position(position): 71 | raise TypeError 72 | 73 | 74 | class LineString(GeoJSON): 75 | def __init__(self, json): 76 | super().__init__(json) 77 | for position in self["coordinates"]: 78 | if not self._correct_position(position): 79 | raise TypeError 80 | 81 | 82 | class MultiLineString(GeoJSON): 83 | def __init__(self, json): 84 | super().__init__(json) 85 | for linestring in self["coordinates"]: 86 | for position in linestring: 87 | if not self._correct_position(position): 88 | raise TypeError 89 | 90 | 91 | class Polygon(GeoJSON): 92 | def __init__(self, json): 93 | super().__init__(json) 94 | for linestring in self["coordinates"]: 95 | for position in linestring: 96 | if not self._correct_position(position): 97 | raise TypeError 98 | 99 | 100 | class MultiPolygon(GeoJSON): 101 | def __init__(self, json): 102 | super().__init__(json) 103 | for polygon in self["coordinates"]: 104 | for linestring in polygon: 105 | for position in linestring: 106 | if not self._correct_position(position): 107 | raise TypeError 108 | 109 | 110 | class Feature(GeoJSON): 111 | def __init__(self, json): 112 | super().__init__(json) 113 | try: 114 | geometry = self["geometry"] 115 | factory = factories[geometry["type"]] 116 | factory(geometry) 117 | 118 | except (KeyError, TypeError, AttributeError): 119 | raise TypeError("Feature not compliant to GeoJSON") 120 | 121 | 122 | class FeatureCollection(GeoJSON): 123 | def __init__(self, json): 124 | super().__init__(json) 125 | try: 126 | if not isinstance(self["features"], list): 127 | raise TypeError 128 | for feature in self["features"]: 129 | Feature(feature) 130 | except (KeyError, TypeError, AttributeError): 131 | raise TypeError("FeatureCollection not compliant to GeoJSON") 132 | 133 | 134 | factories = dict( 135 | [ 136 | (_type.__name__, _type) 137 | for _type in [ 138 | GeometryCollection, 139 | Point, 140 | MultiPoint, 141 | LineString, 142 | MultiLineString, 143 | Polygon, 144 | MultiPolygon, 145 | ] 146 | ] 147 | ) 148 | -------------------------------------------------------------------------------- /eve/io/mongo/media.py: -------------------------------------------------------------------------------- 1 | """ 2 | eve.io.mongo.media 3 | ~~~~~~~~~~~~~~~~~~ 4 | 5 | GridFS media storage for Eve-powered APIs. 6 | 7 | :copyright: (c) 2017 by Nicola Iarocci. 8 | :license: BSD, see LICENSE for more details. 9 | """ 10 | from bson import ObjectId 11 | from flask import Flask 12 | from gridfs import GridFS 13 | 14 | from eve.io.media import MediaStorage 15 | from eve.io.mongo import Mongo 16 | from eve.utils import str_type 17 | 18 | 19 | class GridFSMediaStorage(MediaStorage): 20 | """The GridFSMediaStorage class stores files into GridFS. 21 | 22 | ..versionadded:: 0.3 23 | """ 24 | 25 | def __init__(self, app=None): 26 | """ 27 | :param app: the flask application (eve itself). This can be used by 28 | the class to access, amongst other things, the app.config object to 29 | retrieve class-specific settings. 30 | 31 | .. versionchanged:: 0.6 32 | Support for multiple, cached, GridFS instances 33 | """ 34 | super().__init__(app) 35 | 36 | self.validate() 37 | self._fs = {} 38 | 39 | def validate(self): 40 | """Make sure that the application data layer is a eve.io.mongo.Mongo 41 | instance. 42 | """ 43 | if self.app is None: 44 | raise TypeError("Application object cannot be None") 45 | 46 | if not isinstance(self.app, Flask): 47 | raise TypeError("Application object must be a Eve application") 48 | 49 | def fs(self, resource=None): 50 | """Provides the instance-level GridFS instance, instantiating it if 51 | needed. 52 | 53 | .. versionchanged:: 0.6 54 | Support for multiple, cached, GridFS instances 55 | """ 56 | driver = self.app.data 57 | if driver is None or not isinstance(driver, Mongo): 58 | raise TypeError("Application data object must be of eve.io.Mongo " "type.") 59 | 60 | px = driver.current_mongo_prefix(resource) 61 | if px not in self._fs: 62 | self._fs[px] = GridFS(driver.pymongo(prefix=px).db) 63 | return self._fs[px] 64 | 65 | def get(self, _id, resource=None): 66 | """Returns the file given by unique id. Returns None if no file was 67 | found. 68 | 69 | .. versionchanged: 0.6 70 | Support for _id as string. 71 | """ 72 | if isinstance(_id, str_type): 73 | # Convert to unicode because ObjectId() interprets 12-character 74 | # strings (but not unicode) as binary representations of ObjectId. 75 | try: 76 | _id = ObjectId(unicode(_id)) 77 | except NameError: 78 | _id = ObjectId(_id) 79 | 80 | _file = None 81 | try: 82 | _file = self.fs(resource).get(_id) 83 | except Exception: 84 | pass 85 | return _file 86 | 87 | def put(self, content, filename=None, content_type=None, resource=None): 88 | """Saves a new file in GridFS. Returns the unique id of the stored 89 | file. Also stores content type of the file. 90 | """ 91 | return self.fs(resource).put( 92 | content, filename=filename, content_type=content_type 93 | ) 94 | 95 | def delete(self, _id, resource=None): 96 | """Deletes the file referenced by unique id.""" 97 | self.fs(resource).delete(_id) 98 | 99 | def exists(self, id_or_document, resource=None): 100 | """Returns True if a file referenced by the unique id or the query 101 | document already exists, False otherwise. 102 | 103 | Valid query: {'filename': 'file.txt'} 104 | """ 105 | return self.fs(resource).exists(id_or_document) 106 | -------------------------------------------------------------------------------- /eve/io/mongo/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.io.mongo.parser 5 | ~~~~~~~~~~~~~~~~~~~ 6 | 7 | This module implements a Python-to-Mongo syntax parser. Allows the MongoDB 8 | data-layer to seamlessly respond to a Python-like query. 9 | 10 | :copyright: (c) 2017 by Nicola Iarocci. 11 | :license: BSD, see LICENSE for more details. 12 | """ 13 | 14 | import ast 15 | import sys 16 | from datetime import datetime # noqa 17 | 18 | from bson import ObjectId # noqa 19 | 20 | 21 | def parse(expression): 22 | """Given a python-like conditional statement, returns the equivalent 23 | mongo-like query expression. Conditional and boolean operators (==, <=, >=, 24 | !=, >, <) along with a couple function calls (ObjectId(), datetime()) are 25 | supported. 26 | """ 27 | v = MongoVisitor() 28 | try: 29 | v.visit(ast.parse(expression)) 30 | except SyntaxError as e: 31 | e = ParseError(e) 32 | e.__traceback__ = sys.exc_info()[2] 33 | raise e 34 | return v.mongo_query 35 | 36 | 37 | class ParseError(ValueError): 38 | pass 39 | 40 | 41 | class MongoVisitor(ast.NodeVisitor): 42 | """Implements the python-to-mongo parser. Only Python conditional 43 | statements are supported, however nested, combined with most common compare 44 | and boolean operators (And and Or). 45 | 46 | Supported compare operators: ==, >, <, !=, >=, <= 47 | Supported boolean operators: And, Or 48 | """ 49 | 50 | op_mapper = { 51 | ast.Eq: "", 52 | ast.Gt: "$gt", 53 | ast.GtE: "$gte", 54 | ast.Lt: "$lt", 55 | ast.LtE: "$lte", 56 | ast.NotEq: "$ne", 57 | ast.Or: "$or", 58 | ast.And: "$and", 59 | } 60 | 61 | def visit_Module(self, node): 62 | """Module handler, our entry point.""" 63 | self.mongo_query = {} 64 | self.ops = [] 65 | self.current_value = None 66 | 67 | # perform the magic. 68 | self.generic_visit(node) 69 | 70 | # if we didn't obtain a query, it is likely that an unsupported 71 | # python expression has been passed. 72 | if not self.mongo_query: 73 | raise ParseError( 74 | "Only conditional statements with boolean " 75 | "(and, or) and comparison operators are " 76 | "supported." 77 | ) 78 | 79 | def visit_Expr(self, node): 80 | """Make sure that we are parsing compare or boolean operators""" 81 | if not ( 82 | isinstance(node.value, ast.Compare) or isinstance(node.value, ast.BoolOp) 83 | ): 84 | raise ParseError("Will only parse conditional statements") 85 | self.generic_visit(node) 86 | 87 | def visit_Compare(self, node): 88 | """Compare operator handler.""" 89 | self.visit(node.left) 90 | left = self.current_value 91 | 92 | operator = self.op_mapper[node.ops[0].__class__] if node.ops else None 93 | 94 | if node.comparators: 95 | comparator = node.comparators[0] 96 | self.visit(comparator) 97 | 98 | if operator != "": 99 | value = {operator: self.current_value} 100 | else: 101 | value = self.current_value 102 | 103 | if self.ops: 104 | self.ops[-1].append({left: value}) 105 | else: 106 | self.mongo_query[left] = value 107 | 108 | def visit_BoolOp(self, node): 109 | """Boolean operator handler.""" 110 | op = self.op_mapper[node.op.__class__] 111 | self.ops.append([]) 112 | for value in node.values: 113 | self.visit(value) 114 | 115 | c = self.ops.pop() 116 | if self.ops: 117 | self.ops[-1].append({op: c}) 118 | else: 119 | self.mongo_query[op] = c 120 | 121 | def visit_Call(self, node): 122 | """A couple function calls are supported: bson's ObjectId() and 123 | datetime(). 124 | """ 125 | if isinstance(node.func, ast.Name): 126 | if node.func.id == "ObjectId": 127 | try: 128 | self.current_value = ObjectId(node.args[0].s) 129 | except Exception: 130 | pass 131 | elif node.func.id == "datetime": 132 | values = [] 133 | for arg in node.args: 134 | values.append(arg.n) 135 | try: 136 | self.current_value = datetime(*values) 137 | except Exception: 138 | pass 139 | 140 | def visit_Attribute(self, node): 141 | """Attribute handler ('Contact.Id').""" 142 | self.visit(node.value) 143 | self.current_value += "." + node.attr 144 | 145 | def visit_Name(self, node): 146 | """Names handler.""" 147 | self.current_value = node.id 148 | 149 | def visit_Num(self, node): 150 | """Numbers handler.""" 151 | self.current_value = node.n 152 | 153 | def visit_Str(self, node): 154 | """Strings handler.""" 155 | self.current_value = node.s 156 | -------------------------------------------------------------------------------- /eve/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import logging 4 | 5 | from flask import request 6 | 7 | # TODO right now we are only logging exceptions. We should probably 8 | # add support for some INFO and maybe DEBUG level logging (like, log each time 9 | # a endpoint is hit, etc.) 10 | 11 | 12 | class RequestFilter(logging.Filter): 13 | """Adds Flask's request metadata to the log record so handlers can log 14 | this information too. 15 | 16 | import logging 17 | 18 | handler = logging.FileHandler('app.log') 19 | handler.setFormatter(logging.Formatter( 20 | '%(asctime)s %(levelname)s: %(message)s ' 21 | '[in %(filename)s:%(lineno)d] -- ip: %(clientip)s url: %(url)s')) 22 | app.logger.addHandler(handler) 23 | 24 | The above example adds 'clientip' and request 'url' to every log record. 25 | 26 | Note that the app.logger can also be used by callback functions. 27 | 28 | def log_a_get(resource, request, payload): 29 | app.logger.info('we just responded to a GET request!') 30 | 31 | app = Eve() 32 | app.on_post_GET += log_a_get 33 | 34 | .. versionadded:: 0.6 35 | 36 | """ 37 | 38 | def filter(self, record): 39 | if request: 40 | record.clientip = request.remote_addr 41 | record.url = request.url 42 | record.method = request.method 43 | else: 44 | record.clientip = None 45 | record.url = None 46 | record.method = None 47 | 48 | return True 49 | -------------------------------------------------------------------------------- /eve/methods/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.methods 5 | ~~~~~~~~~~~ 6 | 7 | This package implements the HTTP methods supported by Eve. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | from eve.methods.delete import delete, deleteitem 14 | # flake8: noqa 15 | from eve.methods.get import get, getitem 16 | from eve.methods.patch import patch 17 | from eve.methods.post import post 18 | from eve.methods.put import put 19 | -------------------------------------------------------------------------------- /eve/methods/delete.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.methods.delete 5 | ~~~~~~~~~~~~~~~~~~ 6 | 7 | This module implements the DELETE method. 8 | 9 | :copyright: (c) 2017 by Nicola Iarocci. 10 | :license: BSD, see LICENSE for more details. 11 | """ 12 | 13 | import copy 14 | 15 | from flask import abort 16 | from flask import current_app as app 17 | 18 | from eve.auth import requires_auth 19 | from eve.methods.common import (get_document, oplog_push, pre_event, ratelimit, 20 | resolve_document_etag, utcnow) 21 | from eve.utils import ParsedRequest, config 22 | from eve.versioning import (insert_versioning_documents, late_versioning_catch, 23 | resolve_document_version, versioned_id_field) 24 | 25 | 26 | def all_done(): 27 | return {}, None, None, 204 28 | 29 | 30 | @ratelimit() 31 | @requires_auth("item") 32 | @pre_event 33 | def deleteitem(resource, **lookup): 34 | """ 35 | Default function for handling DELETE requests, it has decorators for 36 | rate limiting, authentication and for raising pre-request events. 37 | After the decorators are applied forwards to call to 38 | :func:`deleteitem_internal` 39 | 40 | .. versionchanged:: 0.5 41 | Split into deleteitem() and deleteitem_internal(). 42 | """ 43 | return deleteitem_internal(resource, concurrency_check=True, **lookup) 44 | 45 | 46 | def deleteitem_internal( 47 | resource, concurrency_check=False, suppress_callbacks=False, original=None, **lookup 48 | ): 49 | """Intended for internal delete calls, this method is not rate limited, 50 | authentication is not checked, pre-request events are not raised, and 51 | concurrency checking is optional. Deletes a resource item. 52 | 53 | :param resource: name of the resource to which the item(s) belong. 54 | :param concurrency_check: concurrency check switch (bool) 55 | :param original: original document if already fetched from the database 56 | :param **lookup: item lookup query. 57 | 58 | .. versionchanged:: 0.6 59 | Support for soft delete. 60 | 61 | .. versionchanged:: 0.5 62 | Return 204 NoContent instead of 200. 63 | Push updates to OpLog. 64 | Original deleteitem() has been split into deleteitem() and 65 | deleteitem_internal(). 66 | 67 | .. versionchanged:: 0.4 68 | Fix #284: If you have a media field, and set datasource projection to 69 | 0 for that field, the media will not be deleted. 70 | Support for document versioning. 71 | 'on_delete_item' events raised before performing the delete. 72 | 'on_deleted_item' events raised after performing the delete. 73 | 74 | .. versionchanged:: 0.3 75 | Delete media files as needed. 76 | Pass the explicit query filter to the data driver, as it does not 77 | support the id argument anymore. 78 | 79 | .. versionchanged:: 0.2 80 | Raise pre_ event. 81 | 82 | .. versionchanged:: 0.0.7 83 | Support for Rate-Limiting. 84 | 85 | .. versionchanged:: 0.0.5 86 | Pass current resource to ``parse_request``, allowing for proper 87 | processing of new configuration settings: `filters`, `sorting`, `paging`. 88 | 89 | .. versionchanged:: 0.0.4 90 | Added the ``requires_auth`` decorator. 91 | """ 92 | resource_def = config.DOMAIN[resource] 93 | soft_delete_enabled = resource_def["soft_delete"] 94 | original = get_document( 95 | resource, 96 | concurrency_check, 97 | original, 98 | force_auth_field_projection=soft_delete_enabled, 99 | **lookup 100 | ) 101 | if not original or (soft_delete_enabled and original.get(config.DELETED) is True): 102 | return all_done() 103 | 104 | # notify callbacks 105 | if not suppress_callbacks: 106 | getattr(app, "on_delete_item")(resource, original) 107 | getattr(app, "on_delete_item_%s" % resource)(original) 108 | 109 | if soft_delete_enabled: 110 | # Instead of removing the document from the db, just mark it as deleted 111 | marked_document = copy.deepcopy(original) 112 | 113 | # Set DELETED flag and update metadata 114 | last_modified = utcnow() 115 | marked_document[config.DELETED] = True 116 | marked_document[config.LAST_UPDATED] = last_modified 117 | 118 | if config.IF_MATCH: 119 | resolve_document_etag(marked_document, resource) 120 | 121 | resolve_document_version(marked_document, resource, "DELETE", original) 122 | 123 | # Update document in database (including version collection if needed) 124 | id = original[resource_def["id_field"]] 125 | try: 126 | app.data.replace(resource, id, marked_document, original) 127 | except app.data.OriginalChangedError: 128 | if concurrency_check: 129 | abort(412, description="Client and server etags don't match") 130 | 131 | # create previous version if it wasn't already there 132 | late_versioning_catch(original, resource) 133 | # and add deleted version 134 | insert_versioning_documents(resource, marked_document) 135 | # update oplog if needed 136 | oplog_push(resource, marked_document, "DELETE", id) 137 | 138 | else: 139 | # Delete the document for real 140 | 141 | # media cleanup 142 | media_fields = app.config["DOMAIN"][resource]["_media"] 143 | 144 | # document might miss one or more media fields because of datasource 145 | # and/or client projection. 146 | missing_media_fields = [f for f in media_fields if f not in original] 147 | if missing_media_fields: 148 | # retrieve the whole document so we have all media fields available 149 | # Should be very a rare occurrence. We can't get rid of the 150 | # get_document() call since it also deals with etag matching, which 151 | # is still needed. Also, this lookup should never fail. 152 | # TODO not happy with this hack. Not at all. Is there a better way? 153 | original = app.data.find_one_raw(resource, **lookup) 154 | 155 | for field in media_fields: 156 | if field in original: 157 | media_field = original[field] 158 | if isinstance(media_field, list): 159 | for file_id in media_field: 160 | app.media.delete(file_id, resource) 161 | else: 162 | app.media.delete(original[field], resource) 163 | 164 | id = original[resource_def["id_field"]] 165 | app.data.remove(resource, lookup) 166 | 167 | # TODO: should attempt to delete version collection even if setting is 168 | # off 169 | if app.config["DOMAIN"][resource]["versioning"] is True: 170 | app.data.remove( 171 | resource + config.VERSIONS, 172 | {versioned_id_field(resource_def): original[resource_def["id_field"]]}, 173 | ) 174 | 175 | # update oplog if needed 176 | oplog_push(resource, original, "DELETE", id) 177 | 178 | if not suppress_callbacks: 179 | getattr(app, "on_deleted_item")(resource, original) 180 | getattr(app, "on_deleted_item_%s" % resource)(original) 181 | 182 | return all_done() 183 | 184 | 185 | @requires_auth("resource") 186 | @pre_event 187 | def delete(resource, **lookup): 188 | """Deletes all item of a resource (collection in MongoDB terms). Won't 189 | drop indexes. Use with caution! 190 | 191 | .. versionchanged:: 0.5 192 | Return 204 NoContent instead of 200. 193 | 194 | .. versionchanged:: 0.4 195 | Support for document versioning. 196 | 'on_delete_resource' raised before performing the actual delete. 197 | 'on_deleted_resource' raised after performing the delete 198 | 199 | .. versionchanged:: 0.3 200 | Support for the lookup filter, which allows for devolution of 201 | sub-resources (only delete documents that match a given condition). 202 | 203 | .. versionchanged:: 0.0.4 204 | Added the ``requires_auth`` decorator. 205 | 206 | .. versionadded:: 0.0.2 207 | """ 208 | 209 | resource_def = config.DOMAIN[resource] 210 | getattr(app, "on_delete_resource")(resource) 211 | getattr(app, "on_delete_resource_%s" % resource)() 212 | default_request = ParsedRequest() 213 | if resource_def["soft_delete"]: 214 | # get_document should always fetch soft deleted documents from the db 215 | # callers must handle soft deleted documents 216 | default_request.show_deleted = True 217 | result, _ = app.data.find(resource, default_request, lookup) 218 | originals = list(result) 219 | if not originals: 220 | return all_done() 221 | # I add new callback as I want the framework to be retro-compatible 222 | getattr(app, "on_delete_resource_originals")(resource, originals, lookup) 223 | getattr(app, "on_delete_resource_originals_%s" % resource)(originals, lookup) 224 | id_field = resource_def["id_field"] 225 | 226 | if resource_def["soft_delete"]: 227 | # I need to check that I have at least some documents not soft_deleted 228 | # I skip all the soft_deleted documents 229 | originals = [x for x in originals if not x.get(config.DELETED)] 230 | if not originals: 231 | # Nothing to be deleted 232 | return all_done() 233 | for document in originals: 234 | lookup[id_field] = document[id_field] 235 | deleteitem_internal( 236 | resource, 237 | concurrency_check=False, 238 | suppress_callbacks=True, 239 | original=document, 240 | **lookup 241 | ) 242 | else: 243 | # TODO if the resource schema includes media files, these won't be 244 | # deleted by use of this global method (it should be disabled). Media 245 | # cleanup is handled at the item endpoint by the delete() method 246 | # (see above). 247 | app.data.remove(resource, lookup) 248 | 249 | # TODO: should attempt to delete version collection even if setting is 250 | # off 251 | if resource_def["versioning"] is True: 252 | app.data.remove(resource + config.VERSIONS, lookup) 253 | 254 | getattr(app, "on_deleted_resource")(resource) 255 | getattr(app, "on_deleted_resource_%s" % resource)() 256 | 257 | return all_done() 258 | -------------------------------------------------------------------------------- /eve/validation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | eve.validation 5 | ~~~~~~~~~~~~~~ 6 | 7 | Helper module. Allows eve submodules (methods.patch/post) to be fully 8 | datalayer-agnostic. Specialized Validator classes are implemented in the 9 | datalayer submodules. 10 | 11 | :copyright: (c) 2017 by Nicola Iarocci. 12 | :license: BSD, see LICENSE for more details. 13 | """ 14 | 15 | import copy 16 | 17 | import cerberus 18 | import cerberus.errors 19 | from cerberus import DocumentError, SchemaError # noqa 20 | 21 | from eve.utils import config 22 | 23 | 24 | class Validator(cerberus.Validator): 25 | def __init__(self, *args, **kwargs): 26 | if not config.VALIDATION_ERROR_AS_LIST: 27 | kwargs["error_handler"] = SingleErrorAsStringErrorHandler 28 | 29 | self.is_update_operation = False 30 | super().__init__(*args, **kwargs) 31 | 32 | def validate_update( 33 | self, document, document_id, persisted_document=None, normalize_document=True 34 | ): 35 | """Validate method to be invoked when performing an update, not an 36 | insert. 37 | 38 | :param document: the document to be validated. 39 | :param document_id: the unique id of the document. 40 | :param persisted_document: the persisted document to be updated. 41 | :param normalize_document: whether apply normalization during patch. 42 | """ 43 | self.is_update_operation = True 44 | self.document_id = document_id 45 | self.persisted_document = persisted_document 46 | return super().validate( 47 | document, update=True, normalize=normalize_document 48 | ) 49 | 50 | def validate_replace(self, document, document_id, persisted_document=None): 51 | """Validation method to be invoked when performing a document 52 | replacement. This differs from :func:`validation_update` since in this 53 | case we want to perform a full :func:`validate` (the new document is to 54 | be considered a new insertion and required fields needs validation). 55 | However, like with validate_update, we also want the current document_id 56 | not to be checked when validating 'unique' values. 57 | 58 | :param document: the document to be validated. 59 | :param document_id: the unique id of the document. 60 | :param persisted_document: the persisted document to be updated. 61 | 62 | .. versionadded:: 0.1.0 63 | """ 64 | self.document_id = document_id 65 | self.persisted_document = persisted_document 66 | return super().validate(document) 67 | 68 | def _normalize_default(self, mapping, schema, field): 69 | """{'nullable': True}""" 70 | 71 | # fields with no default are of no use here 72 | if "default" not in schema[field]: 73 | return 74 | 75 | # if the request already contains the field, we don't set any default 76 | if field in mapping: 77 | return 78 | 79 | # Field already set, we don't want to override with a default on an update 80 | if self.is_update_operation and field in self.persisted_document: 81 | return 82 | 83 | # If we reach here we are processing a field that has a default in the schema 84 | # and the request doesn't explicitly set it. So we are in one of this cases: 85 | # 86 | # - An initial POST 87 | # - A PATCH to an existing document where the field is not set 88 | # - A PUT to a document where the field maybe is set 89 | 90 | super()._normalize_default(mapping, schema, field) 91 | 92 | def _normalize_default_setter(self, mapping, schema, field): 93 | """{'oneof': [ 94 | {'type': 'callable'}, 95 | {'type': 'string'} 96 | ]}""" 97 | if not self.persisted_document or field not in self.persisted_document: 98 | super()._normalize_default_setter(mapping, schema, field) 99 | 100 | def _validate_dependencies(self, dependencies, field, value): 101 | """{'type': ['dict', 'hashable', 'list']}""" 102 | persisted = self._filter_persisted_fields_not_in_document(dependencies) 103 | if persisted: 104 | dcopy = copy.copy(self.document) 105 | for field in persisted: 106 | dcopy[field] = self.persisted_document[field] 107 | validator = self._get_child_validator() 108 | validator.validate(dcopy, update=self.update) 109 | self._error(validator._errors) 110 | else: 111 | super()._validate_dependencies(dependencies, field, value) 112 | 113 | def _filter_persisted_fields_not_in_document(self, fields): 114 | def persisted_but_not_in_document(field): 115 | return ( 116 | field not in self.document 117 | and self.persisted_document 118 | and field in self.persisted_document 119 | ) 120 | 121 | return [field for field in fields if persisted_but_not_in_document(field)] 122 | 123 | def _validate_readonly(self, read_only, field, value): 124 | """{'type': 'boolean'}""" 125 | persisted_value = ( 126 | self.persisted_document.get(field) if self.persisted_document else None 127 | ) 128 | if value != persisted_value: 129 | super()._validate_readonly(read_only, field, value) 130 | 131 | @property 132 | def resource(self): 133 | return self._config.get("resource", None) 134 | 135 | @resource.setter 136 | def resource(self, value): 137 | self._config["resource"] = value 138 | 139 | @property 140 | def document_id(self): 141 | return self._config.get("document_id", None) 142 | 143 | @document_id.setter 144 | def document_id(self, value): 145 | self._config["document_id"] = value 146 | 147 | @property 148 | def persisted_document(self): 149 | return self._config.get("persisted_document", None) 150 | 151 | @persisted_document.setter 152 | def persisted_document(self, value): 153 | self._config["persisted_document"] = value 154 | 155 | 156 | class SingleErrorAsStringErrorHandler(cerberus.errors.BasicErrorHandler): 157 | """Default Cerberus error handler for Eve. 158 | 159 | Since Cerberus 1.0, error messages for fields will always be returned as 160 | lists, even in the case of a single error. To maintain compatibility with 161 | clients, this error handler will unpack single-element error lists unless 162 | the config item VALIDATION_ERROR_AS_LIST is True. 163 | """ 164 | 165 | @property 166 | def pretty_tree(self): 167 | pretty = super().pretty_tree 168 | self._unpack_single_element_lists(pretty) 169 | return pretty 170 | 171 | def _unpack_single_element_lists(self, tree): 172 | for field in tree: 173 | error_list = tree[field] 174 | if len(error_list) > 0 and isinstance(tree[field][-1], dict): 175 | self._unpack_single_element_lists(tree[field][-1]) 176 | if len(tree[field]) == 1: 177 | tree[field] = tree[field][0] 178 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | - security folder: Authentication snippets 2 | - notifications.py: how to be notified and perform custom actions when requests are received. 3 | -------------------------------------------------------------------------------- /examples/notifications.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function 3 | 4 | """ 5 | Custom event notifications 6 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 7 | 8 | Flask supports callback functions via decorators such as 9 | `before_request` and `after_request`. Being a subclass of Flask, Eve 10 | supports this mechanism too, and it's pretty darn powerful. The catch is 11 | that you need to be quite familiar with Flask internals, so for example if 12 | you want to inspect the `request` object you have to explicitly import it 13 | from flask. 14 | 15 | Checkout Eve at https://github.com/pyeve/eve 16 | 17 | This snippet by Nicola Iarocci can be used freely for anything you like. 18 | Consider it public domain. 19 | """ 20 | from flask import request 21 | from notifications_settings import SETTINGS 22 | 23 | from eve import Eve 24 | 25 | app = Eve(auth=None, settings=SETTINGS) 26 | 27 | 28 | @app.before_request 29 | def before(): 30 | print("the request object ready to be processed:", request) 31 | 32 | 33 | @app.after_request 34 | def after(response): 35 | """ 36 | Your function must take one parameter, a `response_class` object and return 37 | a new response object or the same (see Flask documentation). 38 | """ 39 | print("and here we have the response object instead:", response) 40 | return response 41 | 42 | 43 | if __name__ == "__main__": 44 | app.run() 45 | -------------------------------------------------------------------------------- /examples/notifications_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | SETTINGS = {"DEBUG": True, "DOMAIN": {"test": {}}} 3 | -------------------------------------------------------------------------------- /examples/security/README: -------------------------------------------------------------------------------- 1 | - bcrypt.py: securing an Eve-powered API with bcrypt encoded passwords 2 | - roles.py: securing an Eve-powered API with role-based access control 3 | - sha1-hmac.py: securing an Eve-powered API with SHA1/HMAC encoded passwords 4 | - token.py: securing an Eve-powerd API with Token based authentication 5 | - hmac.py: secuing an Eve-powered API with HMAC-based authentication 6 | -------------------------------------------------------------------------------- /examples/security/bcrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Auth-BCrypt 5 | ~~~~~~~~~~~ 6 | 7 | Securing an Eve-powered API with Basic Authentication (RFC2617). 8 | 9 | This script assumes that user accounts are stored in a MongoDB collection 10 | ('accounts'), and that passwords are stored as BCrypt hashes. All API 11 | resources/methods will be secured unless they are made explicitly public 12 | (by fiddling with some settings you can open one or more resources and/or 13 | methods to public access -see docs). 14 | 15 | You will need to install py-bcrypt: ``pip install py-bcrypt`` 16 | 17 | Eve @ https://github.com/pyeve/eve 18 | 19 | This snippet by Nicola Iarocci can be used freely for anything you like. 20 | Consider it public domain. 21 | """ 22 | 23 | import bcrypt 24 | from settings_security import SETTINGS 25 | 26 | from eve import Eve 27 | from eve.auth import BasicAuth 28 | 29 | 30 | class BCryptAuth(BasicAuth): 31 | def check_auth(self, username, password, allowed_roles, resource, method): 32 | # use Eve's own db driver; no additional connections/resources are used 33 | accounts = app.data.driver.db["accounts"] 34 | account = accounts.find_one({"username": username}) 35 | return ( 36 | account 37 | and bcrypt.hashpw(password, account["password"]) == account["password"] 38 | ) 39 | 40 | 41 | if __name__ == "__main__": 42 | app = Eve(auth=BCryptAuth, settings=SETTINGS) 43 | app.run() 44 | -------------------------------------------------------------------------------- /examples/security/hmac.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Auth-HMAC 5 | ~~~~~~~~~ 6 | 7 | Securing an Eve-powered API with HMAC based Authentication. 8 | 9 | The ``eve.auth.HMACAuth`` class allows for custom Amazon S3-like 10 | authentication, which is basically a very secure custom authentication 11 | scheme built around the `Authorization` header. 12 | 13 | The server provides the client with a user id and a secret key through some 14 | out-of-band technique (e.g., the service sends the client an e-mail 15 | containing the user id and secret key). The client will use the supplied 16 | secret key to sign all requests. 17 | 18 | When the client wants to send a request he builds the complete request and 19 | then using the secret key computes a hash over the complete message body 20 | (and optionally some of the message headers if required) 21 | 22 | Next the client add the computed hash and his userid to the message in the 23 | Authorization header: 24 | 25 | Authorization: johndoe:uCMfSzkjue+HSDygYB5aEg== 26 | 27 | and sends it to the service. The service retrieves the userid from the 28 | message header and searches the private key for that user in its own 29 | database. Next he computes the hash over the message body (and selected 30 | headers) using the key to generate its hash. If the hash the client sends 31 | matches the hash the server computes the server knows the message was send 32 | by the real client and was not altered in any way. 33 | 34 | Really the only tricky part is sharing a secret key with the user and 35 | keeping that secure. That is why some services allow for generation of 36 | shared keys with a limited life time so you can give the key to a third 37 | party to temporarily work on your behalf. This is also the reason why the 38 | secret key is generally provided through out-of-band channels (often 39 | a webpage or, as said above, an email or plain old paper). 40 | 41 | The HMACAuth class also supports access roles. 42 | 43 | Checkout Eve at https://github.com/pyeve/eve 44 | 45 | This snippet by Nicola Iarocci can be used freely for anything you like. 46 | Consider it public domain. 47 | """ 48 | import hmac 49 | from hashlib import sha1 50 | 51 | from settings_security import SETTINGS 52 | 53 | from eve import Eve 54 | from eve.auth import HMACAuth 55 | 56 | 57 | class HMACAuth(HMACAuth): 58 | def check_auth( 59 | self, userid, hmac_hash, headers, data, allowed_roles, resource, method 60 | ): 61 | # use Eve's own db driver; no additional connections/resources are used 62 | accounts = app.data.driver.db["accounts"] 63 | user = accounts.find_one({"userid": userid}) 64 | if user: 65 | secret_key = user["secret_key"] 66 | # in this implementation we only hash request data, ignoring the 67 | # headers. 68 | return ( 69 | user and hmac.new(str(secret_key), str(data), sha1).hexdigest() == hmac_hash 70 | ) 71 | 72 | 73 | if __name__ == "__main__": 74 | app = Eve(auth=HMACAuth, settings=SETTINGS) 75 | app.run() 76 | -------------------------------------------------------------------------------- /examples/security/roles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Auth-SHA1/HMAC-Roles 5 | ~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Securing an Eve-powered API with Basic Authentication (RFC2617) and user 8 | roles. 9 | 10 | This script assumes that user accounts are stored in an 'accounts' MongoDB 11 | collection, that passwords are stored as SHA1/HMAC hashes and that user 12 | roles are stored in a 'roles' array. All API resources/methods will be 13 | secured unless they are made explicitly public (by fiddling with some 14 | settings you can open one or more resources and/or methods to public access 15 | -see docs). 16 | 17 | Since we are using werkzeug we don't need any extra import (werkzeug being 18 | one of Flask/Eve prerequisites). 19 | 20 | Checkout Eve at https://github.com/pyeve/eve 21 | 22 | This snippet by Nicola Iarocci can be used freely for anything you like. 23 | Consider it public domain. 24 | """ 25 | 26 | from settings_security import SETTINGS 27 | from werkzeug.security import check_password_hash 28 | 29 | from eve import Eve 30 | from eve.auth import BasicAuth 31 | 32 | 33 | class RolesAuth(BasicAuth): 34 | def check_auth(self, username, password, allowed_roles, resource, method): 35 | # use Eve's own db driver; no additional connections/resources are used 36 | accounts = app.data.driver.db["accounts"] 37 | lookup = {"username": username} 38 | if allowed_roles: 39 | # only retrieve a user if his roles match ``allowed_roles`` 40 | lookup["roles"] = {"$in": allowed_roles} 41 | account = accounts.find_one(lookup) 42 | return account and check_password_hash(account["password"], password) 43 | 44 | 45 | if __name__ == "__main__": 46 | app = Eve(auth=RolesAuth, settings=SETTINGS) 47 | app.run() 48 | -------------------------------------------------------------------------------- /examples/security/settings_security.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | SETTINGS = { 4 | "DEBUG": True, 5 | "MONGO_HOST": "localhost", 6 | "MONGO_PORT": 27017, 7 | "MONGO_DBNAME": "test_db", 8 | "DOMAIN": { 9 | "accounts": { 10 | "username": {"type": "string", "minlength": 5, "maxlength": 20}, 11 | "password": {"type": "string", "minlength": 5, "maxlength": 20}, 12 | "secret_key": {"type": "string", "minlength": 5, "maxlength": 20}, 13 | "roles": {"type": "string", "minlength": 10, "maxlength": 50}, 14 | "token": {"type": "string", "minlength": 10, "maxlength": 50}, 15 | } 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /examples/security/sha1-hmac.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Auth-SHA1/HMAC 5 | ~~~~~~~~~~~~~~ 6 | 7 | Securing an Eve-powered API with Basic Authentication (RFC2617). 8 | 9 | This script assumes that user accounts are stored in a MongoDB collection 10 | ('accounts'), and that passwords are stored as SHA1/HMAC hashes. All API 11 | resources/methods will be secured unless they are made explicitly public 12 | (by fiddling with some settings you can open one or more resources and/or 13 | methods to public access -see docs). 14 | 15 | Since we are using werkzeug we don't need any extra import (werkzeug being 16 | one of Flask/Eve prerequisites). 17 | 18 | Checkout Eve at https://github.com/pyeve/eve 19 | 20 | This snippet by Nicola Iarocci can be used freely for anything you like. 21 | Consider it public domain. 22 | """ 23 | 24 | from settings_security import SETTINGS 25 | from werkzeug.security import check_password_hash 26 | 27 | from eve import Eve 28 | from eve.auth import BasicAuth 29 | 30 | 31 | class Sha1Auth(BasicAuth): 32 | def check_auth(self, username, password, allowed_roles, resource, method): 33 | # use Eve's own db driver; no additional connections/resources are used 34 | accounts = app.data.driver.db["accounts"] 35 | account = accounts.find_one({"username": username}) 36 | return account and check_password_hash(account["password"], password) 37 | 38 | 39 | if __name__ == "__main__": 40 | app = Eve(auth=Sha1Auth, settings=SETTINGS) 41 | app.run() 42 | -------------------------------------------------------------------------------- /examples/security/token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Auth-Token 5 | ~~~~~~~~~~ 6 | 7 | Securing an Eve-powered API with Token based Authentication. 8 | 9 | Token based authentication can be considered a specialized version of Basic 10 | Authentication. The Authorization header tag will contain the auth token. 11 | 12 | This script assumes that user accounts are stored in a MongoDB collection 13 | ('accounts'). All API resources/methods will be secured unless they are 14 | made explicitly public (by fiddling with some settings you can open one or 15 | more resources and/or methods to public access -see docs). 16 | 17 | Checkout Eve at https://github.com/pyeve/eve 18 | 19 | This snippet by Nicola Iarocci can be used freely for anything you like. 20 | Consider it public domain. 21 | """ 22 | 23 | from settings_security import SETTINGS 24 | 25 | from eve import Eve 26 | from eve.auth import TokenAuth 27 | 28 | 29 | class TokenAuth(TokenAuth): 30 | def check_auth(self, token, allowed_roles, resource, method): 31 | """For the purpose of this example the implementation is as simple as 32 | possible. A 'real' token should probably contain a hash of the 33 | username/password combo, which should be then validated against the 34 | account data stored on the DB. 35 | """ 36 | # use Eve's own db driver; no additional connections/resources are used 37 | accounts = app.data.driver.db["accounts"] 38 | return accounts.find_one({"token": token}) 39 | 40 | 41 | if __name__ == "__main__": 42 | app = Eve(auth=TokenAuth, settings=SETTINGS) 43 | app.run() 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | safe = true 3 | quiet = true 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths=eve/tests 3 | python_files=eve/tests/*.py 4 | addopts = --maxfail=2 -rf --capture=no 5 | norecursedirs = testsuite .tox 6 | filterwarnings = 7 | ignore :: DeprecationWarning 8 | ignore :: PendingDeprecationWarning 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import io 3 | import re 4 | from collections import OrderedDict 5 | 6 | from setuptools import find_packages, setup 7 | 8 | DESCRIPTION = "Python REST API for Humans." 9 | with open("README.rst") as f: 10 | LONG_DESCRIPTION = f.read() 11 | 12 | with io.open("eve/__init__.py", "rt", encoding="utf8") as f: 13 | VERSION = re.search(r"__version__ = \"(.*?)\"", f.read()).group(1) 14 | 15 | INSTALL_REQUIRES = [ 16 | "cerberus>=1.1,<2.0", 17 | "events>=0.3,<0.4", 18 | "flask", 19 | "pymongo", 20 | "simplejson>=3.3.0,<4.0", 21 | ] 22 | 23 | EXTRAS_REQUIRE = { 24 | "docs": ["sphinx", "alabaster", "doc8"], 25 | "tests": ["redis", "testfixtures", "pytest", "tox"], 26 | } 27 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] 28 | 29 | setup( 30 | name="Eve", 31 | version=VERSION, 32 | description=DESCRIPTION, 33 | long_description=LONG_DESCRIPTION, 34 | long_description_content_type="text/x-rst", 35 | author="Nicola Iarocci", 36 | author_email="eve@nicolaiarocci.com", 37 | url="http://python-eve.org", 38 | project_urls=OrderedDict( 39 | ( 40 | ("Documentation", "http://python-eve.org"), 41 | ("Code", "https://github.com/pyeve/eve"), 42 | ("Issue tracker", "https://github.com/pyeve/eve/issues"), 43 | ) 44 | ), 45 | license="BSD", 46 | platforms=["any"], 47 | packages=find_packages(exclude=["tests*"]), 48 | test_suite="tests", 49 | install_requires=INSTALL_REQUIRES, 50 | extras_require=EXTRAS_REQUIRE, 51 | python_requires=">=3.7", 52 | classifiers=[ 53 | "Development Status :: 5 - Production/Stable", 54 | "Environment :: Web Environment", 55 | "Intended Audience :: Developers", 56 | "License :: OSI Approved :: BSD License", 57 | "Operating System :: OS Independent", 58 | "Programming Language :: Python", 59 | "Programming Language :: Python :: 3.9", 60 | "Programming Language :: Python :: 3.10", 61 | "Programming Language :: Python :: 3.11", 62 | "Programming Language :: Python :: 3.12", 63 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 64 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 65 | "Topic :: Software Development :: Libraries :: Application Frameworks", 66 | "Topic :: Software Development :: Libraries :: Python Modules", 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /tests/methods/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/methods/patch_atomic_concurrency.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import simplejson as json 4 | 5 | import eve.methods.common 6 | from eve.utils import config 7 | 8 | from tests import TestBase 9 | 10 | """ 11 | Atomic Concurrency Checks 12 | 13 | Prior to commit 54fd697 from 2016-November, ETags would be verified 14 | twice during a patch. One ETag check would be non-atomic by Eve, 15 | then again atomically by MongoDB during app.data.update(filter). 16 | The atomic ETag check was removed during issue #920 in 54fd697 17 | 18 | When running Eve in a scale-out environment (multiple processes), 19 | concurrent simultaneous updates are sometimes allowed, because 20 | the Python-only ETag check is not atomic. 21 | 22 | There is a critical section in patch_internal() between get_document() 23 | and app.data.update() where a competing Eve process can change the 24 | document and ETag. 25 | 26 | This test simulates another process changing data & ETag during 27 | the critical section. The test patches get_document() to return an 28 | intentionally wrong ETag. 29 | """ 30 | 31 | 32 | def get_document_simulate_concurrent_update(*args, **kwargs): 33 | """ 34 | Hostile version of get_document 35 | 36 | This simluates another process updating MongoDB (and ETag) in 37 | eve.methods.patch.patch_internal() during the critical area 38 | between get_document() and app.data.update() 39 | """ 40 | document = eve.methods.common.get_document(*args, **kwargs) 41 | document[config.ETAG] = "unexpected change!" 42 | return document 43 | 44 | 45 | class TestPatchAtomicConcurrent(TestBase): 46 | def setUp(self): 47 | """ 48 | Patch eve.methods.patch.get_document with a hostile version 49 | that simulates simultaneous updates 50 | """ 51 | self.original_get_document = sys.modules["eve.methods.patch"].get_document 52 | sys.modules[ 53 | "eve.methods.patch" 54 | ].get_document = get_document_simulate_concurrent_update 55 | return super().setUp() 56 | 57 | def test_etag_changed_after_get_document(self): 58 | """ 59 | Try to update a document after the ETag was adjusted 60 | outside this process 61 | """ 62 | changes = {"ref": "1234567890123456789054321"} 63 | _r, status = self.patch( 64 | self.item_id_url, data=changes, headers=[("If-Match", self.item_etag)] 65 | ) 66 | self.assertEqual(status, 412) 67 | 68 | def tearDown(self): 69 | """Remove patch of eve.methods.patch.get_document""" 70 | sys.modules["eve.methods.patch"].get_document = self.original_get_document 71 | return super().tearDown() 72 | 73 | def patch(self, url, data, headers=[]): 74 | headers.append(("Content-Type", "application/json")) 75 | r = self.test_client.patch(url, data=json.dumps(data), headers=headers) 76 | return self.parse_response(r) 77 | -------------------------------------------------------------------------------- /tests/methods/ratelimit.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from tests import TestBase 4 | 5 | 6 | class TestRateLimit(TestBase): 7 | def setUp(self): 8 | super().setUp() 9 | try: 10 | from redis import ConnectionError, Redis 11 | 12 | self.app.redis = Redis() 13 | try: 14 | self.app.redis.flushdb() 15 | except ConnectionError: 16 | self.app.redis = None 17 | except ImportError: 18 | self.app.redis = None 19 | 20 | if self.app.redis: 21 | self.app.config["RATE_LIMIT_GET"] = (1, 1) 22 | 23 | def test_ratelimit_home(self): 24 | self.get_ratelimit("/") 25 | 26 | def test_ratelimit_resource(self): 27 | self.get_ratelimit(self.known_resource_url) 28 | 29 | def test_ratelimit_item(self): 30 | self.get_ratelimit(self.item_id_url) 31 | 32 | def test_noratelimits(self): 33 | self.app.config["RATE_LIMIT_GET"] = None 34 | if self.app.redis: 35 | self.app.redis.flushdb() 36 | r = self.test_client.get("/") 37 | self.assert200(r.status_code) 38 | self.assertTrue("X-RateLimit-Remaining" not in r.headers) 39 | self.assertTrue("X-RateLimit-Limit" not in r.headers) 40 | self.assertTrue("X-RateLimit-Reset" not in r.headers) 41 | 42 | def get_ratelimit(self, url): 43 | if self.app.redis: 44 | # we want the following two GET to be executed within the same 45 | # tick (1 second) 46 | t1, t2 = 1, 2 47 | while t1 != t2: 48 | t1 = int(time.time()) 49 | r1 = self.test_client.get(url) 50 | t2 = int(time.time()) 51 | r2 = self.test_client.get(url) 52 | if t1 != t2: 53 | time.sleep(1) 54 | self.assertRateLimit(r1) 55 | self.assert429(r2.status_code) 56 | 57 | time.sleep(1) 58 | self.assertRateLimit(self.test_client.get(url)) 59 | else: 60 | print("Skipped. Needs a running redis-server and 'pip install " "redis'") 61 | 62 | def assertRateLimit(self, r): 63 | self.assertTrue("X-RateLimit-Remaining" in r.headers) 64 | self.assertEqual(r.headers["X-RateLimit-Remaining"], "0") 65 | self.assertTrue("X-RateLimit-Limit" in r.headers) 66 | self.assertEqual(r.headers["X-RateLimit-Limit"], "1") 67 | # renouncing on testing the actual Reset value: 68 | self.assertTrue("X-RateLimit-Reset" in r.headers) 69 | -------------------------------------------------------------------------------- /tests/response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | from ast import literal_eval 5 | 6 | import simplejson as json 7 | 8 | import eve 9 | 10 | from . import TestBase 11 | 12 | 13 | class TestResponse(TestBase): 14 | def setUp(self): 15 | super().setUp() 16 | self.r = self.test_client.get("/%s/" % self.empty_resource) 17 | 18 | def test_response_data(self): 19 | response = None 20 | try: 21 | response = literal_eval(self.r.get_data().decode()) 22 | except Exception: 23 | self.fail("standard response cannot be converted to a dict") 24 | self.assertTrue(isinstance(response, dict)) 25 | 26 | def test_response_object(self): 27 | response = literal_eval(self.r.get_data().decode()) 28 | self.assertTrue(isinstance(response, dict)) 29 | self.assertEqual(len(response), 3) 30 | 31 | resource = response.get("_items") 32 | self.assertTrue(isinstance(resource, list)) 33 | links = response.get("_links") 34 | self.assertTrue(isinstance(links, dict)) 35 | meta = response.get("_meta") 36 | self.assertTrue(isinstance(meta, dict)) 37 | 38 | def test_response_pretty(self): 39 | # check if pretty printing was successful by checking the length of the 40 | # response since pretty printing the respone makes it longer and not 41 | # type dict anymore 42 | self.r = self.test_client.get("/%s/?pretty" % self.empty_resource) 43 | response = self.r.get_data().decode() 44 | self.assertEqual(len(response), 300) 45 | 46 | 47 | class TestNoHateoas(TestBase): 48 | def setUp(self): 49 | super().setUp() 50 | self.app.config["HATEOAS"] = False 51 | self.domain[self.known_resource]["hateoas"] = False 52 | 53 | def test_get_no_hateoas_resource(self): 54 | r = self.test_client.get(self.known_resource_url) 55 | response = json.loads(r.get_data().decode()) 56 | self.assertTrue(isinstance(response, dict)) 57 | self.assertEqual(len(response["_items"]), 25) 58 | item = response["_items"][0] 59 | self.assertTrue(isinstance(item, dict)) 60 | self.assertTrue("_links" not in response) 61 | 62 | def test_get_no_hateoas_item(self): 63 | r = self.test_client.get(self.item_id_url) 64 | response = json.loads(r.get_data().decode()) 65 | self.assertTrue(isinstance(response, dict)) 66 | self.assertTrue("_links" not in response) 67 | 68 | def test_get_no_hateoas_homepage(self): 69 | r = self.test_client.get("/") 70 | self.assert200(r.status_code) 71 | 72 | def test_get_no_hateoas_homepage_reply(self): 73 | r = self.test_client.get("/") 74 | resp = json.loads(r.get_data().decode()) 75 | self.assertEqual(resp, {}) 76 | 77 | self.app.config["INFO"] = "_info" 78 | 79 | r = self.test_client.get("/") 80 | resp = json.loads(r.get_data().decode()) 81 | self.assertEqual(resp["_info"]["server"], "Eve") 82 | self.assertEqual(resp["_info"]["version"], eve.__version__) 83 | 84 | settings_file = os.path.join(self.this_directory, "test_version.py") 85 | self.app = eve.Eve(settings=settings_file) 86 | self.app.config["INFO"] = "_info" 87 | 88 | r = self.app.test_client().get("/v1") 89 | resp = json.loads(r.get_data().decode()) 90 | self.assertEqual(resp["_info"]["api_version"], self.app.config["API_VERSION"]) 91 | self.assertEqual(resp["_info"]["server"], "Eve") 92 | self.assertEqual(resp["_info"]["version"], eve.__version__) 93 | 94 | def test_post_no_hateoas(self): 95 | data = {"item1": json.dumps({"ref": "1234567890123456789054321"})} 96 | headers = [("Content-Type", "application/x-www-form-urlencoded")] 97 | r = self.test_client.post(self.known_resource_url, data=data, headers=headers) 98 | response = json.loads(r.get_data().decode()) 99 | self.assertTrue("_links" not in response) 100 | 101 | def test_patch_no_hateoas(self): 102 | data = {"item1": json.dumps({"ref": "0000000000000000000000000"})} 103 | headers = [ 104 | ("Content-Type", "application/x-www-form-urlencoded"), 105 | ("If-Match", self.item_etag), 106 | ] 107 | r = self.test_client.patch(self.item_id_url, data=data, headers=headers) 108 | response = json.loads(r.get_data().decode()) 109 | self.assertTrue("_links" not in response) 110 | -------------------------------------------------------------------------------- /tests/suite_generator.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class EmbeddedDoc: 5 | def __init__(self, _id): 6 | self._id = _id 7 | self._created = datetime.utcnow() 8 | -------------------------------------------------------------------------------- /tests/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyeve/eve/c6d61b9f634c2d86f66987984b0025e6982a9400/tests/test.db -------------------------------------------------------------------------------- /tests/test_io/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/test_io/flask_pymongo.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pymongo import MongoClient 3 | from pymongo.errors import OperationFailure 4 | 5 | from eve.io.mongo.flask_pymongo import PyMongo 6 | from tests import TestBase 7 | from tests.test_settings import ( 8 | MONGO1_DBNAME, 9 | MONGO1_PASSWORD, 10 | MONGO1_USERNAME, 11 | MONGO_HOST, 12 | MONGO_PORT, 13 | ) 14 | 15 | 16 | class TestPyMongo(TestBase): 17 | def setUp(self, url_converters=None): 18 | super().setUp(url_converters) 19 | self._setupdb() 20 | schema = {"title": {"type": "string"}} 21 | settings = {"schema": schema, "mongo_prefix": "MONGO1"} 22 | 23 | self.app.register_resource("works", settings) 24 | 25 | def test_auth_params_provided_in_mongo_url(self): 26 | self.app.config["MONGO1_URL"] = "mongodb://%s:%s@%s:%s" % ( 27 | MONGO1_USERNAME, 28 | MONGO1_PASSWORD, 29 | MONGO_HOST, 30 | MONGO_PORT, 31 | ) 32 | with self.app.app_context(): 33 | db = PyMongo(self.app, "MONGO1").db 34 | self.assertEqual(0, db.works.count_documents({})) 35 | 36 | def test_auth_params_provided_in_config(self): 37 | self.app.config["MONGO1_USERNAME"] = MONGO1_USERNAME 38 | self.app.config["MONGO1_PASSWORD"] = MONGO1_PASSWORD 39 | with self.app.app_context(): 40 | db = PyMongo(self.app, "MONGO1").db 41 | self.assertEqual(0, db.works.count_documents({})) 42 | 43 | def test_invalid_auth_params_provided(self): 44 | # if bad username and/or password is provided in MONGO_URL and mongo 45 | # run w\o --auth pymongo won't raise exception 46 | def func(): 47 | with self.app.app_context(): 48 | db = PyMongo(self.app, "MONGO1").db 49 | db.works.find_one() 50 | 51 | self.app.config["MONGO1_USERNAME"] = "bad_username" 52 | self.app.config["MONGO1_PASSWORD"] = "bad_password" 53 | self.assertRaises(OperationFailure, func) 54 | 55 | def test_invalid_port(self): 56 | self.app.config["MONGO1_PORT"] = "bad_value" 57 | self.assertRaises(TypeError, self._pymongo_instance) 58 | 59 | def test_invalid_options(self): 60 | self.app.config["MONGO1_OPTIONS"] = {"connectTimeoutMS": "bad_value"} 61 | self.assertRaises(ValueError, self._pymongo_instance) 62 | 63 | def test_valid_port(self): 64 | self.app.config["MONGO1_PORT"] = 27017 65 | with self.app.app_context(): 66 | db = PyMongo(self.app, "MONGO1").db 67 | self.assertEqual(0, db.works.count_documents({})) 68 | 69 | def _setupdb(self): 70 | self.connection = MongoClient() 71 | self.connection.drop_database(MONGO1_DBNAME) 72 | db = self.connection[MONGO1_DBNAME] 73 | try: 74 | db.command("dropUser", MONGO1_USERNAME) 75 | except OperationFailure: 76 | pass 77 | db.command( 78 | "createUser", MONGO1_USERNAME, pwd=MONGO1_PASSWORD, roles=["dbAdmin"] 79 | ) 80 | 81 | def _pymongo_instance(self): 82 | with self.app.app_context(): 83 | PyMongo(self.app, "MONGO1") 84 | -------------------------------------------------------------------------------- /tests/test_io/multi_mongo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | import pytest 5 | import simplejson as json 6 | from bson import ObjectId 7 | from pymongo import MongoClient 8 | from pymongo.errors import OperationFailure 9 | 10 | import eve 11 | from eve.auth import BasicAuth 12 | from tests import TestBase 13 | from tests.test_settings import ( 14 | MONGO1_DBNAME, 15 | MONGO1_PASSWORD, 16 | MONGO1_USERNAME, 17 | MONGO_DBNAME, 18 | MONGO_HOST, 19 | MONGO_PORT, 20 | ) 21 | 22 | 23 | class TestMultiMongo(TestBase): 24 | def setUp(self): 25 | super().setUp() 26 | 27 | self.setupDB2() 28 | 29 | schema = {"author": {"type": "string"}, "title": {"type": "string"}} 30 | settings = {"schema": schema, "mongo_prefix": "MONGO1"} 31 | 32 | self.app.register_resource("works", settings) 33 | 34 | def tearDown(self): 35 | super().tearDown() 36 | self.dropDB2() 37 | 38 | def setupDB2(self): 39 | self.connection = MongoClient() 40 | self.connection.drop_database(MONGO1_DBNAME) 41 | db = self.connection[MONGO1_DBNAME] 42 | try: 43 | db.command("dropUser", MONGO1_USERNAME) 44 | except OperationFailure: 45 | pass 46 | db.command( 47 | "createUser", MONGO1_USERNAME, pwd=MONGO1_PASSWORD, roles=["dbAdmin"] 48 | ) 49 | self.bulk_insert2() 50 | 51 | def dropDB2(self): 52 | self.connection = MongoClient() 53 | self.connection.drop_database(MONGO1_DBNAME) 54 | self.connection.close() 55 | 56 | def bulk_insert2(self): 57 | _db = self.connection[MONGO1_DBNAME] 58 | works = self.random_works(self.known_resource_count) 59 | _db.works.insert_many(works) 60 | self.work = _db.works.find_one() 61 | 62 | def random_works(self, num): 63 | works = [] 64 | for i in range(num): 65 | dt = datetime.now() 66 | work = { 67 | "author": self.random_string(20), 68 | "title": self.random_string(30), 69 | eve.LAST_UPDATED: dt, 70 | eve.DATE_CREATED: dt, 71 | } 72 | works.append(work) 73 | return works 74 | 75 | 76 | class TestMethodsAcrossMultiMongo(TestMultiMongo): 77 | def test_get_multidb(self): 78 | # test that a GET on 'works' reads from MONGO1 79 | id_field = self.domain["works"]["id_field"] 80 | r, s = self.get("works/%s" % self.work[id_field]) 81 | self.assert200(s) 82 | self.assertEqual(r["author"], self.work["author"]) 83 | 84 | # while 'contacts' endpoint reads from MONGO 85 | id_field = self.domain["contacts"]["id_field"] 86 | r, s = self.get(self.known_resource, item=self.item_id) 87 | self.assert200(s) 88 | self.assertEqual(r[id_field], self.item_id) 89 | 90 | def test_post_multidb(self): 91 | # test that a POST on 'works' stores data to MONGO1 92 | work = self._save_work() 93 | db = self.connection[MONGO1_DBNAME] 94 | id_field = self.domain["works"]["id_field"] 95 | new = db.works.find_one({id_field: ObjectId(work[id_field])}) 96 | self.assertTrue(new is not None) 97 | 98 | # while 'contacts' endpoint stores data to MONGO 99 | contact = {"ref": "1234567890123456789054321"} 100 | r, s = self.post(self.known_resource_url, data=contact) 101 | self.assert201(s) 102 | db = self.connection[MONGO_DBNAME] 103 | id_field = self.domain["contacts"]["id_field"] 104 | new = db.contacts.find_one({id_field: ObjectId(r[id_field])}) 105 | self.assertTrue(new is not None) 106 | 107 | def test_patch_multidb(self): 108 | # test that a PATCH on 'works' udpates data on MONGO1 109 | work = self._save_work() 110 | id_field = self.domain["works"]["id_field"] 111 | id, etag = work[id_field], work[eve.ETAG] 112 | changes = {"author": "mike"} 113 | 114 | headers = [("Content-Type", "application/json"), ("If-Match", etag)] 115 | r = self.test_client.patch( 116 | "works/%s" % id, data=json.dumps(changes), headers=headers 117 | ) 118 | self.assert200(r.status_code) 119 | 120 | db = self.connection[MONGO1_DBNAME] 121 | updated = db.works.find_one({id_field: ObjectId(id)}) 122 | self.assertEqual(updated["author"], "mike") 123 | 124 | # while 'contacts' endpoint updates data on MONGO 125 | field, value = "ref", "1234567890123456789012345" 126 | changes = {field: value} 127 | headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] 128 | id_field = self.domain["contacts"]["id_field"] 129 | r = self.test_client.patch( 130 | self.item_id_url, data=json.dumps(changes), headers=headers 131 | ) 132 | self.assert200(r.status_code) 133 | 134 | db = self.connection[MONGO_DBNAME] 135 | updated = db.contacts.find_one({id_field: ObjectId(self.item_id)}) 136 | self.assertEqual(updated[field], value) 137 | 138 | def test_put_multidb(self): 139 | # test that a PUT on 'works' udpates data on MONGO1 140 | work = self._save_work() 141 | id_field = self.domain["works"]["id_field"] 142 | id, etag = work[id_field], work[eve.ETAG] 143 | changes = {"author": "mike", "title": "Eve for dummies"} 144 | 145 | headers = [("Content-Type", "application/json"), ("If-Match", etag)] 146 | r = self.test_client.put( 147 | "works/%s" % id, data=json.dumps(changes), headers=headers 148 | ) 149 | self.assert200(r.status_code) 150 | 151 | db = self.connection[MONGO1_DBNAME] 152 | updated = db.works.find_one({id_field: ObjectId(id)}) 153 | self.assertEqual(updated["author"], "mike") 154 | 155 | # while 'contacts' endpoint updates data on MONGO 156 | field, value = "ref", "1234567890123456789012345" 157 | changes = {field: value} 158 | headers = [("Content-Type", "application/json"), ("If-Match", self.item_etag)] 159 | id_field = self.domain["contacts"]["id_field"] 160 | r = self.test_client.put( 161 | self.item_id_url, data=json.dumps(changes), headers=headers 162 | ) 163 | self.assert200(r.status_code) 164 | 165 | db = self.connection[MONGO_DBNAME] 166 | updated = db.contacts.find_one({id_field: ObjectId(self.item_id)}) 167 | self.assertEqual(updated[field], value) 168 | 169 | def test_delete_multidb(self): 170 | # test that DELETE on 'works' deletes data on MONGO1 171 | work = self._save_work() 172 | id_field = self.domain["works"]["id_field"] 173 | id, etag = work[id_field], work[eve.ETAG] 174 | r = self.test_client.delete("works/%s" % id, headers=[("If-Match", etag)]) 175 | self.assert204(r.status_code) 176 | db = self.connection[MONGO1_DBNAME] 177 | lost = db.works.find_one({id_field: ObjectId(id)}) 178 | self.assertEqual(lost, None) 179 | 180 | # while 'contacts' still deletes on MONGO 181 | r = self.test_client.delete( 182 | self.item_id_url, headers=[("If-Match", self.item_etag)] 183 | ) 184 | self.assert204(r.status_code) 185 | db = self.connection[MONGO_DBNAME] 186 | id_field = self.domain["contacts"]["id_field"] 187 | lost = db.contacts.find_one({id_field: ObjectId(self.item_id)}) 188 | self.assertEqual(lost, None) 189 | 190 | def test_create_index_with_mongo_uri_and_prefix(self): 191 | self.app.config["MONGO_URI"] = "mongodb://%s:%s/%s" % ( 192 | MONGO_HOST, 193 | MONGO_PORT, 194 | MONGO_DBNAME, 195 | ) 196 | self.app.config["MONGO1_URI"] = "mongodb://%s:%s/%s" % ( 197 | MONGO_HOST, 198 | MONGO_PORT, 199 | MONGO1_DBNAME, 200 | ) 201 | settings = { 202 | "schema": { 203 | "name": {"type": "string"}, 204 | "other_field": {"type": "string"}, 205 | "lat_long": {"type": "list"}, 206 | }, 207 | "mongo_indexes": { 208 | "name": [("name", 1)], 209 | "composed": [("name", 1), ("other_field", 1)], 210 | "arguments": ([("lat_long", "2d")], {"sparse": True}), 211 | }, 212 | "mongo_prefix": "MONGO1", 213 | } 214 | self.app.register_resource("mongodb_features", settings) 215 | 216 | # check if index was created using MONGO1 prefix 217 | db = self.connection[MONGO1_DBNAME] 218 | self.assertTrue("mongodb_features" in db.list_collection_names()) 219 | coll = db["mongodb_features"] 220 | indexes = coll.index_information() 221 | 222 | # at least there is an index for the _id field plus the indexes 223 | self.assertTrue(len(indexes) > len(settings["mongo_indexes"])) 224 | 225 | def _save_work(self): 226 | work = {"author": "john doe", "title": "Eve for Dummies"} 227 | r, s = self.post("works", data=work) 228 | self.assert201(s) 229 | return r 230 | 231 | 232 | class MyBasicAuth(BasicAuth): 233 | def check_auth(self, username, password, allowed_roles, resource, method): 234 | self.set_mongo_prefix("MONGO1") 235 | return True 236 | 237 | 238 | class TestMultiMongoAuth(TestMultiMongo): 239 | def test_get_multidb(self): 240 | self.domain["works"]["mongo_prefix"] = "MONGO" 241 | self.domain["works"]["public_item_methods"] = [] 242 | 243 | headers = [("Authorization", "Basic YWRtaW46c2VjcmV0")] 244 | 245 | # this will 404 since there's no 'works' collection on MONGO, 246 | id_field = self.domain["works"]["id_field"] 247 | r = self.test_client.get("works/%s" % self.work[id_field], headers=headers) 248 | self.assert404(r.status_code) 249 | 250 | # now set a custom auth class which sets mongo_prefix at MONGO1 251 | self.domain["works"]["authentication"] = MyBasicAuth 252 | 253 | # this will 200 just fine as the custom auth class has precedence over 254 | # endpoint configuration. 255 | r = self.test_client.get("works/%s" % self.work[id_field], headers=headers) 256 | self.assert200(r.status_code) 257 | # test that we are indeed reading from the correct database instance. 258 | payl = json.loads(r.get_data().decode("utf-8")) 259 | self.assertEqual(payl["author"], self.work["author"]) 260 | 261 | # 'contacts' still reads from MONGO 262 | r = self.test_client.get( 263 | "%s/%s" % (self.known_resource_url, self.item_id), headers=headers 264 | ) 265 | self.assert200(r.status_code) 266 | -------------------------------------------------------------------------------- /tests/test_logging.py: -------------------------------------------------------------------------------- 1 | from testfixtures import log_capture 2 | 3 | from . import TestBase 4 | 5 | 6 | class TestUtils(TestBase): 7 | """collection, document and home_link methods (and resource_uri, which is 8 | used by all of them) are tested in 'tests.methods' since we need an active 9 | flaskapp context 10 | """ 11 | 12 | @log_capture() 13 | def test_logging_info(self, log): 14 | self.app.logger.propagate = True 15 | self.app.logger.info("test info") 16 | log.check(("eve", "INFO", "test info")) 17 | 18 | log_record = log.records[0] 19 | self.assertEqual(log_record.clientip, None) 20 | self.assertEqual(log_record.method, None) 21 | self.assertEqual(log_record.url, None) 22 | -------------------------------------------------------------------------------- /tests/test_prefix.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | RESOURCE_METHODS = ["GET", "POST"] 4 | URL_PREFIX = "prefix" 5 | DOMAIN = {"contacts": {}} 6 | -------------------------------------------------------------------------------- /tests/test_prefix_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | URL_PREFIX = "prefix" 4 | API_VERSION = "v1" 5 | DOMAIN = {"contacts": {}} 6 | -------------------------------------------------------------------------------- /tests/test_settings_env.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # this is just a helper file which we are going 4 | # to try to load with environmental variable in 5 | # test_existing_env_config() test case 6 | 7 | DOMAIN = {"env_domain": {}} 8 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | API_VERSION = "v1" 4 | DOMAIN = {"contacts": {}} 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py3{12,11,10,9},pypy3{10,9},linting 3 | 4 | [testenv] 5 | extras=tests 6 | commands=pytest tests {posargs} 7 | 8 | [testenv:linting] 9 | skipsdist = True 10 | usedevelop = True 11 | basepython = python3.9 12 | deps = pre-commit 13 | commands = pre-commit run --all-files 14 | 15 | [flake8] 16 | max-line-length = 88 17 | ignore = E401,E722,W503,F821,E501,E203 18 | --------------------------------------------------------------------------------