Repository: ramses-tech/ramses Branch: master Commit: ea2e1e896325 Files: 54 Total size: 230.7 KB Directory structure: gitextract_t7nlq2go/ ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── docs/ │ ├── Makefile │ └── source/ │ ├── changelog.rst │ ├── conf.py │ ├── event_handlers.rst │ ├── field_processors.rst │ ├── fields.rst │ ├── getting_started.rst │ ├── index.rst │ ├── raml.rst │ ├── relationships.rst │ └── schemas.rst ├── ramses/ │ ├── __init__.py │ ├── acl.py │ ├── auth.py │ ├── generators.py │ ├── models.py │ ├── registry.py │ ├── scaffolds/ │ │ ├── __init__.py │ │ └── ramses_starter/ │ │ ├── +package+/ │ │ │ ├── __init__.py │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ ├── api.raml │ │ │ ├── items.json │ │ │ ├── requirements.txt │ │ │ └── test_api.py_tmpl │ │ ├── .gitignore_tmpl │ │ ├── README.md │ │ ├── api.raml_tmpl │ │ ├── items.json │ │ ├── local.ini_tmpl │ │ ├── requirements.txt │ │ └── setup.py_tmpl │ ├── scripts/ │ │ ├── __init__.py │ │ └── scaffold_test.py │ ├── utils.py │ └── views.py ├── requirements.dev ├── setup.py ├── tests/ │ ├── __init__.py │ ├── fixtures.py │ ├── test_acl.py │ ├── test_auth.py │ ├── test_generators.py │ ├── test_models.py │ ├── test_registry.py │ ├── test_utils.py │ └── test_views.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ venv .DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ ================================================ FILE: .travis.yml ================================================ # Config file for automatic testing at travis-ci.org language: python env: - TOXENV=py27 - TOXENV=py33 - TOXENV=py34 - TOXENV=py35 python: 3.5 install: - pip install tox script: tox services: - elasticsearch - mongodb before_script: - travis_retry curl -XDELETE 'http://localhost:9200/ramses_starter/' - mongo ramses_starter --eval 'db.dropDatabase();' ================================================ FILE: CONTRIBUTING.md ================================================ ## Team members In alphabetical order: * [Artem Kostiuk](https://github.com/postatum) * [Chris Hart](https://github.com/chrstphrhrt) * [Jonathan Stoikovitch](https://github.com/jstoiko) ## Pull-requests Pull-requests are welcomed! ## Testing 1. Install dev requirements by running `pip install -r requirements.dev` 2. Run tests using `py.test --cov ramses tests` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MANIFEST.in ================================================ include README.md include VERSION recursive-include ramses/scaffolds * ================================================ FILE: README.md ================================================ # `Ramses` [![Build Status](https://travis-ci.org/ramses-tech/ramses.svg?branch=master)](https://travis-ci.org/ramses-tech/ramses) [![Documentation](https://readthedocs.org/projects/ramses/badge/?version=stable)](http://ramses.readthedocs.org) [![Join the chat at https://gitter.im/ramses-tech/ramses](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/ramses-tech/ramses) Ramses is a framework that generates a RESTful API using [RAML](http://raml.org). It uses Pyramid and [Nefertari](https://github.com/ramses-tech/nefertari) which provides Elasticsearch / Posgres / MongoDB / Your Data Store™ -powered views. Looking to get started quickly? You can take a look at the ["Getting Started" guide](https://ramses.readthedocs.org/en/stable/getting_started.html). ================================================ FILE: VERSION ================================================ 0.5.3 ================================================ FILE: docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Nefertari.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Nefertari.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Nefertari" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Nefertari" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: docs/source/changelog.rst ================================================ Changelog ========= * :release:`0.5.3 <2016-05-17>` * :bug:`107` Fixed issue with hyphens in resource paths * :release:`0.5.2 <2016-05-17>` * :support:`99 backported` Use ACL mixin from nefertari-guards (if enabled) * :support:`- backported` Scaffold defaults to Pyramid 1.6.1 * :release:`0.5.1 <2015-11-18>` * :bug:`88` Reworked the creation of related/auth_model models, order does not matter anymore * :release:`0.5.0 <2015-10-07>` * :bug:`- major` Fixed a bug using 'required' '_db_settings' property on 'relationship' field * :support:`-` Added support for `'nefertari-guards' `_ * :support:`-` Added support for Nefertari '_hidden_fields' * :support:`-` Added support for Nefertari event handlers * :support:`-` Simplified field processors, '_before_processors' is now called '_processors', removed '_after_processors' * :support:`-` ACL permission names in RAML now match real permission names instead of http methods * :support:`-` Added support for the property '_nesting_depth' in schemas * :release:`0.4.1 <2015-09-02>` * :bug:`-` Simplified ACLs (refactoring) * :release:`0.4.0 <2015-08-19>` * :support:`-` Added support for JSON schema draft 04 * :support:`-` RAML is now parsed using ramlfications instead of pyraml-parser * :feature:`-` Boolean values in RAML don't have to be strings anymore (previous limitation of pyraml-parser) * :feature:`-` Renamed setting 'ramses.auth' to 'auth' * :feature:`-` Renamed setting 'debug' to 'enable_get_tunneling' * :feature:`-` Field name and request object are now passed to field processors under 'field' and 'request' kwargs respectively * :feature:`-` Added support for relationship processors and backref relationship processors ('backref_after_validation'/'backref_before_validation') * :feature:`-` Renamed schema's 'args' property to '_db_settings' * :feature:`-` Properties 'type' and 'required' are now under '_db_settings' * :feature:`-` Prefixed all Ramses schema properties by an underscore: '_auth_fields', '_public_fields', '_nested_relationships', '_auth_model', '_db_settings' * :feature:`-` Error response bodies are now returned as JSON * :bug:`- major` Fixed processors not applied on fields of type 'list' and type 'dict' * :bug:`- major` Fixed a limitation preventing collection names to use nouns that do not have plural forms * :release:`0.3.1 <2015-07-07>` * :support:`- backported` Added support for callables in 'default' field argument * :support:`- backported` Added support for 'onupdate' field argument * :release:`0.3.0 <2015-06-14>` * :support:`-` Added python3 support * :release:`0.2.3 <2015-06-05>` * :bug:`-` Forward compatibility with nefertari releases * :release:`0.2.2 <2015-06-03>` * :bug:`-` Fixed password minimum length support by adding before and after validation processors * :bug:`-` Fixed race condition in Elasticsearch indexing * :release:`0.2.1 <2015-05-27>` * :bug:`-` Fixed limiting fields to be searched * :bug:`-` Fixed login issue * :bug:`-` Fixed custom processors * :release:`0.2.0 <2015-05-18>` * :feature:`-` Added support for securitySchemes, authentication (Pyramid 'auth ticket') and ACLs * :support:`-` Added several display options to schemas * :support:`-` Added unit tests * :support:`-` Improved docs * :feature:`-` Add support for processors in schema definition * :feature:`-` Add support for custom auth model * :support:`-` ES views now read from ES on update/delete_many * :release:`0.1.1 <2015-04-21>` * :bug:`-` Ramses could not be used in an existing Pyramid project * :release:`0.1.0 <2015-04-08>` * :support:`-` Initial release! ================================================ FILE: docs/source/conf.py ================================================ # -*- coding: utf-8 -*- # # Nefertari documentation build configuration file, created by # sphinx-quickstart on Fri Mar 27 11:16:31 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', # 'sphinxcontrib.fulltoc', 'releases' ] releases_github_path = 'ramses-tech/ramses' releases_debug = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Ramses' copyright = u'Ramses Tech' author = u'Ramses Tech, Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. # version = '0.1' # The full version, including alpha/beta/rc tags. # release = '0.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'Ramsesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'Ramses.tex', u'Ramses Documentation', author, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'ramses', u'Ramses Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Ramses', u'Ramses Documentation', author, 'Ramses', 'API generator for Pyramid using RAML', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False ================================================ FILE: docs/source/event_handlers.rst ================================================ Event Handlers ============== Ramses supports `Nefertari event handlers `_. Ramses event handlers also have access to `Nefertari's wrapper API `_ which provides additional helpers. Setup ----- Writing Event Handlers ^^^^^^^^^^^^^^^^^^^^^^ You can write custom functions inside your ``__init__.py`` file, then add the ``@registry.add`` decorator before the functions that you'd like to turn into CRUD event handlers. Ramses CRUD event handlers has the same API as Nefertari CRUD event handlers. Check Nefertari CRUD Events doc for more details on events API. Example: .. code-block:: python import logging from ramses import registry log = logging.getLogger('foo') @registry.add def log_changed_fields(event): changed = ['{}: {}'.format(name, field.new_value) for name, field in event.fields.items()] logger.debug('Changed fields: ' + ', '.join(changed)) Connecting Event Handlers ^^^^^^^^^^^^^^^^^^^^^^^^^ When you define event handlers in your ``__init__.py`` as described above, you can apply them on per-model basis. If multiple handlers are listed, they are executed in the order in which they are listed. Handlers should be defined in the root of JSON schema using ``_event_handlers`` property. This property is an object, keys of which are called "event tags" and values are lists of handler names. Event tags are composed of two parts: ``_`` whereby: **type** Is either ``before`` or ``after``, depending on when handler should run - before view method call or after respectively. You can read more about when to use `before vs after event handlers `_. **action** Exact name of Nefertari view method that processes the request (action) and special names for authentication actions. Complete list of actions: * **index** - Collection GET * **create** - Collection POST * **update_many** - Collection PATCH/PUT * **delete_many** - Collection DELETE * **collection_options** - Collection OPTIONS * **show** - Item GET * **update** - Item PATCH * **replace** - Item PUT * **delete** - Item DELETE * **item_options** - Item OPTIONS * **login** - User login (POST /auth/login) * **logout** - User logout (POST /auth/logout) * **register** - User register (POST /auth/register) * **set** - triggers on all the following actions: **create**, **update**, **replace**, **update_many** and **register**. Example ------- We will use the following handler to demonstrate how to connect handlers to events. This handler logs ``request`` to the console. .. code-block:: python import logging from ramses import registry log = logging.getLogger('foo') @registry.add def log_request(event): log.debug(event.view.request) Assuming we had a JSON schema representing the model ``User`` and we want to log all collection GET requests on the ``User`` model after they are processed using the ``log_request`` handler, we would register the handler in the JSON schema like this: .. code-block:: json { "type": "object", "title": "User schema", "$schema": "http://json-schema.org/draft-04/schema", "_event_handlers": { "after_index": ["log_request"] }, ... } Other Things You Can Do ----------------------- You can update another field's value, for example, increment a counter: .. code-block:: python from ramses import registry @registry.add def increment_count(event): instance = event.instance or event.response counter = instance.counter incremented = counter + 1 event.set_field_value('counter', incremented) You can update other collections (or filtered collections), for example, mark sub-tasks as completed whenever a task is completed: .. code-block:: python from ramses import registry from nefertari import engine @registry.add def mark_subtasks_completed(event): if 'task' not in event.fields: return completed = event.fields['task'].new_value instance = event.instance or event.response if completed: subtask_model = engine.get_document_cls('Subtask') subtasks = subtask_model.get_collection(task_id=instance.id) subtask_model._update_many(subtasks, {'completed': True}) You can perform more complex queries using Elasticsearch: .. code-block:: python from ramses import registry from nefertari import engine from nefertari.elasticsearch import ES @registry.add def mark_subtasks_after_2015_completed(event): if 'task' not in event.fields: return completed = event.fields['task'].new_value instance = event.instance or event.response if completed: subtask_model = engine.get_document_cls('Subtask') es_query = 'task_id:{} AND created_at:[2015 TO *]'.format(instance.id) subtasks_es = ES(subtask_model.__name__).get_collection(_raw_terms=es_query) subtasks_db = subtask_model.filter_objects(subtasks_es) subtask_model._update_many(subtasks_db, {'completed': True}) ================================================ FILE: docs/source/field_processors.rst ================================================ Field processors ================ Ramses supports `Nefertari field processors `_. Ramses field processors also have access to `Nefertari's wrapper API `_ which provides additional helpers. Setup ----- To setup a field processor, you can define the ``_processors`` property in your field definition (same level as ``_db_settings``). It should be an array of processor names to apply. You can also use the ``_backref_processors`` property to specify processors for backref field. For backref processors to work, ``_db_settings`` must contain the following properties: ``document``, ``type=relationship`` and ``backref_name``. .. code-block:: json "username": { ... "_processors": ["lowercase"] }, ... You can read more about processors in Nefertari's `field processors documentation `_ including the `list of keyword arguments `_ passed to processors. Example ------- If we had following processors defined: .. code-block:: python from .my_helpers import get_stories_by_ids @registry.add def lowercase(**kwargs): """ Make :new_value: lowercase """ return (kwargs['new_value'] or '').lower() @registry.add def validate_stories_exist(**kwargs): """ Make sure added stories exist. """ story_ids = kwargs['new_value'] if story_ids: # Get stories by ids stories = get_stories_by_ids(story_ids) if not stories or len(stories) < len(story_ids): raise Exception("Some of provided stories do not exist") return story_ids .. code-block:: json # User model json { "type": "object", "title": "User schema", "$schema": "http://json-schema.org/draft-04/schema", "properties": { "stories": { "_db_settings": { "type": "relationship", "document": "Story", "backref_name": "owner" }, "_processors": ["validate_stories_exist"], "_backref_processors": ["lowercase"] }, ... } } Notes: * ``validate_stories_exist`` processor will be run when request changes ``User.stories`` value. The processor will make sure all of story IDs from request exist. * ``lowercase`` processor will be run when request changes ``Story.owner`` field. The processor will lowercase new value of the ``Story.owner`` field. ================================================ FILE: docs/source/fields.rst ================================================ Fields ====== Types ----- You can set a field's type by setting the ``type`` property under ``_db_settings``. .. code-block:: json "created_at": { (...) "_db_settings": { "type": "datetime" } } This is a list of all available types: * biginteger * binary * boolean * choice * date * datetime * decimal * dict * float * foreign_key * id_field * integer * interval * list * pickle * relationship * smallinteger * string * text * time * unicode * unicodetext Required Fields --------------- You can set a field as required by setting the ``required`` property under ``_db_settings``. .. code-block:: json "password": { (...) "_db_settings": { (...) "required": true } } Primary Key ----------- You can use an ``id_field`` in lieu of primary key. .. code-block:: json "id": { (...) "_db_settings": { (...) "primary_key": true } } You can alternatively elect a field to be the primary key of your model by setting its ``primary_key`` property under ``_db_settings``. For example, if you decide to use ``username`` as the primary key of your `User` model. This will enable resources to refer to that field in their url, e.g. ``/api/users/john`` .. code-block:: json "username": { (...) "_db_settings": { (...) "primary_key": true } } Constraints ----------- You can set a minimum and/or maximum length of your field by setting the ``min_length`` / ``max_length`` properties under ``_db_settings``. You can also add a unique constraint on a field by setting the ``unique`` property. .. code-block:: json "field": { (...) "_db_settings": { (...) "unique": true, "min_length": 5, "max_length": 50 } } Default Value ------------- You can set a default value for you field by setting the ``default`` property under ``_db_settings``. .. code-block:: json "field": { (...) "_db_settings": { (...) "default": "default value" } }, The ``default`` value can also be set to a Python callable, e.g. .. code-block:: json "datetime_field": { (...) "_db_settings": { (...) "default": "{{datetime.datetime.utcnow}}" } }, Update Default Value -------------------- You can set an update default value for your field by setting the ``onupdate`` property under ``_db_settings``. This is particularly useful to update 'datetime' fields on every updates, e.g. .. code-block:: json "datetime_field": { (...) "_db_settings": { (...) "onupdate": "{{datetime.datetime.utcnow}}" } }, List Fields ----------- You can list the accepted values of any ``list`` or ``choice`` fields by setting the ``choices`` property under ``_db_settings``. .. code-block:: json "field": { (...) "_db_settings": { "type": "choice", "choices": ["choice1", "choice2", "choice3"], "default": "choice1" } } You can also provide the list/choice items' ``item_type``. .. code-block:: json "field": { (...) "_db_settings": { "type": "list", "item_type": "string" } } Other ``_db_settings`` ---------------------- Note that you can pass any engine-specific arguments to your fields by defining such arguments in ``_db_settings``. ================================================ FILE: docs/source/getting_started.rst ================================================ Getting started =============== 1. Create your project in a virtualenv directory (see the `virtualenv documentation `_) .. code-block:: shell $ virtualenv my_project $ source my_project/bin/activate $ pip install ramses $ pcreate -s ramses_starter my_project $ cd my_project $ pserve local.ini 2. Tada! Start editing api.raml to modify the API and items.json for the schema. Requirements ------------ * Python 2.7, 3.3 or 3.4 * Elasticsearch (data is automatically indexed for near real-time search) * Postgres or Mongodb or Your Data Store™ Examples -------- - For a more complete example of a Pyramid project using Ramses, you can take a look at the `Example Project `_. - RAML can be used to generate an end-to-end application, check out `this example `_ using Ramses on the backend and RAML-javascript-client + BackboneJS on the front-end. Tutorials --------- - `Create a REST API in Minutes With Pyramid and Ramses `_ - `Make an Elasticsearch-powered REST API for any data with Ramses `_ ================================================ FILE: docs/source/index.rst ================================================ Ramses ====== Ramses is a framework that generates a RESTful API using `RAML `_. It uses Pyramid and `Nefertari `_ which provides Elasticsearch / Posgres / MongoDB / Your Data Store™ -powered views. Using Elasticsearch enables `Elasticsearch-powered requests `_ which provides near real-time search. Website: ``_ Source code: ``_ Table of Contents ================= .. toctree:: :maxdepth: 2 getting_started raml schemas fields event_handlers field_processors relationships changelog .. image:: ramses.jpg Image credit: Wikipedia ================================================ FILE: docs/source/raml.rst ================================================ RAML Configuration ================== You can read the full RAML specs `here `_. Authentication -------------- In order to enable authentication, add the ``auth`` parameter to your .ini file: .. code-block:: ini auth = true In the root section of your RAML file, you can add a ``securitySchemes``, define the ``x_ticket_auth`` method and list it in your root-level ``securedBy``. This will enable cookie-based authentication. .. code-block:: yaml securitySchemes: - x_ticket_auth: description: Standard Pyramid Auth Ticket policy type: x-Ticket settings: secret: auth_tkt_secret hashalg: sha512 cookie_name: ramses_auth_tkt http_only: 'true' securedBy: [x_ticket_auth] A few convenience routes will be automatically added: * POST ``/auth/register``: register a new user * POST ``/auth/login``: login an existing user * GET ``/auth/logout``: logout currently logged-in user * GET ``/users/self``: returns currently logged-in user ACLs ---- In your ``securitySchemes``, you can add as many ACLs as you need. Then you can reference these ACLs in your resource's ``securedBy``. .. code-block:: yaml securitySchemes: (...) - read_only_users: description: ACL that allows authenticated users to read type: x-ACL settings: collection: | allow admin all allow authenticated view item: | allow admin all allow authenticated view (...) /items: securedBy: [read_only_users] Enabling HTTP Methods --------------------- Listing an HTTP method in your resource definition is all it takes to enable such method. .. code-block:: yaml /items: (...) post: description: Create an item get: description: Get multiple items patch: description: Update multiple items delete: description: delete multiple items /{id}: displayName: One item get: description: Get a particular item delete: description: Delete a particular item patch: description: Update a particular item You can link your schema definition for each resource by adding it to the ``post`` section. .. code-block:: yaml /items: (...) post: (...) body: application/json: schema: !include schemas/items.json ================================================ FILE: docs/source/relationships.rst ================================================ Relationships ============= Basics ------ Relationships in Ramses are used to represent One-To-Many(o2m) and One-To-One(o2o) relationships between objects in database. To set up relationships fields of types ``foreign_key`` and ``relationship`` are used. ``foreign_key`` field is not required when using ``nefertari_mongodb`` engine and is ignored. For this tutorial we are going to use the example of users and stories. In this example we have a OneToMany relationship betweed ``User`` and ``Story``. One user may have many stories but each story has only one owner. Check the end of the tutorial for the complete example RAML file and schemas. Example code is the very minimum needed to explain the subject. We will be referring to the examples along all the tutorial. Field "type": "relationship" ---------------------------- Must be defined on the *One* side of OneToOne or OneToMany relationship (``User`` in our example). Relationships are created as OneToMany by default. Example of using ``relationship`` field (defined on ``User`` model in our example): .. code-block:: json "stories": { "_db_settings": { "type": "relationship", "document": "Story", "backref_name": "owner" } } **Required params:** *type* String. Just ``relationship``. *document* String. Exact name of model class to which relationship is set up. To find out the name of model use singularized uppercased version of route name. E.g. if we want to set up relationship to objects of ``/stories`` then the ``document`` arg will be ``Story``. *backref_name* String. Name of *back reference* field. This field will be auto-generated on model we set up relationship to and will hold the instance of model we are defining. In our example, field ``Story.owner`` will be generated and it will hold instance of ``User`` model to which story instance belongs. **Use this field to change relationships between objects.** Field "type": "foreign_key" --------------------------- This represents a Foreign Key constraint in SQL and is only required when using ``nefertari_sqla`` engine. It is used in conjunction with the relationship field, but is used on the model that ``relationship`` refers to. For example, if the ``User`` model contained the ``relationship`` field, than the ``Story`` model would need a ``foreign_key`` field. **Notes:** * This field is not required and is ignored when using nefertari_mongodb engine. * Name of the ``foreign_key`` field does not depend on relationship params in any way. * This field **MUST NOT** be used to change relationships. This field only exists because it is required by SQLAlchemy. Example of using ``foreign_key`` field (defined on ``Story`` model in our example): .. code-block:: json "owner_id": { "_db_settings": { "type": "foreign_key", "ref_document": "User", "ref_column": "user.username", "ref_column_type": "string" } } **Required params:** *type* String. Just ``foreign_key``. *ref_document* String. Exact name of model class to which foreign key is set up. To find out the name of model use singularized uppercased version of route name. E.g. if we want to set up foreign key to objects of ``/user`` then the ``ref_document`` arg will be ``User``. *ref_column* String. Dotted name/path to ``ref_document`` model's primary key column. ``ref_column`` is the lowercased name of model we refer to in ``ref_document`` joined by a dot with the exact name of its primary key column. In our example this is ``"user.username"``. **ref_column_type** String. Ramses field type of ``ref_document`` model's primary key column specified in ``ref_column`` parameter. In our example this is ``"string"`` because ``User.username`` is ``"type": "string"``. One to One relationship ----------------------- To create OneToOne relationships, specify ``"uselist": false`` in ``_db_settings`` of ``relationship`` field. When setting up One-to-One relationship, it doesn't matter which side defines the ``relationship`` field. E.g. if we had ``Profile`` model and we wanted to set up One-to-One relationship between ``Profile`` and ``User``, we would have to define a regular ``foreign_key`` field on ``Profile``: .. code-block:: json "user_id": { "_db_settings": { "type": "foreign_key", "ref_document": "User", "ref_column": "user.username", "ref_column_type": "string" } } and ``relationship`` field with ``"uselist": false`` on ``User``: .. code-block:: json "profile": { "_db_settings": { "type": "relationship", "document": "Profile", "backref_name": "user", "uselist": false } } This relationship could also be defined the other way but with the same result: ``foreign_key`` field on ``User`` and ``relationship`` field on ``Profile`` pointing to ``User``. Multiple relationships ---------------------- **Note: This part is only valid(required) for nefertari_sqla engine, as nefertari_mongodb engine does not use foreign_key fields.** If we were to define multiple relationships from model A to model B, each relationship must have a corresponding ``foreign_key`` defined. Also you must use a ``foreign_keys`` parameter on each ``relationship`` field to specify which ``foreign_key`` each ``relationship`` uses. E.g. if we were to add new relationship field ``User.assigned_stories``, relationship fields on ``User`` would have to be defined like this: .. code-block:: json "stories": { "_db_settings": { "type": "relationship", "document": "Story", "backref_name": "owner", "foreign_keys": "Story.owner_id" } }, "assigned_stories": { "_db_settings": { "type": "relationship", "document": "Story", "backref_name": "assignee", "foreign_keys": "Story.assignee_id" } } And fields on ``Story`` like so: .. code-block:: json "owner_id": { "_db_settings": { "type": "foreign_key", "ref_document": "User", "ref_column": "user.username", "ref_column_type": "string" } }, "assignee_id": { "_db_settings": { "type": "foreign_key", "ref_document": "User", "ref_column": "user.username", "ref_column_type": "string" } } Complete example ---------------- **example.raml** .. code-block:: yaml #%RAML 0.8 --- title: Example REST API documentation: - title: Home content: | Welcome to the example API. baseUri: http://{host}:{port}/{version} version: v1 /stories: displayName: All stories get: description: Get all stories post: description: Create a new story body: application/json: schema: !include story.json /{id}: displayName: One story get: description: Get a particular story /users: displayName: All users get: description: Get all users post: description: Create a new user body: application/json: schema: !include user.json /{username}: displayName: One user get: description: Get a particular user **user.json** .. code-block:: json { "type": "object", "title": "User schema", "$schema": "http://json-schema.org/draft-04/schema", "required": ["username"], "properties": { "username": { "_db_settings": { "type": "string", "primary_key": true } }, "stories": { "_db_settings": { "type": "relationship", "document": "Story", "backref_name": "owner" } } } } **story.json** .. code-block:: json { "type": "object", "title": "Story schema", "$schema": "http://json-schema.org/draft-04/schema", "properties": { "id": { "_db_settings": { "type": "id_field", "primary_key": true } }, "owner_id": { "_db_settings": { "type": "foreign_key", "ref_document": "User", "ref_column": "user.username", "ref_column_type": "string" } } } } ================================================ FILE: docs/source/schemas.rst ================================================ Defining Schemas ================ JSON Schema ----------- Ramses supports JSON Schema Draft 3 and Draft 4. You can read the official `JSON Schema documentation here `_. .. code-block:: json { "type": "object", "title": "Item schema", "$schema": "http://json-schema.org/draft-04/schema", (...) } All Ramses-specific properties are prefixed with an underscore. Showing Fields -------------- If you've enabled authentication, you can list which fields to return to authenticated users in ``_auth_fields`` and to non-authenticated users in ``_public_fields``. Additionaly, you can list fields to be hidden but remain hidden (with proper persmissions) in ``_hidden_fields``. .. code-block:: json { (...) "_auth_fields": ["id", "name", "description"], "_public_fields": ["name"], "_hidden_fields": ["token"], (...) } Nested Documents ---------------- If you use ``Relationship`` fields in your schemas, you can list those fields in ``_nested_relationships``. Your fields will then become nested documents instead of just showing the ``id``. You can control the level of nesting by specifying the ``_nesting_depth`` property, defaul is 1. .. code-block:: json { (...) "_nested_relationships": ["relationship_field_name"], "_nesting_depth": 2 (...) } Custom "user" Model ------------------- When authentication is enabled, a default "user" model will be created automatically with 4 fields: "username", "email", "groups" and "password". You can extend this default model by defining your own "user" schema and by setting ``_auth_model`` to ``true`` on that schema. You can add any additional fields in addition to those 4 default fields. .. code-block:: json { (...) "_auth_model": true, (...) } ================================================ FILE: ramses/__init__.py ================================================ import logging import ramlfications from nefertari.acl import RootACL as NefertariRootACL from nefertari.utils import dictset log = logging.getLogger(__name__) def includeme(config): from .generators import generate_server, generate_models Settings = dictset(config.registry.settings) config.include('nefertari.engine') config.registry.database_acls = Settings.asbool('database_acls') if config.registry.database_acls: config.include('nefertari_guards') config.include('nefertari') config.include('nefertari.view') config.include('nefertari.json_httpexceptions') # Process nefertari settings if Settings.asbool('enable_get_tunneling'): config.add_tween('nefertari.tweens.get_tunneling') if Settings.asbool('cors.enable'): config.add_tween('nefertari.tweens.cors') if Settings.asbool('ssl_middleware.enable'): config.add_tween('nefertari.tweens.ssl') if Settings.asbool('request_timing.enable'): config.add_tween('nefertari.tweens.request_timing') # Set root factory config.root_factory = NefertariRootACL # Process auth settings root = config.get_root_resource() root_auth = getattr(root, 'auth', False) log.info('Parsing RAML') raml_root = ramlfications.parse(Settings['ramses.raml_schema']) log.info('Starting models generation') generate_models(config, raml_resources=raml_root.resources) if root_auth: from .auth import setup_auth_policies, get_authuser_model if getattr(config.registry, 'auth_model', None) is None: config.registry.auth_model = get_authuser_model() setup_auth_policies(config, raml_root) config.include('nefertari.elasticsearch') log.info('Starting server generation') generate_server(raml_root, config) log.info('Running nefertari.engine.setup_database') from nefertari.engine import setup_database setup_database(config) from nefertari.elasticsearch import ES ES.setup_mappings() if root_auth: config.include('ramses.auth') log.info('Server succesfully generated\n') ================================================ FILE: ramses/acl.py ================================================ import logging import six from pyramid.security import ( Allow, Deny, Everyone, Authenticated, ALL_PERMISSIONS) from nefertari.acl import CollectionACL from nefertari.resource import PERMISSIONS from nefertari.elasticsearch import ES from .utils import resolve_to_callable, is_callable_tag log = logging.getLogger(__name__) actions = { 'allow': Allow, 'deny': Deny, } special_principals = { 'everyone': Everyone, 'authenticated': Authenticated, } ALLOW_ALL = (Allow, Everyone, ALL_PERMISSIONS) def validate_permissions(perms): """ Validate :perms: contains valid permissions. :param perms: List of permission names or ALL_PERMISSIONS. """ if not isinstance(perms, (list, tuple)): perms = [perms] valid_perms = set(PERMISSIONS.values()) if ALL_PERMISSIONS in perms: return perms if set(perms) - valid_perms: raise ValueError( 'Invalid ACL permission names. Valid permissions ' 'are: {}'.format(', '.join(valid_perms))) return perms def parse_permissions(perms): """ Parse permissions ("perms") which are either exact permission names or the keyword 'all'. :param perms: List or comma-separated string of nefertari permission names, or 'all' """ if isinstance(perms, six.string_types): perms = perms.split(',') perms = [perm.strip().lower() for perm in perms] if 'all' in perms: return ALL_PERMISSIONS return validate_permissions(perms) def parse_acl(acl_string): """ Parse raw string :acl_string: of RAML-defined ACLs. If :acl_string: is blank or None, all permissions are given. Values of ACL action and principal are parsed using `actions` and `special_principals` maps and are looked up after `strip()` and `lower()`. ACEs in :acl_string: may be separated by newlines or semicolons. Action, principal and permission lists must be separated by spaces. Permissions must be comma-separated. E.g. 'allow everyone view,create,update' and 'deny authenticated delete' :param acl_string: Raw RAML string containing defined ACEs. """ if not acl_string: return [ALLOW_ALL] aces_list = acl_string.replace('\n', ';').split(';') aces_list = [ace.strip().split(' ', 2) for ace in aces_list if ace] aces_list = [(a, b, c.split(',')) for a, b, c in aces_list] result_acl = [] for action_str, princ_str, perms in aces_list: # Process action action_str = action_str.strip().lower() action = actions.get(action_str) if action is None: raise ValueError( 'Unknown ACL action: {}. Valid actions: {}'.format( action_str, list(actions.keys()))) # Process principal princ_str = princ_str.strip().lower() if princ_str in special_principals: principal = special_principals[princ_str] elif is_callable_tag(princ_str): principal = resolve_to_callable(princ_str) else: principal = princ_str # Process permissions permissions = parse_permissions(perms) result_acl.append((action, principal, permissions)) return result_acl class BaseACL(CollectionACL): """ ACL Base class. """ es_based = False _collection_acl = (ALLOW_ALL, ) _item_acl = (ALLOW_ALL, ) def _apply_callables(self, acl, obj=None): """ Iterate over ACEs from :acl: and apply callable principals if any. Principals are passed 3 arguments on call: :ace: Single ACE object that looks like (action, callable, permission or [permission]) :request: Current request object :obj: Object instance to be accessed via the ACL Principals must return a single ACE or a list of ACEs. :param acl: Sequence of valid Pyramid ACEs which will be processed :param obj: Object to be accessed via the ACL """ new_acl = [] for i, ace in enumerate(acl): principal = ace[1] if six.callable(principal): ace = principal(ace=ace, request=self.request, obj=obj) if not ace: continue if not isinstance(ace[0], (list, tuple)): ace = [ace] ace = [(a, b, validate_permissions(c)) for a, b, c in ace] else: ace = [ace] new_acl += ace return tuple(new_acl) def __acl__(self): """ Apply callables to `self._collection_acl` and return result. """ return self._apply_callables(acl=self._collection_acl) def generate_item_acl(self, item): acl = self._apply_callables( acl=self._item_acl, obj=item) if acl is None: acl = self.__acl__() return acl def item_acl(self, item): """ Apply callables to `self._item_acl` and return result. """ return self.generate_item_acl(item) def item_db_id(self, key): # ``self`` can be used for current authenticated user key if key != 'self': return key user = getattr(self.request, 'user', None) if user is None or not isinstance(user, self.item_model): return key return getattr(user, user.pk_field()) def __getitem__(self, key): """ Get item using method depending on value of `self.es_based` """ if not self.es_based: return super(BaseACL, self).__getitem__(key) return self.getitem_es(self.item_db_id(key)) def getitem_es(self, key): es = ES(self.item_model.__name__) obj = es.get_item(id=key) obj.__acl__ = self.item_acl(obj) obj.__parent__ = self obj.__name__ = key return obj class DatabaseACLMixin(object): """ Mixin to be used when ACLs are stored in database. """ def item_acl(self, item): """ Objectify ACL if ES is used or call item.get_acl() if db is used. """ if self.es_based: from nefertari_guards.elasticsearch import get_es_item_acl return get_es_item_acl(item) return super(DatabaseACLMixin, self).item_acl(item) def getitem_es(self, key): """ Override to support ACL filtering. To do so: passes `self.request` to `get_item` and uses `ACLFilterES`. """ from nefertari_guards.elasticsearch import ACLFilterES es = ACLFilterES(self.item_model.__name__) params = { 'id': key, 'request': self.request, } obj = es.get_item(**params) obj.__acl__ = self.item_acl(obj) obj.__parent__ = self obj.__name__ = key return obj def generate_acl(config, model_cls, raml_resource, es_based=True): """ Generate an ACL. Generated ACL class has a `item_model` attribute set to :model_cls:. ACLs used for collection and item access control are generated from a first security scheme with type `x-ACL`. If :raml_resource: has no x-ACL security schemes defined then ALLOW_ALL ACL is used. If the `collection` or `item` settings are empty, then ALLOW_ALL ACL is used. :param model_cls: Generated model class :param raml_resource: Instance of ramlfications.raml.ResourceNode for which ACL is being generated :param es_based: Boolean inidicating whether ACL should query ES or not when getting an object """ schemes = raml_resource.security_schemes or [] schemes = [sch for sch in schemes if sch.type == 'x-ACL'] if not schemes: collection_acl = item_acl = [] log.debug('No ACL scheme applied. Using ACL: {}'.format(item_acl)) else: sec_scheme = schemes[0] log.debug('{} ACL scheme applied'.format(sec_scheme.name)) settings = sec_scheme.settings or {} collection_acl = parse_acl(acl_string=settings.get('collection')) item_acl = parse_acl(acl_string=settings.get('item')) class GeneratedACLBase(object): item_model = model_cls def __init__(self, request, es_based=es_based): super(GeneratedACLBase, self).__init__(request=request) self.es_based = es_based self._collection_acl = collection_acl self._item_acl = item_acl bases = [GeneratedACLBase] if config.registry.database_acls: from nefertari_guards.acl import DatabaseACLMixin as GuardsMixin bases += [DatabaseACLMixin, GuardsMixin] bases.append(BaseACL) return type('GeneratedACL', tuple(bases), {}) ================================================ FILE: ramses/auth.py ================================================ """ Auth module that contains all code needed for authentication/authorization policies setup. In particular: :includeme: Function that actually creates routes listed above and connects view to them :create_system_user: Function that creates system/admin user :_setup_ticket_policy: Setup Pyramid AuthTktAuthenticationPolicy :_setup_apikey_policy: Setup nefertari.ApiKeyAuthenticationPolicy :setup_auth_policies: Runs generation of particular auth policy """ import logging import transaction from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from pyramid.security import Allow, ALL_PERMISSIONS import cryptacular.bcrypt from nefertari.utils import dictset from nefertari.json_httpexceptions import * from nefertari.authentication.policies import ApiKeyAuthenticationPolicy log = logging.getLogger(__name__) class ACLAssignRegisterMixin(object): """ Mixin that sets ``User._acl`` field after user is registered. """ def register(self, *args, **kwargs): response = super(ACLAssignRegisterMixin, self).register( *args, **kwargs) user = self.request._user mapping = self.request.registry._model_collections if not user._acl and self.Model.__name__ in mapping: from nefertari_guards import engine as guards_engine factory = mapping[self.Model.__name__].view._factory acl = factory(self.request).generate_item_acl(user) acl = guards_engine.ACLField.stringify_acl(acl) user.update({'_acl': acl}) return response def _setup_ticket_policy(config, params): """ Setup Pyramid AuthTktAuthenticationPolicy. Notes: * Initial `secret` params value is considered to be a name of config param that represents a cookie name. * `auth_model.get_groups_by_userid` is used as a `callback`. * Also connects basic routes to perform authentication actions. :param config: Pyramid Configurator instance. :param params: Nefertari dictset which contains security scheme `settings`. """ from nefertari.authentication.views import ( TicketAuthRegisterView, TicketAuthLoginView, TicketAuthLogoutView) log.info('Configuring Pyramid Ticket Authn policy') if 'secret' not in params: raise ValueError( 'Missing required security scheme settings: secret') params['secret'] = config.registry.settings[params['secret']] auth_model = config.registry.auth_model params['callback'] = auth_model.get_groups_by_userid config.add_request_method( auth_model.get_authuser_by_userid, 'user', reify=True) policy = AuthTktAuthenticationPolicy(**params) RegisterViewBase = TicketAuthRegisterView if config.registry.database_acls: class RegisterViewBase(ACLAssignRegisterMixin, TicketAuthRegisterView): pass class RamsesTicketAuthRegisterView(RegisterViewBase): Model = config.registry.auth_model class RamsesTicketAuthLoginView(TicketAuthLoginView): Model = config.registry.auth_model class RamsesTicketAuthLogoutView(TicketAuthLogoutView): Model = config.registry.auth_model common_kw = { 'prefix': 'auth', 'factory': 'nefertari.acl.AuthenticationACL', } root = config.get_root_resource() root.add('register', view=RamsesTicketAuthRegisterView, **common_kw) root.add('login', view=RamsesTicketAuthLoginView, **common_kw) root.add('logout', view=RamsesTicketAuthLogoutView, **common_kw) return policy def _setup_apikey_policy(config, params): """ Setup `nefertari.ApiKeyAuthenticationPolicy`. Notes: * User may provide model name in :params['user_model']: do define the name of the user model. * `auth_model.get_groups_by_token` is used to perform username and token check * `auth_model.get_token_credentials` is used to get username and token from userid * Also connects basic routes to perform authentication actions. Arguments: :config: Pyramid Configurator instance. :params: Nefertari dictset which contains security scheme `settings`. """ from nefertari.authentication.views import ( TokenAuthRegisterView, TokenAuthClaimView, TokenAuthResetView) log.info('Configuring ApiKey Authn policy') auth_model = config.registry.auth_model params['check'] = auth_model.get_groups_by_token params['credentials_callback'] = auth_model.get_token_credentials params['user_model'] = auth_model config.add_request_method( auth_model.get_authuser_by_name, 'user', reify=True) policy = ApiKeyAuthenticationPolicy(**params) RegisterViewBase = TokenAuthRegisterView if config.registry.database_acls: class RegisterViewBase(ACLAssignRegisterMixin, TokenAuthRegisterView): pass class RamsesTokenAuthRegisterView(RegisterViewBase): Model = auth_model class RamsesTokenAuthClaimView(TokenAuthClaimView): Model = auth_model class RamsesTokenAuthResetView(TokenAuthResetView): Model = auth_model common_kw = { 'prefix': 'auth', 'factory': 'nefertari.acl.AuthenticationACL', } root = config.get_root_resource() root.add('register', view=RamsesTokenAuthRegisterView, **common_kw) root.add('token', view=RamsesTokenAuthClaimView, **common_kw) root.add('reset_token', view=RamsesTokenAuthResetView, **common_kw) return policy """ Map of `security_scheme_type`: `generator_function`, where: * `security_scheme_type`: String that represents RAML security scheme type name that should be used to apply a particular authentication system. * `generator_function`: Function that receives instance of Pyramid Configurator instance and dictset of security scheme settings and returns generated Pyramid authentication policy instance. """ AUTHENTICATION_POLICIES = { 'x-ApiKey': _setup_apikey_policy, 'x-Ticket': _setup_ticket_policy, } def setup_auth_policies(config, raml_root): """ Setup authentication, authorization policies. Performs basic validation to check all the required values are present and performs authentication, authorization policies generation using generator functions from `AUTHENTICATION_POLICIES`. :param config: Pyramid Configurator instance. :param raml_root: Instance of ramlfications.raml.RootNode. """ log.info('Configuring auth policies') secured_by_all = raml_root.secured_by or [] secured_by = [item for item in secured_by_all if item] if not secured_by: log.info('API is not secured. `secured_by` attribute ' 'value missing.') return secured_by = secured_by[0] schemes = {scheme.name: scheme for scheme in raml_root.security_schemes} if secured_by not in schemes: raise ValueError( 'Undefined security scheme used in `secured_by`: {}'.format( secured_by)) scheme = schemes[secured_by] if scheme.type not in AUTHENTICATION_POLICIES: raise ValueError('Unsupported security scheme type: {}'.format( scheme.type)) # Setup Authentication policy policy_generator = AUTHENTICATION_POLICIES[scheme.type] params = dictset(scheme.settings or {}) authn_policy = policy_generator(config, params) config.set_authentication_policy(authn_policy) # Setup Authorization policy authz_policy = ACLAuthorizationPolicy() config.set_authorization_policy(authz_policy) def create_system_user(config): log.info('Creating system user') crypt = cryptacular.bcrypt.BCRYPTPasswordManager() settings = config.registry.settings try: auth_model = config.registry.auth_model s_user = settings['system.user'] s_pass = str(crypt.encode(settings['system.password'])) s_email = settings['system.email'] defaults = dict( password=s_pass, email=s_email, groups=['admin'], ) if config.registry.database_acls: defaults['_acl'] = [(Allow, 'g:admin', ALL_PERMISSIONS)] user, created = auth_model.get_or_create( username=s_user, defaults=defaults) if created: transaction.commit() except KeyError as e: log.error('Failed to create system user. Missing config: %s' % e) def get_authuser_model(): """ Define and return AuthUser model using nefertari base classes """ from nefertari.authentication.models import AuthUserMixin from nefertari import engine class AuthUser(AuthUserMixin, engine.BaseDocument): __tablename__ = 'ramses_authuser' return AuthUser def includeme(config): create_system_user(config) ================================================ FILE: ramses/generators.py ================================================ import logging from inflection import singularize from .views import generate_rest_view from .acl import generate_acl from .utils import ( is_dynamic_uri, resource_view_attrs, generate_model_name, dynamic_part_name, attr_subresource, singular_subresource, get_static_parent, get_route_name, get_resource_uri, ) log = logging.getLogger(__name__) def _get_nefertari_parent_resource( raml_resource, generated_resources, default): parent_raml_res = get_static_parent(raml_resource) if parent_raml_res is not None: parent_resource = generated_resources.get(parent_raml_res.path) return parent_resource or default return default def generate_resource(config, raml_resource, parent_resource): """ Perform complete one resource configuration process This function generates: ACL, view, route, resource, database model for a given `raml_resource`. New nefertari resource is attached to `parent_resource` class which is an instance of `nefertari.resource.Resource`. Things to consider: * Top-level resources must be collection names. * No resources are explicitly created for dynamic (ending with '}') RAML resources as they are implicitly processed by parent collection resources. * Resource nesting must look like collection/id/collection/id/... * Only part of resource path after last '/' is taken into account, thus each level of resource nesting should add one more path element. E.g. /stories -> /stories/{id} and not /stories -> /stories/mystories/{id}. Latter route will be generated at /stories/{id}. :param raml_resource: Instance of ramlfications.raml.ResourceNode. :param parent_resource: Parent nefertari resource object. """ from .models import get_existing_model # Don't generate resources for dynamic routes as they are already # generated by their parent resource_uri = get_resource_uri(raml_resource) if is_dynamic_uri(resource_uri): if parent_resource.is_root: raise Exception("Top-level resources can't be dynamic and must " "represent collections instead") return route_name = get_route_name(resource_uri) log.info('Configuring resource: `{}`. Parent: `{}`'.format( route_name, parent_resource.uid or 'root')) # Get DB model. If this is an attribute or singular resource, # we don't need to get model is_singular = singular_subresource(raml_resource, route_name) is_attr_res = attr_subresource(raml_resource, route_name) if not parent_resource.is_root and (is_attr_res or is_singular): model_cls = parent_resource.view.Model else: model_name = generate_model_name(raml_resource) model_cls = get_existing_model(model_name) resource_kwargs = {} # Generate ACL log.info('Generating ACL for `{}`'.format(route_name)) resource_kwargs['factory'] = generate_acl( config, model_cls=model_cls, raml_resource=raml_resource) # Generate dynamic part name if not is_singular: resource_kwargs['id_name'] = dynamic_part_name( raml_resource=raml_resource, route_name=route_name, pk_field=model_cls.pk_field()) # Generate REST view log.info('Generating view for `{}`'.format(route_name)) view_attrs = resource_view_attrs(raml_resource, is_singular) resource_kwargs['view'] = generate_rest_view( config, model_cls=model_cls, attrs=view_attrs, attr_view=is_attr_res, singular=is_singular, ) # In case of singular resource, model still needs to be generated, # but we store it on a different view attribute if is_singular: model_name = generate_model_name(raml_resource) view_cls = resource_kwargs['view'] view_cls._parent_model = view_cls.Model view_cls.Model = get_existing_model(model_name) # Create new nefertari resource log.info('Creating new resource for `{}`'.format(route_name)) clean_uri = resource_uri.strip('/') resource_args = (singularize(clean_uri),) if not is_singular: resource_args += (clean_uri,) return parent_resource.add(*resource_args, **resource_kwargs) def generate_server(raml_root, config): """ Handle server generation process. :param raml_root: Instance of ramlfications.raml.RootNode. :param config: Pyramid Configurator instance. """ log.info('Server generation started') if not raml_root.resources: return root_resource = config.get_root_resource() generated_resources = {} for raml_resource in raml_root.resources: if raml_resource.path in generated_resources: continue # Get Nefertari parent resource parent_resource = _get_nefertari_parent_resource( raml_resource, generated_resources, root_resource) # Get generated resource and store it new_resource = generate_resource( config, raml_resource, parent_resource) if new_resource is not None: generated_resources[raml_resource.path] = new_resource def generate_models(config, raml_resources): """ Generate model for each resource in :raml_resources: The DB model name is generated using singular titled version of current resource's url. E.g. for resource under url '/stories', model with name 'Story' will be generated. :param config: Pyramid Configurator instance. :param raml_resources: List of ramlfications.raml.ResourceNode. """ from .models import handle_model_generation if not raml_resources: return for raml_resource in raml_resources: # No need to generate models for dynamic resource if is_dynamic_uri(raml_resource.path): continue # Since POST resource must define schema use only POST # resources to generate models if raml_resource.method.upper() != 'POST': continue # Generate DB model # If this is an attribute resource we don't need to generate model resource_uri = get_resource_uri(raml_resource) route_name = get_route_name(resource_uri) if not attr_subresource(raml_resource, route_name): log.info('Configuring model for route `{}`'.format(route_name)) model_cls, is_auth_model = handle_model_generation( config, raml_resource) if is_auth_model: config.registry.auth_model = model_cls ================================================ FILE: ramses/models.py ================================================ import logging from nefertari import engine from inflection import pluralize from .utils import ( resolve_to_callable, is_callable_tag, resource_schema, generate_model_name, get_events_map) from . import registry log = logging.getLogger(__name__) """ Map of RAML types names to nefertari.engine fields. """ type_fields = { 'string': engine.StringField, 'float': engine.FloatField, 'integer': engine.IntegerField, 'boolean': engine.BooleanField, 'datetime': engine.DateTimeField, 'file': engine.BinaryField, 'relationship': engine.Relationship, 'dict': engine.DictField, 'foreign_key': engine.ForeignKeyField, 'big_integer': engine.BigIntegerField, 'date': engine.DateField, 'choice': engine.ChoiceField, 'interval': engine.IntervalField, 'decimal': engine.DecimalField, 'pickle': engine.PickleField, 'small_integer': engine.SmallIntegerField, 'text': engine.TextField, 'time': engine.TimeField, 'unicode': engine.UnicodeField, 'unicode_text': engine.UnicodeTextField, 'id_field': engine.IdField, 'list': engine.ListField, } def get_existing_model(model_name): """ Try to find existing model class named `model_name`. :param model_name: String name of the model class. """ try: model_cls = engine.get_document_cls(model_name) log.debug('Model `{}` already exists. Using existing one'.format( model_name)) return model_cls except ValueError: log.debug('Model `{}` does not exist'.format(model_name)) def prepare_relationship(config, model_name, raml_resource): """ Create referenced model if it doesn't exist. When preparing a relationship, we check to see if the model that will be referenced already exists. If not, it is created so that it will be possible to use it in a relationship. Thus the first usage of this model in RAML file must provide its schema in POST method resource body schema. :param model_name: Name of model which should be generated. :param raml_resource: Instance of ramlfications.raml.ResourceNode for which :model_name: will be defined. """ if get_existing_model(model_name) is None: plural_route = '/' + pluralize(model_name.lower()) route = '/' + model_name.lower() for res in raml_resource.root.resources: if res.method.upper() != 'POST': continue if res.path.endswith(plural_route) or res.path.endswith(route): break else: raise ValueError('Model `{}` used in relationship is not ' 'defined'.format(model_name)) setup_data_model(config, res, model_name) def generate_model_cls(config, schema, model_name, raml_resource, es_based=True): """ Generate model class. Engine DB field types are determined using `type_fields` and only those types may be used. :param schema: Model schema dict parsed from RAML. :param model_name: String that is used as new model's name. :param raml_resource: Instance of ramlfications.raml.ResourceNode. :param es_based: Boolean indicating if generated model should be a subclass of Elasticsearch-based document class or not. It True, ESBaseDocument is used; BaseDocument is used otherwise. Defaults to True. """ from nefertari.authentication.models import AuthModelMethodsMixin base_cls = engine.ESBaseDocument if es_based else engine.BaseDocument model_name = str(model_name) metaclass = type(base_cls) auth_model = schema.get('_auth_model', False) bases = [] if config.registry.database_acls: from nefertari_guards import engine as guards_engine bases.append(guards_engine.DocumentACLMixin) if auth_model: bases.append(AuthModelMethodsMixin) bases.append(base_cls) attrs = { '__tablename__': model_name.lower(), '_public_fields': schema.get('_public_fields') or [], '_auth_fields': schema.get('_auth_fields') or [], '_hidden_fields': schema.get('_hidden_fields') or [], '_nested_relationships': schema.get('_nested_relationships') or [], } if '_nesting_depth' in schema: attrs['_nesting_depth'] = schema.get('_nesting_depth') # Generate fields from properties properties = schema.get('properties', {}) for field_name, props in properties.items(): if field_name in attrs: continue db_settings = props.get('_db_settings') if db_settings is None: continue field_kwargs = db_settings.copy() field_kwargs['required'] = bool(field_kwargs.get('required')) for default_attr_key in ('default', 'onupdate'): value = field_kwargs.get(default_attr_key) if is_callable_tag(value): field_kwargs[default_attr_key] = resolve_to_callable(value) type_name = ( field_kwargs.pop('type', 'string') or 'string').lower() if type_name not in type_fields: raise ValueError('Unknown type: {}'.format(type_name)) field_cls = type_fields[type_name] if field_cls is engine.Relationship: prepare_relationship( config, field_kwargs['document'], raml_resource) if field_cls is engine.ForeignKeyField: key = 'ref_column_type' field_kwargs[key] = type_fields[field_kwargs[key]] if field_cls is engine.ListField: key = 'item_type' field_kwargs[key] = type_fields[field_kwargs[key]] attrs[field_name] = field_cls(**field_kwargs) # Update model definition with methods and variables defined in registry attrs.update(registry.mget(model_name)) # Generate new model class model_cls = metaclass(model_name, tuple(bases), attrs) setup_model_event_subscribers(config, model_cls, schema) setup_fields_processors(config, model_cls, schema) return model_cls, auth_model def setup_data_model(config, raml_resource, model_name): """ Setup storage/data model and return generated model class. Process follows these steps: * Resource schema is found and restructured by `resource_schema`. * Model class is generated from properties dict using util function `generate_model_cls`. :param raml_resource: Instance of ramlfications.raml.ResourceNode. :param model_name: String representing model name. """ model_cls = get_existing_model(model_name) schema = resource_schema(raml_resource) if not schema: raise Exception('Missing schema for model `{}`'.format(model_name)) if model_cls is not None: return model_cls, schema.get('_auth_model', False) log.info('Generating model class `{}`'.format(model_name)) return generate_model_cls( config, schema=schema, model_name=model_name, raml_resource=raml_resource, ) def handle_model_generation(config, raml_resource): """ Generates model name and runs `setup_data_model` to get or generate actual model class. :param raml_resource: Instance of ramlfications.raml.ResourceNode. """ model_name = generate_model_name(raml_resource) try: return setup_data_model(config, raml_resource, model_name) except ValueError as ex: raise ValueError('{}: {}'.format(model_name, str(ex))) def setup_model_event_subscribers(config, model_cls, schema): """ Set up model event subscribers. :param config: Pyramid Configurator instance. :param model_cls: Model class for which handlers should be connected. :param schema: Dict of model JSON schema. """ events_map = get_events_map() model_events = schema.get('_event_handlers', {}) event_kwargs = {'model': model_cls} for event_tag, subscribers in model_events.items(): type_, action = event_tag.split('_') event_objects = events_map[type_][action] if not isinstance(event_objects, list): event_objects = [event_objects] for sub_name in subscribers: sub_func = resolve_to_callable(sub_name) config.subscribe_to_events( sub_func, event_objects, **event_kwargs) def setup_fields_processors(config, model_cls, schema): """ Set up model fields' processors. :param config: Pyramid Configurator instance. :param model_cls: Model class for field of which processors should be set up. :param schema: Dict of model JSON schema. """ properties = schema.get('properties', {}) for field_name, props in properties.items(): if not props: continue processors = props.get('_processors') backref_processors = props.get('_backref_processors') if processors: processors = [resolve_to_callable(val) for val in processors] setup_kwargs = {'model': model_cls, 'field': field_name} config.add_field_processors(processors, **setup_kwargs) if backref_processors: db_settings = props.get('_db_settings', {}) is_relationship = db_settings.get('type') == 'relationship' document = db_settings.get('document') backref_name = db_settings.get('backref_name') if not (is_relationship and document and backref_name): continue backref_processors = [ resolve_to_callable(val) for val in backref_processors] setup_kwargs = { 'model': engine.get_document_cls(document), 'field': backref_name } config.add_field_processors( backref_processors, **setup_kwargs) ================================================ FILE: ramses/registry.py ================================================ """ Naive registry that is just a subclass of a python dictionary. It is meant to be used to store objects and retrieve them when needed. The registry is recreated on each app launch and is best suited to store some dynamic or short-term data. Storing an object should be performed by using the `add` function, and retrieving it by using the `get` function. Examples: Register a function under a function name:: from ramses import registry @registry.add def foo(): print 'In foo' assert registry.get('foo') is foo Register a function under a different name:: from ramses import registry @registry.add('bar') def foo(): print 'In foo' assert registry.get('bar') is foo Register an arbitrary object:: from ramses import registry myvar = 'my awesome var' registry.add('my_stored_var', myvar) assert registry.get('my_stored_var') == myvar Register and get an object by namespace:: from ramses import registry myvar = 'my awesome var' registry.add('Foo.my_stored_var', myvar) assert registry.mget('Foo') == {'my_stored_var': myvar} """ import six class Registry(dict): pass registry = Registry() def add(*args): def decorator(function): registry[name] = function return function if len(args) == 1 and six.callable(args[0]): function = args[0] name = function.__name__ return decorator(function) elif len(args) == 2: registry[args[0]] = args[1] else: name = args[0] return decorator def get(name): try: return registry[name] except KeyError: raise KeyError( "Object named '{}' is not registered in ramses " "registry".format(name)) def mget(namespace): namespace = namespace.lower() + '.' data = {} for key, val in registry.items(): key = key.lower() if not key.startswith(namespace): continue clean_key = key.split(namespace)[-1] data[clean_key] = val return data ================================================ FILE: ramses/scaffolds/__init__.py ================================================ import os import subprocess from six import moves from pyramid.scaffolds import PyramidTemplate class RamsesStarterTemplate(PyramidTemplate): _template_dir = 'ramses_starter' summary = 'Ramses starter' def pre(self, command, output_dir, vars): dbengine_choices = {'1': 'sqla', '2': 'mongodb'} vars['engine'] = dbengine_choices[moves.input(""" Which database backend would you like to use: (1) for SQLAlchemy/PostgreSQL, or (2) for MongoEngine/MongoDB? [default is '1']: """) or '1'] if vars['package'] == 'site': raise ValueError(""" "Site" is a reserved keyword in Python. Please use a different project name. """) def post(self, command, output_dir, vars): os.chdir(str(output_dir)) subprocess.call('pip install -r requirements.txt', shell=True) subprocess.call('pip install nefertari-{}'.format(vars['engine']), shell=True) msg = """Goodbye boilerplate! Welcome to Ramses.""" self.out(msg) ================================================ FILE: ramses/scaffolds/ramses_starter/+package+/__init__.py ================================================ from pyramid.config import Configurator def main(global_config, **settings): config = Configurator(settings=settings) config.include('ramses') return config.make_wsgi_app() ================================================ FILE: ramses/scaffolds/ramses_starter/+package+/tests/__init__.py ================================================ ================================================ FILE: ramses/scaffolds/ramses_starter/+package+/tests/api.raml ================================================ #%RAML 0.8 --- title: Items API documentation: - title: Items REST API content: | Welcome to the Items API. baseUri: http://{host}:{port}/{version} version: api mediaType: application/json protocols: [HTTP] /items: displayName: Collection of items head: description: Head request responses: 200: description: Return headers get: description: Get all item responses: 200: description: Returns a list of items post: description: Create a new item body: application/json: schema: !include items.json example: | { "id": "507f191e810c19729de860ea", "name": "Banana", "description": "Tasty" } responses: 201: description: Created item body: application/json: schema: !include items.json /{id}: uriParameters: id: displayName: Item id type: string example: 507f191e810c19729de860ea displayName: Collection-item head: description: Head request responses: 200: description: Return headers get: description: Get a particular item responses: 200: body: application/json: schema: !include items.json delete: description: Delete a particular item responses: 200: description: Deleted item patch: description: Update a particular item body: application/json: example: { "name": "Tree" } responses: 200: body: application/json: schema: !include items.json ================================================ FILE: ramses/scaffolds/ramses_starter/+package+/tests/items.json ================================================ { "type": "object", "title": "Item schema", "$schema": "http://json-schema.org/draft-04/schema", "required": ["name"], "properties": { "id": { "type": ["string", "null"], "_db_settings": { "type": "id_field", "required": true, "primary_key": true } }, "name": { "type": "string", "_db_settings": { "type": "string", "required": true } }, "description": { "type": ["string", "null"], "_db_settings": { "type": "text" } } } } ================================================ FILE: ramses/scaffolds/ramses_starter/+package+/tests/requirements.txt ================================================ -e git+https://github.com/ramses-tech/ra.git@develop#egg=ra -e git+https://github.com/ramses-tech/nefertari.git@develop#egg=nefertari -e git+https://github.com/ramses-tech/nefertari-mongodb.git@develop#egg=nefertari-mongodb ================================================ FILE: ramses/scaffolds/ramses_starter/+package+/tests/test_api.py_tmpl ================================================ import os import ra import webtest import pytest appdir = os.path.abspath( os.path.join(os.path.dirname(__file__), '..', '..')) ramlfile = os.path.abspath( os.path.join(os.path.dirname(__file__), 'api.raml')) testapp = webtest.TestApp('config:local.ini', relative_to=appdir) @pytest.fixture(autouse=True) def setup(req, examples): """ Setup database state for tests. NOTE: For objects to be created, when using SQLA transaction needs to be commited as follows: import transaction transaction.commit() """ from nefertari import engine Item = engine.get_document_cls('Item') if req.match(exclude='POST /items'): if Item.get_collection(_count=True) == 0: example = examples.build('item') Item(**example).save() # ra entry point: instantiate the API test suite api = ra.api(ramlfile, testapp) api.autotest() ================================================ FILE: ramses/scaffolds/ramses_starter/.gitignore_tmpl ================================================ venv .DS_Store # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ local.ini mock/ ================================================ FILE: ramses/scaffolds/ramses_starter/README.md ================================================ ## Installation ``` $ pip install -r requirements.txt ``` ## Run ``` $ pserve local.ini ``` ================================================ FILE: ramses/scaffolds/ramses_starter/api.raml_tmpl ================================================ #%RAML 0.8 --- title: {{package}} API documentation: - title: {{package}} REST API content: | Welcome to the {{package}} API. baseUri: http://{host}:{port}/{version} version: v1 mediaType: application/json protocols: [HTTP] /items: displayName: Collection of items get: description: Get all item post: description: Create a new item body: application/json: schema: !include items.json /{id}: displayName: Collection-item get: description: Get a particular item delete: description: Delete a particular item patch: description: Update a particular item ================================================ FILE: ramses/scaffolds/ramses_starter/items.json ================================================ { "type": "object", "title": "Item schema", "$schema": "http://json-schema.org/draft-04/schema", "required": ["id", "name"], "properties": { "id": { "type": ["integer", "null"], "_db_settings": { "type": "id_field", "required": true, "primary_key": true } }, "name": { "type": "string", "_db_settings": { "type": "string", "required": true } }, "description": { "type": ["string", "null"], "_db_settings": { "type": "text" } } } } ================================================ FILE: ramses/scaffolds/ramses_starter/local.ini_tmpl ================================================ [app:{{package}}] use = egg:{{package}} # Ramses ramses.raml_schema = api.raml database_acls = false # Nefertari nefertari.engine = nefertari_{{engine}} enable_get_tunneling = true # SQLA sqlalchemy.url = postgresql://localhost:5432/{{package}} # MongoDB settings mongodb.host = localhost mongodb.port = 27017 mongodb.db = {{package}} # Elasticsearch elasticsearch.hosts = localhost:9200 elasticsearch.sniff = false elasticsearch.index_name = {{package}} elasticsearch.index.disable = false elasticsearch.enable_refresh_query = false elasticsearch.enable_aggregations = false elasticsearch.enable_polymorphic_query = false # {{package}} host = localhost base_url = http://%(host)s:6543 # CORS cors.enable = false cors.allow_origins = %(base_url)s cors.allow_credentials = true [composite:main] use = egg:Paste#urlmap /api/ = {{package}} [server:main] use = egg:waitress#main host = 0.0.0.0 port = 6543 threads = 3 [loggers] keys = root, {{package}}, nefertari, ramses [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [logger_{{package}}] level = INFO handlers = qualname = {{package}} [logger_nefertari] level = INFO handlers = qualname = nefertari [logger_ramses] level = INFO handlers = qualname = ramses [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(module)s.%(funcName)s: %(message)s ================================================ FILE: ramses/scaffolds/ramses_starter/requirements.txt ================================================ nefertari ramses Paste==2.0.2 pyramid==1.6.1 waitress==0.8.9 -e . ================================================ FILE: ramses/scaffolds/ramses_starter/setup.py_tmpl ================================================ from setuptools import setup, find_packages requires = ['pyramid'] setup(name='{{package}}', version='0.0.1', description='', long_description='', classifiers=[ "Programming Language :: Python", "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], author='', author_email='', url='', keywords='web pyramid pylons raml ramses', packages=find_packages(), include_package_data=True, zip_safe=False, install_requires=requires, tests_require=requires, test_suite="{{package}}", entry_points="""\ [paste.app_factory] main = {{package}}:main """, ) ================================================ FILE: ramses/scripts/__init__.py ================================================ ================================================ FILE: ramses/scripts/scaffold_test.py ================================================ from nefertari.scripts.scaffold_test import ( ScaffoldTestCommand as NefTestCommand) class ScaffoldTestCommand(NefTestCommand): file = __file__ def main(*args, **kwargs): ScaffoldTestCommand().run() if __name__ == '__main__': main() ================================================ FILE: ramses/utils.py ================================================ import re import logging from contextlib import contextmanager import six import inflection log = logging.getLogger(__name__) class ContentTypes(object): """ ContentType values. """ JSON = 'application/json' TEXT_XML = 'text/xml' MULTIPART_FORMDATA = 'multipart/form-data' FORM_URLENCODED = 'application/x-www-form-urlencoded' def convert_schema(raml_schema, mime_type): """ Restructure `raml_schema` to a dictionary that has 'properties' as well as other schema keys/values. The resulting dictionary looks like this:: { "properties": { "field1": { "required": boolean, "type": ..., ...more field options }, ...more properties }, "public_fields": [...], "auth_fields": [...], ...more schema options } :param raml_schema: RAML request body schema. :param mime_type: ContentType of the schema as a string from RAML file. Only JSON is currently supported. """ if mime_type == ContentTypes.JSON: if not isinstance(raml_schema, dict): raise TypeError( 'Schema is not a valid JSON. Please check your ' 'schema syntax.\n{}...'.format(str(raml_schema)[:60])) return raml_schema if mime_type == ContentTypes.TEXT_XML: # Process XML schema pass def is_dynamic_uri(uri): """ Determine whether `uri` is a dynamic uri or not. Assumes a dynamic uri is one that ends with '}' which is a Pyramid way to define dynamic parts in uri. :param uri: URI as a string. """ return uri.strip('/').endswith('}') def clean_dynamic_uri(uri): """ Strips /, {, } from dynamic `uri`. :param uri: URI as a string. """ return uri.replace('/', '').replace('{', '').replace('}', '') def generate_model_name(raml_resource): """ Generate model name. :param raml_resource: Instance of ramlfications.raml.ResourceNode. """ resource_uri = get_resource_uri(raml_resource).strip('/') resource_uri = re.sub('\W', ' ', resource_uri) model_name = inflection.titleize(resource_uri) return inflection.singularize(model_name).replace(' ', '') def dynamic_part_name(raml_resource, route_name, pk_field): """ Generate a dynamic part for a resource :raml_resource:. A dynamic part is generated using 2 parts: :route_name: of the resource and the dynamic part of first dynamic child resources. If :raml_resource: has no dynamic child resources, 'id' is used as the 2nd part. E.g. if your dynamic part on route 'stories' is named 'superId' then dynamic part will be 'stories_superId'. :param raml_resource: Instance of ramlfications.raml.ResourceNode for which dynamic part name is being generated. :param route_name: Cleaned name of :raml_resource: :param pk_field: Model Primary Key field name. """ subresources = get_resource_children(raml_resource) dynamic_uris = [res.path for res in subresources if is_dynamic_uri(res.path)] if dynamic_uris: dynamic_part = extract_dynamic_part(dynamic_uris[0]) else: dynamic_part = pk_field return '_'.join([route_name, dynamic_part]) def extract_dynamic_part(uri): """ Extract dynamic url part from :uri: string. :param uri: URI string that may contain dynamic part. """ for part in uri.split('/'): part = part.strip() if part.startswith('{') and part.endswith('}'): return clean_dynamic_uri(part) def resource_view_attrs(raml_resource, singular=False): """ Generate view method names needed for `raml_resource` view. Collects HTTP method names from resource siblings and dynamic children if exist. Collected methods are then translated to `nefertari.view.BaseView` method names, each of which is used to process a particular HTTP method request. Maps of {HTTP_method: view_method} `collection_methods` and `item_methods` are used to convert collection and item methods respectively. :param raml_resource: Instance of ramlfications.raml.ResourceNode :param singular: Boolean indicating if resource is singular or not """ from .views import collection_methods, item_methods # Singular resource doesn't have collection methods though # it looks like a collection if singular: collection_methods = item_methods siblings = get_resource_siblings(raml_resource) http_methods = [sibl.method.lower() for sibl in siblings] attrs = [collection_methods.get(method) for method in http_methods] # Check if resource has dynamic child resource like collection/{id} # If dynamic child resource exists, add its siblings' methods to attrs, # as both resources are handled by a single view children = get_resource_children(raml_resource) http_submethods = [child.method.lower() for child in children if is_dynamic_uri(child.path)] attrs += [item_methods.get(method) for method in http_submethods] return set(filter(bool, attrs)) def resource_schema(raml_resource): """ Get schema properties of RAML resource :raml_resource:. Must be called with RAML resource that defines body schema. First body that defines schema is used. Schema is converted on return using 'convert_schema'. :param raml_resource: Instance of ramlfications.raml.ResourceNode of POST method. """ # NOTE: Must be called with resource that defines body schema log.info('Searching for model schema') if not raml_resource.body: raise ValueError('RAML resource has no body to setup database ' 'schema from') for body in raml_resource.body: if body.schema: return convert_schema(body.schema, body.mime_type) log.debug('No model schema found.') def is_dynamic_resource(raml_resource): """ Determine if :raml_resource: is a dynamic resource. :param raml_resource: Instance of ramlfications.raml.ResourceNode. """ return raml_resource and is_dynamic_uri(raml_resource.path) def get_static_parent(raml_resource, method=None): """ Get static parent resource of :raml_resource: with HTTP method :method:. :param raml_resource:Instance of ramlfications.raml.ResourceNode. :param method: HTTP method name which matching static resource must have. """ parent = raml_resource.parent while is_dynamic_resource(parent): parent = parent.parent if parent is None: return parent match_method = method is not None if match_method: if parent.method.upper() == method.upper(): return parent else: return parent for res in parent.root.resources: if res.path == parent.path: if res.method.upper() == method.upper(): return res def attr_subresource(raml_resource, route_name): """ Determine if :raml_resource: is an attribute subresource. :param raml_resource: Instance of ramlfications.raml.ResourceNode. :param route_name: Name of the :raml_resource:. """ static_parent = get_static_parent(raml_resource, method='POST') if static_parent is None: return False schema = resource_schema(static_parent) or {} properties = schema.get('properties', {}) if route_name in properties: db_settings = properties[route_name].get('_db_settings', {}) return db_settings.get('type') in ('dict', 'list') return False def singular_subresource(raml_resource, route_name): """ Determine if :raml_resource: is a singular subresource. :param raml_resource: Instance of ramlfications.raml.ResourceNode. :param route_name: Name of the :raml_resource:. """ static_parent = get_static_parent(raml_resource, method='POST') if static_parent is None: return False schema = resource_schema(static_parent) or {} properties = schema.get('properties', {}) if route_name not in properties: return False db_settings = properties[route_name].get('_db_settings', {}) is_obj = db_settings.get('type') == 'relationship' single_obj = not db_settings.get('uselist', True) return is_obj and single_obj def is_callable_tag(tag): """ Determine whether :tag: is a valid callable string tag. String is assumed to be valid callable if it starts with '{{' and ends with '}}'. :param tag: String name of tag. """ return (isinstance(tag, six.string_types) and tag.strip().startswith('{{') and tag.strip().endswith('}}')) def resolve_to_callable(callable_name): """ Resolve string :callable_name: to a callable. :param callable_name: String representing callable name as registered in ramses registry or dotted import path of callable. Can be wrapped in double curly brackets, e.g. '{{my_callable}}'. """ from . import registry clean_callable_name = callable_name.replace( '{{', '').replace('}}', '').strip() try: return registry.get(clean_callable_name) except KeyError: try: from zope.dottedname.resolve import resolve return resolve(clean_callable_name) except ImportError: raise ImportError( 'Failed to load callable `{}`'.format(clean_callable_name)) def get_resource_siblings(raml_resource): """ Get siblings of :raml_resource:. :param raml_resource: Instance of ramlfications.raml.ResourceNode. """ path = raml_resource.path return [res for res in raml_resource.root.resources if res.path == path] def get_resource_children(raml_resource): """ Get children of :raml_resource:. :param raml_resource: Instance of ramlfications.raml.ResourceNode. """ path = raml_resource.path return [res for res in raml_resource.root.resources if res.parent and res.parent.path == path] def get_events_map(): """ Prepare map of event subscribers. * Extends copies of BEFORE_EVENTS and AFTER_EVENTS maps with 'set' action. * Returns map of {before/after: {action: event class(es)}} """ from nefertari import events set_keys = ('create', 'update', 'replace', 'update_many', 'register') before_events = events.BEFORE_EVENTS.copy() before_events['set'] = [before_events[key] for key in set_keys] after_events = events.AFTER_EVENTS.copy() after_events['set'] = [after_events[key] for key in set_keys] return { 'before': before_events, 'after': after_events, } @contextmanager def patch_view_model(view_cls, model_cls): """ Patches view_cls.Model with model_cls. :param view_cls: View class "Model" param of which should be patched :param model_cls: Model class which should be used to patch view_cls.Model """ original_model = view_cls.Model view_cls.Model = model_cls try: yield finally: view_cls.Model = original_model def get_route_name(resource_uri): """ Get route name from RAML resource URI. :param resource_uri: String representing RAML resource URI. :returns string: String with route name, which is :resource_uri: stripped of non-word characters. """ resource_uri = resource_uri.strip('/') resource_uri = re.sub('\W', '', resource_uri) return resource_uri def get_resource_uri(raml_resource): """ Get cleaned resource URI of RAML resource. :param raml_resource: Instance of ramlfications.raml.ResourceNode. """ return raml_resource.path.split('/')[-1].strip() ================================================ FILE: ramses/views.py ================================================ import logging import six from nefertari.view import BaseView as NefertariBaseView from nefertari.json_httpexceptions import JHTTPNotFound from .utils import patch_view_model log = logging.getLogger(__name__) """ Maps of {HTTP_method: neferteri view method name} """ collection_methods = { 'get': 'index', 'head': 'index', 'post': 'create', 'put': 'update_many', 'patch': 'update_many', 'delete': 'delete_many', 'options': 'collection_options', } item_methods = { 'get': 'show', 'head': 'show', 'post': 'create', 'put': 'replace', 'patch': 'update', 'delete': 'delete', 'options': 'item_options', } class SetObjectACLMixin(object): def set_object_acl(self, obj): """ Set object ACL on creation if not already present. """ if not obj._acl: from nefertari_guards import engine as guards_engine acl = self._factory(self.request).generate_item_acl(obj) obj._acl = guards_engine.ACLField.stringify_acl(acl) class BaseView(object): """ Base view class for other all views that defines few helper methods. Use `self.get_collection` and `self.get_item` to get access to set of objects and object respectively which are valid at current level. """ @property def clean_id_name(self): id_name = self._resource.id_name if '_' in id_name: return id_name.split('_', 1)[1] else: return id_name def set_object_acl(self, obj): pass def resolve_kw(self, kwargs): """ Resolve :kwargs: like `story_id: 1` to the form of `id: 1`. """ resolved = {} for key, value in kwargs.items(): split = key.split('_', 1) if len(split) > 1: key = split[1] resolved[key] = value return resolved def _location(self, obj): """ Get location of the `obj` Arguments: :obj: self.Model instance. """ field_name = self.clean_id_name return self.request.route_url( self._resource.uid, **{self._resource.id_name: getattr(obj, field_name)}) def _parent_queryset(self): """ Get queryset of parent view. Generated queryset is used to run queries in the current level view. """ parent = self._resource.parent if hasattr(parent, 'view'): req = self.request.blank(self.request.path) req.registry = self.request.registry req.matchdict = { parent.id_name: self.request.matchdict.get(parent.id_name)} parent_view = parent.view(parent.view._factory, req) obj = parent_view.get_item(**req.matchdict) if isinstance(self, ItemSubresourceBaseView): return prop = self._resource.collection_name return getattr(obj, prop, None) def get_collection(self, **kwargs): """ Get objects collection taking into account generated queryset of parent view. This method allows working with nested resources properly. Thus a queryset returned by this method will be a subset of its parent view's queryset, thus filtering out objects that don't belong to the parent object. """ self._query_params.update(kwargs) objects = self._parent_queryset() if objects is not None: return self.Model.filter_objects( objects, **self._query_params) return self.Model.get_collection(**self._query_params) def get_item(self, **kwargs): """ Get collection item taking into account generated queryset of parent view. This method allows working with nested resources properly. Thus an item returned by this method will belong to its parent view's queryset, thus filtering out objects that don't belong to the parent object. Returns an object from the applicable ACL. If ACL wasn't applied, it is applied explicitly. """ if six.callable(self.context): self.reload_context(es_based=False, **kwargs) objects = self._parent_queryset() if objects is not None and self.context not in objects: raise JHTTPNotFound('{}({}) not found'.format( self.Model.__name__, self._get_context_key(**kwargs))) return self.context def _get_context_key(self, **kwargs): """ Get value of `self._resource.id_name` from :kwargs: """ return str(kwargs.get(self._resource.id_name)) def reload_context(self, es_based, **kwargs): """ Reload `self.context` object into a DB or ES object. A reload is performed by getting the object ID from :kwargs: and then getting a context key item from the new instance of `self._factory` which is an ACL class used by the current view. Arguments: :es_based: Boolean. Whether to init ACL ac es-based or not. This affects the backend which will be queried - either DB or ES :kwargs: Kwargs that contain value for current resource 'id_name' key """ from .acl import BaseACL key = self._get_context_key(**kwargs) kwargs = {'request': self.request} if issubclass(self._factory, BaseACL): kwargs['es_based'] = es_based acl = self._factory(**kwargs) if acl.item_model is None: acl.item_model = self.Model self.context = acl[key] class CollectionView(BaseView): """ View that works with database and implements handlers for all available CRUD operations. """ def index(self, **kwargs): return self.get_collection() def show(self, **kwargs): return self.get_item(**kwargs) def create(self, **kwargs): obj = self.Model(**self._json_params) self.set_object_acl(obj) return obj.save(self.request) def update(self, **kwargs): obj = self.get_item(**kwargs) return obj.update(self._json_params, self.request) def replace(self, **kwargs): return self.update(**kwargs) def delete(self, **kwargs): obj = self.get_item(**kwargs) obj.delete(self.request) def delete_many(self, **kwargs): objects = self.get_collection() return self.Model._delete_many(objects, self.request) def update_many(self, **kwargs): objects = self.get_collection(**self._query_params) return self.Model._update_many( objects, self._json_params, self.request) class ESBaseView(BaseView): """ Elasticsearch base view that fetches data from ES. Implements analogues of _parent_queryset, get_collection, get_item fetching data from ES instead of database. Use `self.get_collection_es` and `self.get_item_es` to get access to the set of objects and individual object respectively which are valid at the current level. """ def _parent_queryset_es(self): """ Get queryset (list of object IDs) of parent view. The generated queryset is used to run queries in the current level's view. """ parent = self._resource.parent if hasattr(parent, 'view'): req = self.request.blank(self.request.path) req.registry = self.request.registry req.matchdict = { parent.id_name: self.request.matchdict.get(parent.id_name)} parent_view = parent.view(parent.view._factory, req) obj = parent_view.get_item_es(**req.matchdict) prop = self._resource.collection_name objects_ids = getattr(obj, prop, None) return objects_ids def get_es_object_ids(self, objects): """ Return IDs of :objects: if they are not IDs already. """ id_field = self.clean_id_name ids = [getattr(obj, id_field, obj) for obj in objects] return list(set(str(id_) for id_ in ids)) def get_collection_es(self): """ Get ES objects collection taking into account the generated queryset of parent view. This method allows working with nested resources properly. Thus a queryset returned by this method will be a subset of its parent view's queryset, thus filtering out objects that don't belong to the parent object. """ objects_ids = self._parent_queryset_es() if objects_ids is not None: objects_ids = self.get_es_object_ids(objects_ids) if not objects_ids: return [] self._query_params['id'] = objects_ids return super(ESBaseView, self).get_collection_es() def get_item_es(self, **kwargs): """ Get ES collection item taking into account generated queryset of parent view. This method allows working with nested resources properly. Thus an item returned by this method will belong to its parent view's queryset, thus filtering out objects that don't belong to the parent object. Returns an object retrieved from the applicable ACL. If an ACL wasn't applied, it is applied explicitly. """ item_id = self._get_context_key(**kwargs) objects_ids = self._parent_queryset_es() if objects_ids is not None: objects_ids = self.get_es_object_ids(objects_ids) if six.callable(self.context): self.reload_context(es_based=True, **kwargs) if (objects_ids is not None) and (item_id not in objects_ids): raise JHTTPNotFound('{}(id={}) resource not found'.format( self.Model.__name__, item_id)) return self.context class ESCollectionView(ESBaseView, CollectionView): """ View that reads data from ES. Write operations are inherited from :CollectionView: """ def index(self, **kwargs): return self.get_collection_es() def show(self, **kwargs): return self.get_item_es(**kwargs) def update(self, **kwargs): """ Explicitly reload context with DB usage to get access to complete DB object. """ self.reload_context(es_based=False, **kwargs) return super(ESCollectionView, self).update(**kwargs) def delete(self, **kwargs): """ Explicitly reload context with DB usage to get access to complete DB object. """ self.reload_context(es_based=False, **kwargs) return super(ESCollectionView, self).delete(**kwargs) def get_dbcollection_with_es(self, **kwargs): """ Get DB objects collection by first querying ES. """ es_objects = self.get_collection_es() db_objects = self.Model.filter_objects(es_objects) return db_objects def delete_many(self, **kwargs): """ Delete multiple objects from collection. First ES is queried, then the results are used to query the DB. This is done to make sure deleted objects are those filtered by ES in the 'index' method (so user deletes what he saw). """ db_objects = self.get_dbcollection_with_es(**kwargs) return self.Model._delete_many(db_objects, self.request) def update_many(self, **kwargs): """ Update multiple objects from collection. First ES is queried, then the results are used to query DB. This is done to make sure updated objects are those filtered by ES in the 'index' method (so user updates what he saw). """ db_objects = self.get_dbcollection_with_es(**kwargs) return self.Model._update_many( db_objects, self._json_params, self.request) class ItemSubresourceBaseView(BaseView): """ Base class for all subresources of collection item resources which don't represent a collection of their own. E.g. /users/{id}/profile, where 'profile' is a singular resource or /users/{id}/some_action, where the 'some_action' action may be performed when requesting this route. Subclass ItemSubresourceBaseView in your project when you want to define a subroute and view of an item route defined in RAML and generated by ramses. Use `self.get_item` to get an object on which actions are being performed. Moved into a separate class so all item subresources have a common base class, thus making checks like `isinstance(view, baseClass)` easier. Also to override `_get_context_key` to return parent resource's id_name and `get_item` to reload context on each access. """ def _get_context_key(self, **kwargs): """ Get value of `self._resource.parent.id_name` from :kwargs: """ return str(kwargs.get(self._resource.parent.id_name)) def get_item(self, **kwargs): """ Reload context on each access. """ self.reload_context(es_based=False, **kwargs) return super(ItemSubresourceBaseView, self).get_item(**kwargs) class ItemAttributeView(ItemSubresourceBaseView): """ View used to work with attribute resources. Attribute resources represent field: ListField, DictField. You may subclass ItemAttributeView in your project when you want to define custom attribute subroute and view of a item route defined in RAML and generated by ramses. """ def __init__(self, *args, **kw): super(ItemAttributeView, self).__init__(*args, **kw) self.attr = self.request.path.split('/')[-1] self.value_type = None self.unique = True def index(self, **kwargs): obj = self.get_item(**kwargs) return getattr(obj, self.attr) def create(self, **kwargs): obj = self.get_item(**kwargs) obj.update_iterables( self._json_params, self.attr, unique=self.unique, value_type=self.value_type, request=self.request) return getattr(obj, self.attr, None) class ItemSingularView(ItemSubresourceBaseView): """ View used to work with singular resources. Singular resources represent a one-to-one relationship. E.g. users/1/profile. You may subclass ItemSingularView in your project when you want to define a custom singular subroute and view of an item route defined in RAML and generated by ramses. If you decide to do so, make sure to set `self._singular_model` to a model class, instances of which will be processed by this view. """ _parent_model = None def __init__(self, *args, **kw): super(ItemSingularView, self).__init__(*args, **kw) self.attr = self.request.path.split('/')[-1] def get_item(self, **kwargs): with patch_view_model(self, self._parent_model): return super(ItemSingularView, self).get_item(**kwargs) def show(self, **kwargs): parent_obj = self.get_item(**kwargs) return getattr(parent_obj, self.attr) def create(self, **kwargs): parent_obj = self.get_item(**kwargs) obj = self.Model(**self._json_params) self.set_object_acl(obj) obj = obj.save(self.request) parent_obj.update({self.attr: obj}, self.request) return obj def update(self, **kwargs): parent_obj = self.get_item(**kwargs) obj = getattr(parent_obj, self.attr) obj.update(self._json_params, self.request) return obj def replace(self, **kwargs): return self.update(**kwargs) def delete(self, **kwargs): parent_obj = self.get_item(**kwargs) obj = getattr(parent_obj, self.attr) obj.delete(self.request) def generate_rest_view(config, model_cls, attrs=None, es_based=True, attr_view=False, singular=False): """ Generate REST view for a model class. :param model_cls: Generated DB model class. :param attr: List of strings that represent names of view methods, new generated view should support. Not supported methods are replaced with property that raises AttributeError to display MethodNotAllowed error. :param es_based: Boolean indicating if generated view should read from elasticsearch. If True - collection reads are performed from elasticsearch. Database is used for reads otherwise. Defaults to True. :param attr_view: Boolean indicating if ItemAttributeView should be used as a base class for generated view. :param singular: Boolean indicating if ItemSingularView should be used as a base class for generated view. """ valid_attrs = (list(collection_methods.values()) + list(item_methods.values())) missing_attrs = set(valid_attrs) - set(attrs) if singular: bases = [ItemSingularView] elif attr_view: bases = [ItemAttributeView] elif es_based: bases = [ESCollectionView] else: bases = [CollectionView] if config.registry.database_acls: from nefertari_guards.view import ACLFilterViewMixin bases = [SetObjectACLMixin] + bases + [ACLFilterViewMixin] bases.append(NefertariBaseView) RESTView = type('RESTView', tuple(bases), {'Model': model_cls}) def _attr_error(*args, **kwargs): raise AttributeError for attr in missing_attrs: setattr(RESTView, attr, property(_attr_error)) return RESTView ================================================ FILE: requirements.dev ================================================ mock pytest pytest-cov releases sphinx virtualenv -e git+https://github.com/ramses-tech/nefertari.git@develop#egg=nefertari -e git+https://github.com/ramses-tech/nefertari-guards.git@develop#egg=nefertari_guards -e . ================================================ FILE: setup.py ================================================ import os from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) README = open(os.path.join(here, 'README.md')).read() VERSION = open(os.path.join(here, 'VERSION')).read() requires = [ 'cryptacular', 'inflection', 'nefertari>=0.7.0', 'pyramid', 'ramlfications==0.1.8', 'six', 'transaction', ] setup(name='ramses', version=VERSION, description='Generate a RESTful API for Pyramid using RAML', long_description=README, classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Framework :: Pyramid", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ], author='Ramses Tech', author_email='hello@ramses.tech', url='https://github.com/ramses-tech/ramses', keywords='web pyramid pylons ramses raml', packages=find_packages(), include_package_data=True, zip_safe=False, install_requires=requires, tests_require=requires, test_suite="ramses", entry_points="""\ [pyramid.scaffold] ramses_starter = ramses.scaffolds:RamsesStarterTemplate """) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/fixtures.py ================================================ import pytest @pytest.fixture def clear_registry(request): from ramses import registry registry.registry.clear() @pytest.fixture def engine_mock(request): import nefertari from mock import Mock class BaseDocument(object): pass class ESBaseDocument(object): pass original_engine = nefertari.engine nefertari.engine = Mock() nefertari.engine.BaseDocument = BaseDocument nefertari.engine.ESBaseDocument = ESBaseDocument def clear(): nefertari.engine = original_engine request.addfinalizer(clear) return nefertari.engine @pytest.fixture def guards_engine_mock(request): import nefertari_guards from nefertari_guards import engine from mock import Mock class DocumentACLMixin(object): pass original_engine = engine nefertari_guards.engine = Mock() nefertari_guards.engine.DocumentACLMixin = DocumentACLMixin def clear(): nefertari_guards.engine = original_engine request.addfinalizer(clear) return nefertari_guards.engine def config_mock(): from mock import Mock config = Mock() config.registry.database_acls = False return config ================================================ FILE: tests/test_acl.py ================================================ import pytest from mock import Mock, patch, call from pyramid.security import ( Allow, Deny, Everyone, Authenticated, ALL_PERMISSIONS) from ramses import acl from .fixtures import config_mock class TestACLHelpers(object): def test_validate_permissions_all_perms(self): perms = ALL_PERMISSIONS assert acl.validate_permissions(perms) == [perms] assert acl.validate_permissions([perms]) == [perms] def test_validate_permissions_valid(self): perms = ['update', 'delete'] assert acl.validate_permissions(perms) == perms def test_validate_permissions_invalid(self): with pytest.raises(ValueError) as ex: acl.validate_permissions(['foobar']) assert 'Invalid ACL permission names' in str(ex.value) def test_parse_permissions_all_permissions(self): perms = acl.parse_permissions('all,view,create') assert perms is ALL_PERMISSIONS def test_parse_permissions_invalid_perm_name(self): with pytest.raises(ValueError) as ex: acl.parse_permissions('foo,create') expected = ('Invalid ACL permission names. Valid ' 'permissions are: ') assert expected in str(ex.value) def test_parse_permissions(self): perms = acl.parse_permissions('view') assert perms == ['view'] perms = acl.parse_permissions('view,create') assert sorted(perms) == ['create', 'view'] def test_parse_acl_no_string(self): perms = acl.parse_acl('') assert perms == [acl.ALLOW_ALL] def test_parse_acl_unknown_action(self): with pytest.raises(ValueError) as ex: acl.parse_acl('foobar admin all') assert 'Unknown ACL action: foobar' in str(ex.value) @patch.object(acl, 'parse_permissions') def test_parse_acl_multiple_values(self, mock_perms): mock_perms.return_value = 'Foo' perms = acl.parse_acl( 'allow everyone read,write;allow authenticated all') mock_perms.assert_has_calls([ call(['read', 'write']), call(['all']), ]) assert sorted(perms) == sorted([ (Allow, Everyone, 'Foo'), (Allow, Authenticated, 'Foo'), ]) @patch.object(acl, 'parse_permissions') def test_parse_acl_special_principal(self, mock_perms): mock_perms.return_value = 'Foo' perms = acl.parse_acl('allow everyone all') mock_perms.assert_called_once_with(['all']) assert perms == [(Allow, Everyone, 'Foo')] @patch.object(acl, 'parse_permissions') def test_parse_acl_group_principal(self, mock_perms): mock_perms.return_value = 'Foo' perms = acl.parse_acl('allow g:admin all') mock_perms.assert_called_once_with(['all']) assert perms == [(Allow, 'g:admin', 'Foo')] @patch.object(acl, 'resolve_to_callable') @patch.object(acl, 'parse_permissions') def test_parse_acl_callable_principal(self, mock_perms, mock_res): mock_perms.return_value = 'Foo' mock_res.return_value = 'registry callable' perms = acl.parse_acl('allow {{my_user}} all') mock_perms.assert_called_once_with(['all']) mock_res.assert_called_once_with('{{my_user}}') assert perms == [(Allow, 'registry callable', 'Foo')] @patch.object(acl, 'parse_acl') class TestGenerateACL(object): def test_no_security(self, mock_parse): config = config_mock() acl_cls = acl.generate_acl( config, model_cls='Foo', raml_resource=Mock(security_schemes=[]), es_based=True) assert acl_cls.item_model == 'Foo' assert issubclass(acl_cls, acl.BaseACL) instance = acl_cls(request=None) assert instance.es_based assert instance._collection_acl == [] assert instance._item_acl == [] assert not mock_parse.called def test_wrong_security_scheme_type(self, mock_parse): raml_resource = Mock(security_schemes=[ Mock(type='x-Foo', settings={'collection': 4, 'item': 7}) ]) config = config_mock() acl_cls = acl.generate_acl( config, model_cls='Foo', raml_resource=raml_resource, es_based=False) assert not mock_parse.called assert acl_cls.item_model == 'Foo' assert issubclass(acl_cls, acl.BaseACL) instance = acl_cls(request=None) assert not instance.es_based assert instance._collection_acl == [] assert instance._item_acl == [] def test_correct_security_scheme(self, mock_parse): raml_resource = Mock(security_schemes=[ Mock(type='x-ACL', settings={'collection': 4, 'item': 7}) ]) config = config_mock() acl_cls = acl.generate_acl( config, model_cls='Foo', raml_resource=raml_resource, es_based=False) mock_parse.assert_has_calls([ call(acl_string=4), call(acl_string=7), ]) instance = acl_cls(request=None) assert instance._collection_acl == mock_parse() assert instance._item_acl == mock_parse() assert not instance.es_based def test_database_acls_option(self, mock_parse): raml_resource = Mock(security_schemes=[ Mock(type='x-ACL', settings={'collection': 4, 'item': 7}) ]) kwargs = dict( model_cls='Foo', raml_resource=raml_resource, es_based=False, ) config = config_mock() config.registry.database_acls = False acl_cls = acl.generate_acl(config, **kwargs) assert not issubclass(acl_cls, acl.DatabaseACLMixin) config.registry.database_acls = True acl_cls = acl.generate_acl(config, **kwargs) assert issubclass(acl_cls, acl.DatabaseACLMixin) class TestBaseACL(object): def test_init(self): obj = acl.BaseACL(request='Foo') assert obj.item_model is None assert obj._collection_acl == (acl.ALLOW_ALL,) assert obj._item_acl == (acl.ALLOW_ALL,) assert obj.request == 'Foo' def test_apply_callables_no_callables(self): obj = acl.BaseACL('req') new_acl = obj._apply_callables( acl=[('foo', 'bar', 'baz')], obj='obj') assert new_acl == (('foo', 'bar', 'baz'),) @patch.object(acl, 'validate_permissions') def test_apply_callables(self, mock_meth): mock_meth.return_value = '123' principal = Mock(return_value=(7, 8, 9)) obj = acl.BaseACL('req') new_acl = obj._apply_callables( acl=[('foo', principal, 'bar')], obj='obj') assert new_acl == ((7, 8, '123'),) principal.assert_called_once_with( ace=('foo', principal, 'bar'), request='req', obj='obj') mock_meth.assert_called_once_with(9) @patch.object(acl, 'parse_permissions') def test_apply_callables_principal_returns_none(self, mock_meth): mock_meth.return_value = '123' principal = Mock(return_value=None) obj = acl.BaseACL('req') new_acl = obj._apply_callables( acl=[('foo', principal, 'bar')], obj='obj') assert new_acl == () principal.assert_called_once_with( ace=('foo', principal, 'bar'), request='req', obj='obj') assert not mock_meth.called @patch.object(acl, 'validate_permissions') def test_apply_callables_principal_returns_list(self, mock_meth): mock_meth.return_value = '123' principal = Mock(return_value=[(7, 8, 9)]) obj = acl.BaseACL('req') new_acl = obj._apply_callables( acl=[('foo', principal, 'bar')], obj='obj') assert new_acl == ((7, 8, '123'),) principal.assert_called_once_with( ace=('foo', principal, 'bar'), request='req', obj='obj') mock_meth.assert_called_once_with(9) def test_apply_callables_functional(self): obj = acl.BaseACL('req') principal = lambda ace, request, obj: [(Allow, Everyone, 'view')] new_acl = obj._apply_callables( acl=[(Deny, principal, ALL_PERMISSIONS)], ) assert new_acl == ((Allow, Everyone, ['view']),) def test_magic_acl(self): obj = acl.BaseACL('req') obj._collection_acl = [(1, 2, 3)] obj._apply_callables = Mock() result = obj.__acl__() obj._apply_callables.assert_called_once_with( acl=[(1, 2, 3)], ) assert result == obj._apply_callables() def test_item_acl(self): obj = acl.BaseACL('req') obj._item_acl = [(1, 2, 3)] obj._apply_callables = Mock() result = obj.item_acl('foobar') obj._apply_callables.assert_called_once_with( acl=[(1, 2, 3)], obj='foobar' ) assert result == obj._apply_callables() def test_magic_getitem_es_based(self): obj = acl.BaseACL('req') obj.item_db_id = Mock(return_value=42) obj.getitem_es = Mock() obj.es_based = True obj.__getitem__(1) obj.item_db_id.assert_called_once_with(1) obj.getitem_es.assert_called_once_with(42) def test_magic_getitem_db_based(self): obj = acl.BaseACL('req') obj.item_db_id = Mock(return_value=42) obj.item_model = Mock() obj.item_model.pk_field.return_value = 'id' obj.es_based = False obj.__getitem__(1) obj.item_db_id.assert_called_once_with(1) @patch('ramses.acl.ES') def test_getitem_es(self, mock_es): found_obj = Mock() es_obj = Mock() es_obj.get_item.return_value = found_obj mock_es.return_value = es_obj obj = acl.BaseACL('req') obj.item_model = Mock(__name__='Foo') obj.item_model.pk_field.return_value = 'myname' obj.item_acl = Mock() value = obj.getitem_es(key='varvar') mock_es.assert_called_with('Foo') es_obj.get_item.assert_called_once_with(id='varvar') obj.item_acl.assert_called_once_with(found_obj) assert value.__acl__ == obj.item_acl() assert value.__parent__ is obj assert value.__name__ == 'varvar' ================================================ FILE: tests/test_auth.py ================================================ import pytest from mock import Mock, patch from nefertari.utils import dictset from pyramid.security import Allow, ALL_PERMISSIONS from .fixtures import engine_mock, guards_engine_mock @pytest.mark.usefixtures('engine_mock') class TestACLAssignRegisterMixin(object): def _dummy_view(self): from ramses import auth class DummyBase(object): def register(self, *args, **kwargs): return 1 class DummyView(auth.ACLAssignRegisterMixin, DummyBase): def __init__(self, *args, **kwargs): super(DummyView, self).__init__(*args, **kwargs) self.Model = Mock() self.request = Mock(_user=Mock()) self.request.registry._model_collections = {} return DummyView def test_register_acl_present(self): DummyView = self._dummy_view() view = DummyView() view.request._user._acl = ['a'] assert view.register() == 1 assert view.request._user._acl == ['a'] def test_register_no_model_collection(self): DummyView = self._dummy_view() view = DummyView() view.Model.__name__ = 'Foo' view.request._user._acl = [] assert view.register() == 1 assert view.request._user._acl == [] def test_register_acl_set(self, guards_engine_mock): DummyView = self._dummy_view() view = DummyView() view.Model.__name__ = 'Foo' resource = Mock() view.request.registry._model_collections['Foo'] = resource view.request._user._acl = [] assert view.register() == 1 factory = resource.view._factory factory.assert_called_once_with(view.request) factory().generate_item_acl.assert_called_once_with( view.request._user) guards_engine_mock.ACLField.stringify_acl.assert_called_once_with( factory().generate_item_acl()) view.request._user.update.assert_called_once_with( {'_acl': guards_engine_mock.ACLField.stringify_acl()}) @pytest.mark.usefixtures('engine_mock') class TestSetupTicketPolicy(object): def test_no_secret(self): from ramses import auth with pytest.raises(ValueError) as ex: auth._setup_ticket_policy(config='', params={}) expected = 'Missing required security scheme settings: secret' assert expected == str(ex.value) @patch('ramses.auth.AuthTktAuthenticationPolicy') def test_params_converted(self, mock_policy): from ramses import auth params = dictset( secure=True, include_ip=True, http_only=False, wild_domain=True, debug=True, parent_domain=True, secret='my_secret_setting' ) auth_model = Mock() config = Mock() config.registry.settings = {'my_secret_setting': 12345} config.registry.auth_model = auth_model auth._setup_ticket_policy(config=config, params=params) mock_policy.assert_called_once_with( include_ip=True, secure=True, parent_domain=True, callback=auth_model.get_groups_by_userid, secret=12345, wild_domain=True, debug=True, http_only=False ) @patch('ramses.auth.AuthTktAuthenticationPolicy') def test_request_method_added(self, mock_policy): from ramses import auth config = Mock() config.registry.settings = {'my_secret': 12345} config.registry.auth_model = Mock() policy = auth._setup_ticket_policy( config=config, params={'secret': 'my_secret'}) config.add_request_method.assert_called_once_with( config.registry.auth_model.get_authuser_by_userid, 'user', reify=True) assert policy == mock_policy() @patch('ramses.auth.AuthTktAuthenticationPolicy') def test_routes_views_added(self, mock_policy): from ramses import auth config = Mock() config.registry.settings = {'my_secret': 12345} config.registry.auth_model = Mock() root = Mock() config.get_root_resource.return_value = root auth._setup_ticket_policy( config=config, params={'secret': 'my_secret'}) assert root.add.call_count == 3 login, logout, register = root.add.call_args_list login_kwargs = login[1] assert sorted(login_kwargs.keys()) == sorted([ 'view', 'prefix', 'factory']) assert login_kwargs['prefix'] == 'auth' assert login_kwargs['factory'] == 'nefertari.acl.AuthenticationACL' logout_kwargs = logout[1] assert sorted(logout_kwargs.keys()) == sorted([ 'view', 'prefix', 'factory']) assert logout_kwargs['prefix'] == 'auth' assert logout_kwargs['factory'] == 'nefertari.acl.AuthenticationACL' register_kwargs = register[1] assert sorted(register_kwargs.keys()) == sorted([ 'view', 'prefix', 'factory']) assert register_kwargs['prefix'] == 'auth' assert register_kwargs['factory'] == 'nefertari.acl.AuthenticationACL' @pytest.mark.usefixtures('engine_mock') class TestSetupApiKeyPolicy(object): @patch('ramses.auth.ApiKeyAuthenticationPolicy') def test_policy_params(self, mock_policy): from ramses import auth auth_model = Mock() config = Mock() config.registry.auth_model = auth_model policy = auth._setup_apikey_policy(config, {'foo': 'bar'}) mock_policy.assert_called_once_with( foo='bar', check=auth_model.get_groups_by_token, credentials_callback=auth_model.get_token_credentials, user_model=auth_model, ) assert policy == mock_policy() @patch('ramses.auth.ApiKeyAuthenticationPolicy') def test_routes_views_added(self, mock_policy): from ramses import auth auth_model = Mock() config = Mock() config.registry.auth_model = auth_model root = Mock() config.get_root_resource.return_value = root auth._setup_apikey_policy(config, {}) assert root.add.call_count == 3 token, reset_token, register = root.add.call_args_list token_kwargs = token[1] assert sorted(token_kwargs.keys()) == sorted([ 'view', 'prefix', 'factory']) assert token_kwargs['prefix'] == 'auth' assert token_kwargs['factory'] == 'nefertari.acl.AuthenticationACL' reset_token_kwargs = reset_token[1] assert sorted(reset_token_kwargs.keys()) == sorted([ 'view', 'prefix', 'factory']) assert reset_token_kwargs['prefix'] == 'auth' assert reset_token_kwargs['factory'] == 'nefertari.acl.AuthenticationACL' register_kwargs = register[1] assert sorted(register_kwargs.keys()) == sorted([ 'view', 'prefix', 'factory']) assert register_kwargs['prefix'] == 'auth' assert register_kwargs['factory'] == 'nefertari.acl.AuthenticationACL' @pytest.mark.usefixtures('engine_mock') class TestSetupAuthPolicies(object): def test_not_secured(self): from ramses import auth raml_data = Mock(secured_by=[None]) config = Mock() auth.setup_auth_policies(config, raml_data) assert not config.set_authentication_policy.called assert not config.set_authorization_policy.called def test_not_defined_security_scheme(self): from ramses import auth scheme = Mock() scheme.name = 'foo' raml_data = Mock(secured_by=['zoo'], security_schemes=[scheme]) with pytest.raises(ValueError) as ex: auth.setup_auth_policies('asd', raml_data) expected = 'Undefined security scheme used in `secured_by`: zoo' assert expected == str(ex.value) def test_not_supported_scheme_type(self): from ramses import auth scheme = Mock(type='asd123') scheme.name = 'foo' raml_data = Mock(secured_by=['foo'], security_schemes=[scheme]) with pytest.raises(ValueError) as ex: auth.setup_auth_policies(None, raml_data) expected = 'Unsupported security scheme type: asd123' assert expected == str(ex.value) @patch('ramses.auth.ACLAuthorizationPolicy') def test_policies_calls(self, mock_acl): from ramses import auth scheme = Mock(type='mytype', settings={'name': 'user1'}) scheme.name = 'foo' raml_data = Mock(secured_by=['foo'], security_schemes=[scheme]) config = Mock() mock_setup = Mock() with patch.dict(auth.AUTHENTICATION_POLICIES, {'mytype': mock_setup}): auth.setup_auth_policies(config, raml_data) mock_setup.assert_called_once_with(config, {'name': 'user1'}) config.set_authentication_policy.assert_called_once_with( mock_setup()) mock_acl.assert_called_once_with() config.set_authorization_policy.assert_called_once_with( mock_acl()) @pytest.mark.usefixtures('engine_mock') class TestHelperFunctions(object): def test_create_system_user_key_error(self): from ramses import auth config = Mock() config.registry.settings = {} auth.create_system_user(config) assert not config.registry.auth_model.get_or_create.called @patch('ramses.auth.transaction') @patch('ramses.auth.cryptacular') def test_create_system_user_exists(self, mock_crypt, mock_trans): from ramses import auth encoder = mock_crypt.bcrypt.BCRYPTPasswordManager() encoder.encode.return_value = '654321' config = Mock() config.registry.settings = { 'system.user': 'user12', 'system.password': '123456', 'system.email': 'user12@example.com', } config.registry.auth_model.get_or_create.return_value = (1, False) auth.create_system_user(config) assert not mock_trans.commit.called encoder.encode.assert_called_once_with('123456') config.registry.auth_model.get_or_create.assert_called_once_with( username='user12', defaults={ 'password': '654321', 'email': 'user12@example.com', 'groups': ['admin'], '_acl': [(Allow, 'g:admin', ALL_PERMISSIONS)], } ) @patch('ramses.auth.transaction') @patch('ramses.auth.cryptacular') def test_create_system_user_created(self, mock_crypt, mock_trans): from ramses import auth encoder = mock_crypt.bcrypt.BCRYPTPasswordManager() encoder.encode.return_value = '654321' config = Mock() config.registry.settings = { 'system.user': 'user12', 'system.password': '123456', 'system.email': 'user12@example.com', } config.registry.auth_model.get_or_create.return_value = ( Mock(), True) auth.create_system_user(config) mock_trans.commit.assert_called_once_with() encoder.encode.assert_called_once_with('123456') config.registry.auth_model.get_or_create.assert_called_once_with( username='user12', defaults={ 'password': '654321', 'email': 'user12@example.com', 'groups': ['admin'], '_acl': [(Allow, 'g:admin', ALL_PERMISSIONS)], } ) @patch('ramses.auth.create_system_user') def test_includeme(self, mock_create): from ramses import auth auth.includeme(config=1) mock_create.assert_called_once_with(1) ================================================ FILE: tests/test_generators.py ================================================ import pytest from mock import Mock, patch, call from ramses import generators from .fixtures import engine_mock, config_mock class TestHelperFunctions(object): @patch.object(generators, 'get_static_parent') def test_get_nefertari_parent_resource_no_parent(self, mock_get): mock_get.return_value = None assert generators._get_nefertari_parent_resource(1, 2, 3) == 3 mock_get.assert_called_once_with(1) @patch.object(generators, 'get_static_parent') def test_get_nefertari_parent_resource_parent_not_defined( self, mock_get): mock_get.return_value = Mock(path='foo') assert generators._get_nefertari_parent_resource( 1, {}, 3) == 3 mock_get.assert_called_once_with(1) @patch.object(generators, 'get_static_parent') def test_get_nefertari_parent_resource_parent_defined( self, mock_get): mock_get.return_value = Mock(path='foo') assert generators._get_nefertari_parent_resource( 1, {'foo': 'bar'}, 3) == 'bar' mock_get.assert_called_once_with(1) @patch.object(generators, 'generate_resource') def test_generate_server_no_resources(self, mock_gen): generators.generate_server(Mock(resources=None), 'foo') assert not mock_gen.called @patch.object(generators, '_get_nefertari_parent_resource') @patch.object(generators, 'generate_resource') def test_generate_server_resources_generated( self, mock_gen, mock_get): config = Mock() resources = [ Mock(path='/foo'), Mock(path='/bar'), ] generators.generate_server(Mock(resources=resources), config) assert mock_get.call_count == 2 mock_gen.assert_has_calls([ call(config, resources[0], mock_get()), call(config, resources[1], mock_get()), ]) @patch.object(generators, '_get_nefertari_parent_resource') @patch.object(generators, 'generate_resource') def test_generate_server_call_per_path( self, mock_gen, mock_get): config = Mock() resources = [ Mock(path='/foo'), Mock(path='/foo'), ] generators.generate_server(Mock(resources=resources), config) assert mock_get.call_count == 1 mock_gen.assert_called_once_with(config, resources[0], mock_get()) @pytest.mark.usefixtures('engine_mock') class TestGenerateModels(object): @patch('ramses.generators.is_dynamic_uri') def test_no_resources(self, mock_dyn): generators.generate_models(config=1, raml_resources=[]) assert not mock_dyn.called @patch('ramses.models.handle_model_generation') def test_dynamic_uri(self, mock_handle): generators.generate_models( config=1, raml_resources=[Mock(path='/{id}')]) assert not mock_handle.called @patch('ramses.models.handle_model_generation') def test_no_post_resources(self, mock_handle): generators.generate_models(config=1, raml_resources=[ Mock(path='/stories', method='get'), Mock(path='/stories', method='options'), Mock(path='/stories', method='patch'), ]) assert not mock_handle.called @patch('ramses.generators.attr_subresource') @patch('ramses.models.handle_model_generation') def test_attr_subresource(self, mock_handle, mock_attr): mock_attr.return_value = True resource = Mock(path='/stories', method='POST') generators.generate_models(config=1, raml_resources=[resource]) assert not mock_handle.called mock_attr.assert_called_once_with(resource, 'stories') @patch('ramses.generators.attr_subresource') @patch('ramses.models.handle_model_generation') def test_non_auth_model(self, mock_handle, mock_attr): mock_attr.return_value = False mock_handle.return_value = ('Foo', False) config = Mock() resource = Mock(path='/stories', method='POST') generators.generate_models( config=config, raml_resources=[resource]) mock_attr.assert_called_once_with(resource, 'stories') mock_handle.assert_called_once_with(config, resource) assert config.registry.auth_model != 'Foo' @patch('ramses.generators.attr_subresource') @patch('ramses.models.handle_model_generation') def test_auth_model(self, mock_handle, mock_attr): mock_attr.return_value = False mock_handle.return_value = ('Foo', True) config = Mock() resource = Mock(path='/stories', method='POST') generators.generate_models( config=config, raml_resources=[resource]) mock_attr.assert_called_once_with(resource, 'stories') mock_handle.assert_called_once_with(config, resource) assert config.registry.auth_model == 'Foo' class TestGenerateResource(object): def test_dynamic_root_parent(self): raml_resource = Mock(path='/foobar/{id}') parent_resource = Mock(is_root=True) config = config_mock() with pytest.raises(Exception) as ex: generators.generate_resource( config, raml_resource, parent_resource) expected = ("Top-level resources can't be dynamic and must " "represent collections instead") assert str(ex.value) == expected def test_dynamic_not_root_parent(self): raml_resource = Mock(path='/foobar/{id}') parent_resource = Mock(is_root=False) config = config_mock() new_resource = generators.generate_resource( config, raml_resource, parent_resource) assert new_resource is None @patch('ramses.generators.dynamic_part_name') @patch('ramses.generators.singular_subresource') @patch('ramses.generators.attr_subresource') @patch('ramses.models.get_existing_model') @patch('ramses.generators.generate_acl') @patch('ramses.generators.resource_view_attrs') @patch('ramses.generators.generate_rest_view') def test_full_run( self, generate_view, view_attrs, generate_acl, get_model, attr_res, singular_res, mock_dyn): mock_dyn.return_value = 'fooid' model_cls = Mock() model_cls.pk_field.return_value = 'my_id' attr_res.return_value = False singular_res.return_value = False get_model.return_value = model_cls raml_resource = Mock(path='/stories') parent_resource = Mock(is_root=False, uid=1) config = config_mock() res = generators.generate_resource( config, raml_resource, parent_resource) get_model.assert_called_once_with('Story') generate_acl.assert_called_once_with( config, model_cls=model_cls, raml_resource=raml_resource) mock_dyn.assert_called_once_with( raml_resource=raml_resource, route_name='stories', pk_field='my_id') view_attrs.assert_called_once_with(raml_resource, False) generate_view.assert_called_once_with( config, model_cls=model_cls, attrs=view_attrs(), attr_view=False, singular=False ) parent_resource.add.assert_called_once_with( 'story', 'stories', id_name='fooid', factory=generate_acl(), view=generate_view() ) assert res == parent_resource.add() @patch('ramses.generators.dynamic_part_name') @patch('ramses.generators.singular_subresource') @patch('ramses.generators.attr_subresource') @patch('ramses.models.get_existing_model') @patch('ramses.generators.generate_acl') @patch('ramses.generators.resource_view_attrs') @patch('ramses.generators.generate_rest_view') def test_full_run_singular( self, generate_view, view_attrs, generate_acl, get_model, attr_res, singular_res, mock_dyn): mock_dyn.return_value = 'fooid' model_cls = Mock() model_cls.pk_field.return_value = 'my_id' attr_res.return_value = False singular_res.return_value = True get_model.return_value = model_cls raml_resource = Mock(path='/stories') parent_resource = Mock(is_root=False, uid=1) parent_resource.view.Model.pk_field.return_value = 'other_id' config = config_mock() res = generators.generate_resource( config, raml_resource, parent_resource) get_model.assert_called_once_with('Story') generate_acl.assert_called_once_with( config, model_cls=parent_resource.view.Model, raml_resource=raml_resource) assert not mock_dyn.called view_attrs.assert_called_once_with(raml_resource, True) generate_view.assert_called_once_with( config, model_cls=parent_resource.view.Model, attrs=view_attrs(), attr_view=False, singular=True ) parent_resource.add.assert_called_once_with( 'story', factory=generate_acl(), view=generate_view() ) assert res == parent_resource.add() ================================================ FILE: tests/test_models.py ================================================ import pytest from mock import Mock, patch, call from .fixtures import engine_mock, config_mock, guards_engine_mock @pytest.mark.usefixtures('engine_mock') class TestHelperFunctions(object): @patch('ramses.models.engine') def test_get_existing_model_not_found(self, mock_eng): from ramses import models mock_eng.get_document_cls.side_effect = ValueError model_cls = models.get_existing_model('Foo') assert model_cls is None mock_eng.get_document_cls.assert_called_once_with('Foo') @patch('ramses.models.engine') def test_get_existing_model_found(self, mock_eng): from ramses import models mock_eng.get_document_cls.return_value = 1 model_cls = models.get_existing_model('Foo') assert model_cls == 1 mock_eng.get_document_cls.assert_called_once_with('Foo') @patch('ramses.models.setup_data_model') @patch('ramses.models.get_existing_model') def test_prepare_relationship_model_exists(self, mock_get, mock_set): from ramses import models config = Mock() models.prepare_relationship( config, 'Story', 'raml_resource') mock_get.assert_called_once_with('Story') assert not mock_set.called @patch('ramses.models.get_existing_model') def test_prepare_relationship_resource_not_found(self, mock_get): from ramses import models config = Mock() resource = Mock(root=Mock(resources=[ Mock(method='get', path='/stories'), Mock(method='options', path='/stories'), Mock(method='post', path='/items'), ])) mock_get.return_value = None with pytest.raises(ValueError) as ex: models.prepare_relationship(config, 'Story', resource) expected = ('Model `Story` used in relationship ' 'is not defined') assert str(ex.value) == expected @patch('ramses.models.setup_data_model') @patch('ramses.models.get_existing_model') def test_prepare_relationship_resource_found( self, mock_get, mock_set): from ramses import models config = Mock() matching_res = Mock(method='post', path='/stories') resource = Mock(root=Mock(resources=[ matching_res, Mock(method='options', path='/stories'), Mock(method='post', path='/items'), ])) mock_get.return_value = None config = config_mock() models.prepare_relationship(config, 'Story', resource) mock_set.assert_called_once_with(config, matching_res, 'Story') @patch('ramses.models.resource_schema') @patch('ramses.models.get_existing_model') def test_setup_data_model_existing_model(self, mock_get, mock_schema): from ramses import models config = Mock() mock_get.return_value = 1 mock_schema.return_value = {"foo": "bar"} model, auth_model = models.setup_data_model(config, 'foo', 'Bar') assert not auth_model assert model == 1 mock_get.assert_called_once_with('Bar') @patch('ramses.models.resource_schema') @patch('ramses.models.get_existing_model') def test_setup_data_model_existing_auth_model(self, mock_get, mock_schema): from ramses import models config = Mock() mock_get.return_value = 1 mock_schema.return_value = {"_auth_model": True} model, auth_model = models.setup_data_model(config, 'foo', 'Bar') assert auth_model assert model == 1 mock_get.assert_called_once_with('Bar') @patch('ramses.models.resource_schema') @patch('ramses.models.get_existing_model') def test_setup_data_model_no_schema(self, mock_get, mock_schema): from ramses import models config = Mock() mock_get.return_value = None mock_schema.return_value = None with pytest.raises(Exception) as ex: models.setup_data_model(config, 'foo', 'Bar') assert str(ex.value) == 'Missing schema for model `Bar`' mock_get.assert_called_once_with('Bar') mock_schema.assert_called_once_with('foo') @patch('ramses.models.resource_schema') @patch('ramses.models.generate_model_cls') @patch('ramses.models.get_existing_model') def test_setup_data_model_success(self, mock_get, mock_gen, mock_schema): from ramses import models mock_get.return_value = None mock_schema.return_value = {'field1': 'val1'} config = config_mock() model = models.setup_data_model(config, 'foo', 'Bar') mock_get.assert_called_once_with('Bar') mock_schema.assert_called_once_with('foo') mock_gen.assert_called_once_with( config, schema={'field1': 'val1'}, model_name='Bar', raml_resource='foo') assert model == mock_gen() @patch('ramses.models.setup_data_model') def test_handle_model_generation_value_err(self, mock_set): from ramses import models config = Mock() mock_set.side_effect = ValueError('strange error') config = config_mock() with pytest.raises(ValueError) as ex: raml_resource = Mock(path='/stories') models.handle_model_generation(config, raml_resource) assert str(ex.value) == 'Story: strange error' mock_set.assert_called_once_with(config, raml_resource, 'Story') @patch('ramses.models.setup_data_model') def test_handle_model_generation(self, mock_set): from ramses import models config = Mock() mock_set.return_value = ('Foo1', True) config = config_mock() raml_resource = Mock(path='/stories') model, auth_model = models.handle_model_generation( config, raml_resource) mock_set.assert_called_once_with(config, raml_resource, 'Story') assert model == 'Foo1' assert auth_model @patch('ramses.models.setup_fields_processors') @patch('ramses.models.setup_model_event_subscribers') @patch('ramses.models.registry') @pytest.mark.usefixtures('engine_mock') class TestGenerateModelCls(object): def _test_schema(self): return { 'properties': {}, '_auth_model': False, '_public_fields': ['public_field1'], '_auth_fields': ['auth_field1'], '_hidden_fields': ['hidden_field1'], '_nested_relationships': ['nested_field1'], '_nesting_depth': 3 } @patch('ramses.models.resolve_to_callable') def test_simple_case( self, mock_res, mock_reg, mock_subscribers, mock_proc): from nefertari.authentication.models import AuthModelMethodsMixin from ramses import models config = config_mock() models.engine.FloatField.reset_mock() schema = self._test_schema() schema['properties']['progress'] = { "_db_settings": { "type": "float", "required": True, "default": 0, } } mock_reg.mget.return_value = {'foo': 'bar'} model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=None) assert not auth_model assert model_cls.__name__ == 'Story' assert hasattr(model_cls, 'progress') assert model_cls.__tablename__ == 'story' assert model_cls._public_fields == ['public_field1'] assert model_cls._nesting_depth == 3 assert model_cls._auth_fields == ['auth_field1'] assert model_cls._hidden_fields == ['hidden_field1'] assert model_cls._nested_relationships == ['nested_field1'] assert model_cls.foo == 'bar' assert issubclass(model_cls, models.engine.ESBaseDocument) assert not issubclass(model_cls, AuthModelMethodsMixin) models.engine.FloatField.assert_called_once_with( default=0, required=True) mock_reg.mget.assert_called_once_with('Story') mock_subscribers.assert_called_once_with( config, model_cls, schema) mock_proc.assert_called_once_with( config, model_cls, schema) @patch('ramses.models.resolve_to_callable') def test_callable_default( self, mock_res, mock_reg, mock_subscribers, mock_proc): from ramses import models config = config_mock() models.engine.FloatField.reset_mock() schema = self._test_schema() schema['properties']['progress'] = { "_db_settings": { "type": "float", "default": "{{foobar}}", } } mock_res.return_value = 1 model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=None) models.engine.FloatField.assert_called_with( default=1, required=False) mock_res.assert_called_once_with('{{foobar}}') def test_auth_model(self, mock_reg, mock_subscribers, mock_proc): from nefertari.authentication.models import AuthModelMethodsMixin from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['progress'] = {'_db_settings': {}} schema['_auth_model'] = True mock_reg.mget.return_value = {'foo': 'bar'} model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=None) assert auth_model assert issubclass(model_cls, AuthModelMethodsMixin) def test_database_acls_option( self, mock_reg, mock_subscribers, mock_proc, guards_engine_mock): from ramses import models schema = self._test_schema() schema['properties']['progress'] = {'_db_settings': {}} schema['_auth_model'] = True mock_reg.mget.return_value = {'foo': 'bar'} config = config_mock() config.registry.database_acls = False model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story1', raml_resource=None) assert not issubclass(model_cls, guards_engine_mock.DocumentACLMixin) config.registry.database_acls = True model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story2', raml_resource=None) assert issubclass(model_cls, guards_engine_mock.DocumentACLMixin) def test_db_based_model(self, mock_reg, mock_subscribers, mock_proc): from nefertari.authentication.models import AuthModelMethodsMixin from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['progress'] = {'_db_settings': {}} mock_reg.mget.return_value = {'foo': 'bar'} model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=None, es_based=False) assert issubclass(model_cls, models.engine.BaseDocument) assert not issubclass(model_cls, models.engine.ESBaseDocument) assert not issubclass(model_cls, AuthModelMethodsMixin) def test_no_db_settings(self, mock_reg, mock_subscribers, mock_proc): from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['progress'] = {'type': 'pickle'} mock_reg.mget.return_value = {'foo': 'bar'} model_cls, auth_model = models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=None, es_based=False) assert not models.engine.PickleField.called def test_unknown_field_type( self, mock_reg, mock_subscribers, mock_proc): from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['progress'] = { '_db_settings': {'type': 'foobar'}} mock_reg.mget.return_value = {'foo': 'bar'} with pytest.raises(ValueError) as ex: models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=None) assert str(ex.value) == 'Unknown type: foobar' @patch('ramses.models.prepare_relationship') def test_relationship_field( self, mock_prep, mock_reg, mock_subscribers, mock_proc): from ramses import models config = Mock() schema = self._test_schema() schema['properties']['progress'] = { '_db_settings': { 'type': 'relationship', 'document': 'FooBar', } } mock_reg.mget.return_value = {'foo': 'bar'} config = config_mock() models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=1) mock_prep.assert_called_once_with( config, 'FooBar', 1) def test_foreignkey_field( self, mock_reg, mock_subscribers, mock_proc): from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['progress'] = { "_db_settings": { "type": "foreign_key", "ref_column_type": "string" } } mock_reg.mget.return_value = {'foo': 'bar'} models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=1) models.engine.ForeignKeyField.assert_called_once_with( required=False, ref_column_type=models.engine.StringField) def test_list_field(self, mock_reg, mock_subscribers, mock_proc): from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['progress'] = { "_db_settings": { "type": "list", "item_type": "integer" } } mock_reg.mget.return_value = {'foo': 'bar'} models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=1) models.engine.ListField.assert_called_once_with( required=False, item_type=models.engine.IntegerField) def test_duplicate_field_name( self, mock_reg, mock_subscribers, mock_proc): from ramses import models config = config_mock() schema = self._test_schema() schema['properties']['_public_fields'] = { '_db_settings': {'type': 'interval'}} mock_reg.mget.return_value = {'foo': 'bar'} models.generate_model_cls( config, schema=schema, model_name='Story', raml_resource=1) assert not models.engine.IntervalField.called class TestSubscribersSetup(object): @patch('ramses.models.resolve_to_callable') @patch('ramses.models.get_events_map') def test_setup_model_event_subscribers(self, mock_get, mock_resolve): from ramses import models mock_get.return_value = {'before': {'index': 'eventcls'}} mock_resolve.return_value = 1 config = Mock() model_cls = 'mymodel' schema = { '_event_handlers': { 'before_index': ['func1', 'func2'] } } models.setup_model_event_subscribers(config, model_cls, schema) mock_get.assert_called_once_with() mock_resolve.assert_has_calls([call('func1'), call('func2')]) config.subscribe_to_events.assert_has_calls([ call(mock_resolve(), ['eventcls'], model='mymodel'), call(mock_resolve(), ['eventcls'], model='mymodel'), ]) @patch('ramses.models.resolve_to_callable') @patch('ramses.models.engine') def test_setup_fields_processors(self, mock_eng, mock_resolve): from ramses import models config = Mock() schema = { 'properties': { 'stories': { "_db_settings": { "type": "relationship", "document": "Story", "backref_name": "owner", }, "_processors": ["lowercase"], "_backref_processors": ["backref_lowercase"] } } } models.setup_fields_processors(config, 'mymodel', schema) mock_resolve.assert_has_calls([ call('lowercase'), call('backref_lowercase')]) mock_eng.get_document_cls.assert_called_once_with('Story') config.add_field_processors.assert_has_calls([ call([mock_resolve()], model='mymodel', field='stories'), call([mock_resolve()], model=mock_eng.get_document_cls(), field='owner'), ]) @patch('ramses.models.resolve_to_callable') @patch('ramses.models.engine') def test_setup_fields_processors_backref_not_rel( self, mock_eng, mock_resolve): from ramses import models config = Mock() schema = { 'properties': { 'stories': { "_db_settings": { "type": "wqeqweqwe", "document": "Story", "backref_name": "owner", }, "_backref_processors": ["backref_lowercase"] } } } models.setup_fields_processors(config, 'mymodel', schema) assert not config.add_field_processors.called @patch('ramses.models.resolve_to_callable') @patch('ramses.models.engine') def test_setup_fields_processors_backref_no_doc( self, mock_eng, mock_resolve): from ramses import models config = Mock() schema = { 'properties': { 'stories': { "_db_settings": { "type": "relationship", "backref_name": "owner", }, "_backref_processors": ["backref_lowercase"] } } } models.setup_fields_processors(config, 'mymodel', schema) assert not config.add_field_processors.called @patch('ramses.models.resolve_to_callable') @patch('ramses.models.engine') def test_setup_fields_processors_backref_no_backname( self, mock_eng, mock_resolve): from ramses import models config = Mock() schema = { 'properties': { 'stories': { "_db_settings": { "type": "relationship", "document": "Story", }, "_backref_processors": ["backref_lowercase"] } } } models.setup_fields_processors(config, 'mymodel', schema) assert not config.add_field_processors.called ================================================ FILE: tests/test_registry.py ================================================ import pytest from .fixtures import clear_registry from ramses import registry @pytest.mark.usefixtures('clear_registry') class TestRegistry(object): def test_add_decorator(self): @registry.add def foo(*args, **kwargs): return args, kwargs assert registry.registry['foo'] is foo assert list(registry.registry.keys()) == ['foo'] def test_add_decorator_with_name(self): @registry.add('bar') def foo(*args, **kwargs): return args, kwargs assert registry.registry['bar'] is foo assert list(registry.registry.keys()) == ['bar'] def test_add_arbitrary_object(self): registry.add('foo', 1) registry.add('bar', 2) assert registry.registry['foo'] == 1 assert registry.registry['bar'] == 2 assert sorted(registry.registry.keys()) == ['bar', 'foo'] def test_get(self): registry.registry['foo'] = 1 assert registry.get('foo') == 1 def test_get_error(self): assert not list(registry.registry.keys()) with pytest.raises(KeyError) as ex: registry.get('foo') assert 'is not registered in ramses registry' in str(ex.value) def test_mget(self): registry.registry['Foo.bar'] = 1 registry.registry['Foo.zoo'] = 2 assert registry.mget('FoO') == {'bar': 1, 'zoo': 2} def test_mget_not_existing(self): registry.registry['Foo.bar'] = 1 registry.registry['Foo.zoo'] = 2 assert registry.mget('asdasdasd') == {} ================================================ FILE: tests/test_utils.py ================================================ import pytest from mock import Mock, patch from ramses import utils class TestUtils(object): def test_contenttypes(self): assert utils.ContentTypes.JSON == 'application/json' assert utils.ContentTypes.TEXT_XML == 'text/xml' assert utils.ContentTypes.MULTIPART_FORMDATA == \ 'multipart/form-data' assert utils.ContentTypes.FORM_URLENCODED == \ 'application/x-www-form-urlencoded' def test_convert_schema_json(self): schema = utils.convert_schema({'foo': 'bar'}, 'application/json') assert schema == {'foo': 'bar'} def test_convert_schema_json_error(self): with pytest.raises(TypeError) as ex: utils.convert_schema('foo', 'application/json') assert 'Schema is not a valid JSON' in str(ex.value) def test_convert_schema_xml(self): assert utils.convert_schema({'foo': 'bar'}, 'text/xml') is None def test_is_dynamic_uri(self): assert utils.is_dynamic_uri('/{id}') assert not utils.is_dynamic_uri('/collection') def test_clean_dynamic_uri(self): clean = utils.clean_dynamic_uri('/{item_id}') assert clean == 'item_id' def test_generate_model_name(self): resource = Mock(path='/zoo/alien-users') model_name = utils.generate_model_name(resource) assert model_name == 'AlienUser' @patch.object(utils, 'get_resource_children') def test_dynamic_part_name(self, get_children): get_children.return_value = [ Mock(path='/items'), Mock(path='/{myid}')] resource = Mock() part_name = utils.dynamic_part_name( resource, 'stories', 'default_id') assert part_name == 'stories_myid' get_children.assert_called_once_with(resource) @patch.object(utils, 'get_resource_children') def test_dynamic_part_name_no_dynamic(self, get_children): get_children.return_value = [Mock(path='/items')] resource = Mock() part_name = utils.dynamic_part_name( resource, 'stories', 'default_id') assert part_name == 'stories_default_id' get_children.assert_called_once_with(resource) @patch.object(utils, 'get_resource_children') def test_dynamic_part_name_no_resources(self, get_children): get_children.return_value = [] resource = Mock(resources=None) part_name = utils.dynamic_part_name( resource, 'stories', 'default_id') assert part_name == 'stories_default_id' get_children.assert_called_once_with(resource) def test_extract_dynamic_part(self): assert utils.extract_dynamic_part('/stories/{id}/foo') == 'id' assert utils.extract_dynamic_part('/stories/{id}') == 'id' def test_extract_dynamic_part_fail(self): assert utils.extract_dynamic_part('/stories/id') is None def _get_mock_method_resources(self, *methods): return [Mock(method=meth) for meth in methods] @patch.object(utils, 'get_resource_children') @patch.object(utils, 'get_resource_siblings') def test_resource_view_attrs_no_dynamic_subres(self, get_sib, get_child): get_child.return_value = [] get_sib.return_value = self._get_mock_method_resources( 'get', 'post', 'put', 'patch', 'delete') resource = Mock() attrs = utils.resource_view_attrs(resource, singular=False) get_sib.assert_called_once_with(resource) get_child.assert_called_once_with(resource) assert attrs == set(['create', 'delete_many', 'index', 'update_many']) @patch.object(utils, 'get_resource_children') @patch.object(utils, 'get_resource_siblings') def test_resource_view_attrs_dynamic_subres(self, get_sib, get_child): get_child.return_value = self._get_mock_method_resources( 'get', 'put', 'patch', 'delete') get_sib.return_value = self._get_mock_method_resources( 'get', 'post', 'put', 'patch', 'delete') resource = Mock() attrs = utils.resource_view_attrs(resource, singular=False) get_sib.assert_called_once_with(resource) get_child.assert_called_once_with(resource) assert attrs == set([ 'create', 'delete_many', 'index', 'update_many', 'show', 'update', 'delete', 'replace' ]) @patch.object(utils, 'get_resource_children') @patch.object(utils, 'get_resource_siblings') def test_resource_view_attrs_singular(self, get_sib, get_child): get_child.return_value = [] get_sib.return_value = self._get_mock_method_resources( 'get', 'post', 'put', 'patch', 'delete') resource = Mock() attrs = utils.resource_view_attrs(resource, singular=True) get_sib.assert_called_once_with(resource) get_child.assert_called_once_with(resource) assert attrs == set(['create', 'delete', 'show', 'update', 'replace']) @patch.object(utils, 'get_resource_children') @patch.object(utils, 'get_resource_siblings') def test_resource_view_attrs_no_subresources(self, get_sib, get_child): child_res = self._get_mock_method_resources('get') child_res[0].path = '/items' get_child.return_value = child_res get_sib.return_value = self._get_mock_method_resources( 'get', 'post', 'put', 'patch', 'delete') resource = Mock() attrs = utils.resource_view_attrs(resource, singular=False) get_sib.assert_called_once_with(resource) get_child.assert_called_once_with(resource) assert attrs == set(['create', 'delete_many', 'index', 'update_many']) @patch.object(utils, 'get_resource_children') @patch.object(utils, 'get_resource_siblings') def test_resource_view_attrs_no_methods(self, get_sib, get_child): get_sib.return_value = [] get_child.return_value = [] resource = Mock() attrs = utils.resource_view_attrs(resource, singular=False) get_sib.assert_called_once_with(resource) get_child.assert_called_once_with(resource) assert attrs == set() @patch.object(utils, 'get_resource_children') @patch.object(utils, 'get_resource_siblings') def test_resource_view_attrs_not_supported_method( self, get_sib, get_child): get_sib.return_value = [] get_child.return_value = self._get_mock_method_resources( 'nice_method') resource = Mock() attrs = utils.resource_view_attrs(resource, singular=False) assert attrs == set() def test_resource_schema_no_body(self): resource = Mock(body=None) with pytest.raises(ValueError) as ex: utils.resource_schema(resource) expected = 'RAML resource has no body to setup database' assert expected in str(ex.value) def test_resource_schema_no_schemas(self): resource = Mock(body=[Mock(schema=None), Mock(schema='')]) assert utils.resource_schema(resource) is None def test_resource_schema_success(self): resource = Mock(body=[ Mock(schema={'foo': 'bar'}, mime_type=utils.ContentTypes.JSON) ]) assert utils.resource_schema(resource) == {'foo': 'bar'} def test_is_dynamic_resource_no_resource(self): assert not utils.is_dynamic_resource(None) def test_is_dynamic_resource_dynamic(self): resource = Mock(path='/{id}') assert utils.is_dynamic_resource(resource) def test_is_dynamic_resource_not_dynamic(self): resource = Mock(path='/stories') assert not utils.is_dynamic_resource(resource) def test_get_static_parent(self): parent = Mock(path='/stories', method='post') resource = Mock(path='/{id}') resource.parent = parent assert utils.get_static_parent(resource, method='post') is parent def test_get_static_parent_none(self): resource = Mock(path='/{id}') resource.parent = None assert utils.get_static_parent(resource, method='post') is None def test_get_static_parent_wrong_parent_method(self): root = Mock(resources=[ Mock(path='/stories', method='options'), Mock(path='/users', method='post'), Mock(path='/stories', method='post'), ]) parent = Mock(path='/stories', method='get', root=root) resource = Mock(path='/{id}') resource.parent = parent res = utils.get_static_parent(resource, method='post') assert res.method == 'post' assert res.path == '/stories' def test_get_static_parent_without_method_parent_present(self): root = Mock(resources=[ Mock(path='/stories', method='options'), Mock(path='/stories', method='post'), ]) parent = Mock(path='/stories', method='get', root=root) resource = Mock(path='/{id}') resource.parent = parent res = utils.get_static_parent(resource) assert res.method == 'get' assert res.path == '/stories' def test_get_static_parent_none_found_in_root(self): root = Mock(resources=[ Mock(path='/stories', method='get'), ]) parent = Mock(path='/stories', method='options', root=root) resource = Mock(path='/{id}') resource.parent = parent assert utils.get_static_parent(resource, method='post') is None @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_attr_subresource_no_static_parent(self, mock_schema, mock_par): mock_par.return_value = None assert not utils.attr_subresource('foo', 1) mock_par.assert_called_once_with('foo', method='POST') @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_attr_subresource_no_schema(self, mock_schema, mock_par): parent = Mock() mock_par.return_value = parent mock_schema.return_value = None assert not utils.attr_subresource('foo', 1) mock_par.assert_called_once_with('foo', method='POST') mock_schema.assert_called_once_with(parent) @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_attr_subresource_not_attr(self, mock_schema, mock_par): parent = Mock() mock_par.return_value = parent mock_schema.return_value = { 'properties': { 'route_name': { '_db_settings': { 'type': 'string' } } } } assert not utils.attr_subresource('resource', 'route_name') mock_par.assert_called_once_with('resource', method='POST') mock_schema.assert_called_once_with(parent) @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_attr_subresource_dict(self, mock_schema, mock_par): parent = Mock() mock_par.return_value = parent mock_schema.return_value = { 'properties': { 'route_name': { '_db_settings': { 'type': 'dict' } }, 'route_name2': { '_db_settings': { 'type': 'list' } } } } assert utils.attr_subresource('resource', 'route_name') mock_par.assert_called_once_with('resource', method='POST') mock_schema.assert_called_once_with(parent) assert utils.attr_subresource('resource', 'route_name2') @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_singular_subresource_no_static_parent(self, mock_schema, mock_par): mock_par.return_value = None assert not utils.singular_subresource('foo', 1) mock_par.assert_called_once_with('foo', method='POST') @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_singular_subresource_no_schema(self, mock_schema, mock_par): parent = Mock() mock_par.return_value = parent mock_schema.return_value = None assert not utils.singular_subresource('foo', 1) mock_par.assert_called_once_with('foo', method='POST') mock_schema.assert_called_once_with(parent) @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_singular_subresource_not_attr(self, mock_schema, mock_par): parent = Mock() mock_par.return_value = parent mock_schema.return_value = { 'properties': { 'route_name': { '_db_settings': { 'type': 'string' } } } } assert not utils.singular_subresource('resource', 'route_name') mock_par.assert_called_once_with('resource', method='POST') mock_schema.assert_called_once_with(parent) @patch('ramses.utils.get_static_parent') @patch('ramses.utils.resource_schema') def test_singular_subresource_dict(self, mock_schema, mock_par): parent = Mock() mock_par.return_value = parent mock_schema.return_value = { 'properties': { 'route_name': { '_db_settings': { 'type': 'relationship', 'uselist': False } }, } } assert utils.singular_subresource('resource', 'route_name') mock_par.assert_called_once_with('resource', method='POST') mock_schema.assert_called_once_with(parent) def test_is_callable_tag_not_str(self): assert not utils.is_callable_tag(1) assert not utils.is_callable_tag(None) def test_is_callable_tag_not_tag(self): assert not utils.is_callable_tag('foobar') def test_is_callable_tag(self): assert utils.is_callable_tag('{{foobar}}') def test_resolve_to_callable_not_found(self): with pytest.raises(ImportError) as ex: utils.resolve_to_callable('{{foobar}}') assert str(ex.value) == 'Failed to load callable `foobar`' def test_resolve_to_callable_registry(self): from ramses import registry @registry.add def foo(): pass func = utils.resolve_to_callable('{{foo}}') assert func is foo func = utils.resolve_to_callable('foo') assert func is foo def test_resolve_to_callable_dotted_path(self): from datetime import datetime func = utils.resolve_to_callable('{{datetime.datetime}}') assert func is datetime func = utils.resolve_to_callable('datetime.datetime') assert func is datetime def test_get_events_map(self): from nefertari import events events_map = utils.get_events_map() after, before = events_map['after'], events_map['before'] after_set, before_set = after.pop('set'), before.pop('set') assert sorted(events.BEFORE_EVENTS.keys()) == sorted( before.keys()) assert sorted(events.AFTER_EVENTS.keys()) == sorted( after.keys()) assert after_set == [ events.AfterCreate, events.AfterUpdate, events.AfterReplace, events.AfterUpdateMany, events.AfterRegister, ] assert before_set == [ events.BeforeCreate, events.BeforeUpdate, events.BeforeReplace, events.BeforeUpdateMany, events.BeforeRegister, ] def test_patch_view_model(self): view_cls = Mock() model1 = Mock() model2 = Mock() view_cls.Model = model1 with utils.patch_view_model(view_cls, model2): view_cls.Model() assert view_cls.Model is model1 assert not model1.called model2.assert_called_once_with() def test_get_route_name(self): resource_uri = '/foo-=-=-=-123' assert utils.get_route_name(resource_uri) == 'foo123' def test_get_resource_uri(self): resource = Mock(path='/foobar/zoo ') assert utils.get_resource_uri(resource) == 'zoo' ================================================ FILE: tests/test_views.py ================================================ import pytest from mock import Mock, patch from nefertari.json_httpexceptions import ( JHTTPNotFound, JHTTPMethodNotAllowed) from nefertari.view import BaseView from ramses import views from .fixtures import config_mock, guards_engine_mock class ViewTestBase(object): view_cls = None view_kwargs = dict( context={}, _query_params={'foo': 'bar'}, _json_params={'foo2': 'bar2'}, ) request_kwargs = dict( method='GET', accept=[''], ) def _test_view(self): class View(self.view_cls, BaseView): _json_encoder = 'foo' request = Mock(**self.request_kwargs) return View(request=request, **self.view_kwargs) class TestSetObjectACLMixin(object): def test_set_object_acl(self, guards_engine_mock): view = views.SetObjectACLMixin() view.request = 'foo' view._factory = Mock() obj = Mock(_acl=None) view.set_object_acl(obj) view._factory.assert_called_once_with(view.request) view._factory().generate_item_acl.assert_called_once_with(obj) field = guards_engine_mock.ACLField field.stringify_acl.assert_called_once_with( view._factory().generate_item_acl()) assert obj._acl == field.stringify_acl() class TestBaseView(ViewTestBase): view_cls = views.BaseView def test_init(self): view = self._test_view() assert view._query_params['_limit'] == 20 def test_clean_id_name(self): view = self._test_view() view._resource = Mock(id_name='foo') assert view.clean_id_name == 'foo' view._resource = Mock(id_name='foo_bar') assert view.clean_id_name == 'bar' def test_resolve_kw(self): view = self._test_view() kwargs = {'foo_bar_qoo': 1, 'arg_val': 4, 'q': 3} assert view.resolve_kw(kwargs) == {'bar_qoo': 1, 'val': 4, 'q': 3} def test_location(self): view = self._test_view() view._resource = Mock(id_name='myid', uid='items') view._location(Mock(myid=123)) view.request.route_url.assert_called_once_with( 'items', myid=123) def test_location_split_id(self): view = self._test_view() view._resource = Mock(id_name='items_myid', uid='items') view._location(Mock(myid=123)) view.request.route_url.assert_called_once_with( 'items', items_myid=123) def test_get_collection_has_parent(self): view = self._test_view() view._parent_queryset = Mock(return_value=[1, 2, 3]) view.Model = Mock() view.get_collection(name='ok') view._parent_queryset.assert_called_once_with() view.Model.filter_objects.assert_called_once_with( [1, 2, 3], _limit=20, foo='bar', name='ok') def test_get_collection_has_parent_empty_queryset(self): view = self._test_view() view._parent_queryset = Mock(return_value=[]) view.Model = Mock() view.get_collection(name='ok') view._parent_queryset.assert_called_once_with() view.Model.filter_objects.assert_called_once_with( [], _limit=20, foo='bar', name='ok') def test_get_collection_no_parent(self): view = self._test_view() view._parent_queryset = Mock(return_value=None) view.Model = Mock() view.get_collection(name='ok') view._parent_queryset.assert_called_once_with() assert not view.Model.filter_objects.called view.Model.get_collection.assert_called_once_with( _limit=20, foo='bar', name='ok') def test_get_item_no_parent(self): view = self._test_view() view._parent_queryset = Mock(return_value=None) view.context = 1 assert view.get_item(name='wqe') == 1 def test_get_item_not_found_in_parent(self): view = self._test_view() view.Model = Mock(__name__='foo') view._get_context_key = Mock(return_value='123123') view._parent_queryset = Mock(return_value=[2, 3]) view.context = 1 with pytest.raises(JHTTPNotFound): view.get_item(name='wqe') def test_get_item_found_in_parent(self): view = self._test_view() view._parent_queryset = Mock(return_value=[1, 3]) view.context = 1 assert view.get_item(name='wqe') == 1 def test_get_item_found_in_parent_context_callable(self): func = lambda x: x view = self._test_view() view._parent_queryset = Mock(return_value=[func, 3]) view.reload_context = Mock() view.context = func assert view.get_item(name='wqe') is view.context view.reload_context.assert_called_once_with( es_based=False, name='wqe') def test_get_context_key(self): view = self._test_view() view._resource = Mock(id_name='foo') assert view._get_context_key(foo='bar') == 'bar' def test_parent_queryset(self): from pyramid.config import Configurator from ramses.acl import BaseACL config = Configurator() config.include('nefertari') root = config.get_root_resource() class View(self.view_cls, BaseView): _json_encoder = 'foo' user = root.add( 'user', 'users', id_name='username', view=View, factory=BaseACL) user.add( 'story', 'stories', id_name='prof_id', view=View, factory=BaseACL) view_cls = root.resource_map['user:story'].view view_cls._json_encoder = 'foo' request = Mock( registry=Mock(), path='/foo/foo', matchdict={'username': 'user12', 'prof_id': 4}, accept=[''], method='GET' ) request.params.mixed.return_value = {'foo1': 'bar1'} request.blank.return_value = request stories_view = view_cls( request=request, context={}, _query_params={'foo1': 'bar1'}, _json_params={'foo2': 'bar2'},) parent_view = stories_view._resource.parent.view with patch.object(parent_view, 'get_item') as get_item: parent_view.get_item = get_item result = stories_view._parent_queryset() get_item.assert_called_once_with(username='user12') assert result == get_item().stories def test_reload_context(self): class Factory(dict): item_model = None def __getitem__(self, key): return key view = self._test_view() view._factory = Factory view._get_context_key = Mock(return_value='foo') view.reload_context(es_based=False, arg='asd') view._get_context_key.assert_called_once_with(arg='asd') assert view.context == 'foo' class TestCollectionView(ViewTestBase): view_cls = views.CollectionView def test_index(self): view = self._test_view() view.get_collection = Mock() resp = view.index(foo='bar') view.get_collection.assert_called_once_with() assert resp == view.get_collection() def test_show(self): view = self._test_view() view.get_item = Mock() resp = view.show(foo='bar') view.get_item.assert_called_once_with(foo='bar') assert resp == view.get_item() def test_create(self): view = self._test_view() view.set_object_acl = Mock() view.request.registry._root_resources = { 'foo': Mock(auth=False) } view.Model = Mock() obj = Mock() obj.to_dict.return_value = {'id': 1} view.Model().save.return_value = obj view._location = Mock(return_value='/sadasd') resp = view.create(foo='bar') view.Model.assert_called_with(foo2='bar2') view.Model().save.assert_called_with(view.request) assert view.set_object_acl.call_count == 1 assert resp == view.Model().save() def test_update(self): view = self._test_view() view.get_item = Mock() view._location = Mock(return_value='/sadasd') resp = view.update(foo=1) view.get_item.assert_called_once_with(foo=1) view.get_item().update.assert_called_once_with( {'foo2': 'bar2'}, view.request) assert resp == view.get_item().update() def test_replace(self): view = self._test_view() view.update = Mock() resp = view.replace(foo=1) view.update.assert_called_once_with(foo=1) assert resp == view.update() def test_delete(self): view = self._test_view() view.get_item = Mock() resp = view.delete(foo=1) view.get_item.assert_called_once_with(foo=1) view.get_item().delete.assert_called_once_with( view.request) assert resp is None def test_delete_many(self): view = self._test_view() view.Model = Mock(__name__='Mock') view.Model._delete_many.return_value = 123 view.get_collection = Mock() resp = view.delete_many(foo=1) view.get_collection.assert_called_once_with() view.Model._delete_many.assert_called_once_with( view.get_collection(), view.request) assert resp == 123 def test_update_many(self): view = self._test_view() view.Model = Mock(__name__='Mock') view.Model._update_many.return_value = 123 view.get_collection = Mock() resp = view.update_many(qoo=1) view.get_collection.assert_called_once_with(_limit=20, foo='bar') view.Model._update_many.assert_called_once_with( view.get_collection(), {'foo2': 'bar2'}, view.request) assert resp == 123 class TestESBaseView(ViewTestBase): view_cls = views.ESBaseView def test_parent_queryset_es(self): from pyramid.config import Configurator from ramses.acl import BaseACL class View(self.view_cls, BaseView): _json_encoder = 'foo' config = Configurator() config.include('nefertari') root = config.get_root_resource() user = root.add( 'user', 'users', id_name='username', view=View, factory=BaseACL) user.add( 'story', 'stories', id_name='prof_id', view=View, factory=BaseACL) view_cls = root.resource_map['user:story'].view view_cls._json_encoder = 'foo' request = Mock( registry=Mock(), path='/foo/foo', matchdict={'username': 'user12', 'prof_id': 4}, accept=[''], method='GET' ) request.params.mixed.return_value = {'foo1': 'bar1'} request.blank.return_value = request stories_view = view_cls( request=request, context={}, _query_params={'foo1': 'bar1'}, _json_params={'foo2': 'bar2'},) parent_view = stories_view._resource.parent.view with patch.object(parent_view, 'get_item_es') as get_item_es: parent_view.get_item_es = get_item_es result = stories_view._parent_queryset_es() get_item_es.assert_called_once_with(username='user12') assert result == get_item_es().stories def test_get_es_object_ids(self): view = self._test_view() view._resource = Mock(id_name='foobar') objects = [Mock(foobar=4), Mock(foobar=7)] assert sorted(view.get_es_object_ids(objects)) == ['4', '7'] @patch('nefertari.elasticsearch.ES') def test_get_collection_es_no_parent(self, mock_es): mock_es.settings.asbool.return_value = False view = self._test_view() view._parent_queryset_es = Mock(return_value=None) view.Model = Mock(__name__='Foo') view.get_collection_es() mock_es.assert_called_once_with('Foo') mock_es().get_collection.assert_called_once_with( _limit=20, foo='bar') @patch('nefertari.elasticsearch.ES') def test_get_collection_es_parent_no_obj_ids(self, mock_es): mock_es.settings.asbool.return_value = False view = self._test_view() view._parent_queryset_es = Mock(return_value=[1, 2]) view.Model = Mock(__name__='Foo') view.get_es_object_ids = Mock(return_value=None) result = view.get_collection_es() assert not mock_es().get_collection.called assert result == [] @patch('nefertari.elasticsearch.ES') def test_get_collection_es_parent_with_ids(self, mock_es): mock_es.settings.asbool.return_value = False view = self._test_view() view._parent_queryset_es = Mock(return_value=['obj1', 'obj2']) view.Model = Mock(__name__='Foo') view.get_es_object_ids = Mock(return_value=[1, 2]) view.get_collection_es() view.get_es_object_ids.assert_called_once_with(['obj1', 'obj2']) mock_es().get_collection.assert_called_once_with( _limit=20, foo='bar', id=[1, 2]) def test_get_item_es_no_parent(self): view = self._test_view() view._get_context_key = Mock(return_value=1) view._parent_queryset_es = Mock(return_value=None) view.reload_context = Mock() view.context = 'foo' resp = view.get_item_es(a=4) view._get_context_key.assert_called_once_with(a=4) view._parent_queryset_es.assert_called_once_with() assert not view.reload_context.called assert resp == 'foo' def test_get_item_es_matching_id(self): view = self._test_view() view._get_context_key = Mock(return_value=1) view._parent_queryset_es = Mock(return_value=['obj1', 'obj2']) view.get_es_object_ids = Mock(return_value=[1, 2]) view.reload_context = Mock() view.context = 'foo' resp = view.get_item_es(a=4) view.get_es_object_ids.assert_called_once_with(['obj1', 'obj2']) view._get_context_key.assert_called_once_with(a=4) view._parent_queryset_es.assert_called_once_with() assert not view.reload_context.called assert resp == 'foo' def test_get_item_es_not_matching_id(self): view = self._test_view() view._get_context_key = Mock(return_value=1) view._parent_queryset_es = Mock(return_value=['obj1', 'obj2']) view.get_es_object_ids = Mock(return_value=[2, 3]) view.reload_context = Mock() view.Model = Mock(__name__='Foo') view.context = 'foo' with pytest.raises(JHTTPNotFound) as ex: view.get_item_es(a=4) assert 'Foo(id=1) resource not found' in str(ex.value) def test_get_item_es_callable_context(self): view = self._test_view() view._get_context_key = Mock(return_value=1) view._parent_queryset_es = Mock(return_value=['obj1', 'obj2']) view.get_es_object_ids = Mock(return_value=[1, 2]) view.reload_context = Mock() view.context = lambda x: x resp = view.get_item_es(a=4) view.reload_context.assert_called_once_with(es_based=True, a=4) assert resp == view.context class TestESCollectionView(ViewTestBase): view_cls = views.ESCollectionView def test_index(self): view = self._test_view() view.aggregate = Mock(side_effect=KeyError) view.get_collection_es = Mock() resp = view.index(foo=1) view.get_collection_es.assert_called_once_with() assert resp == view.get_collection_es() def test_show(self): view = self._test_view() view.get_item_es = Mock() resp = view.show(foo=1) view.get_item_es.assert_called_once_with(foo=1) assert resp == view.get_item_es() def test_update(self): view = self._test_view() view.get_item = Mock() view.reload_context = Mock() view._location = Mock(return_value='/sadasd') resp = view.update(foo=1) view.reload_context.assert_called_once_with(es_based=False, foo=1) view.get_item.assert_called_once_with(foo=1) view.get_item().update.assert_called_once_with( {'foo2': 'bar2'}, view.request) assert resp == view.get_item().update() def test_replace(self): view = self._test_view() view.update = Mock() resp = view.replace(foo=1) view.update.assert_called_once_with(foo=1) assert resp == view.update() def test_get_dbcollection_with_es(self): view = self._test_view() view._query_params['_limit'] = 50 view.get_collection_es = Mock(return_value=[1, 2]) view.Model = Mock() result = view.get_dbcollection_with_es(foo='bar') view.get_collection_es.assert_called_once_with() view.Model.filter_objects.assert_called_once_with([1, 2]) assert result == view.Model.filter_objects() def test_delete_many(self): view = self._test_view() view.Model = Mock(__name__='Foo') view.Model._delete_many.return_value = 123 view.get_dbcollection_with_es = Mock() result = view.delete_many(foo=1) view.get_dbcollection_with_es.assert_called_once_with(foo=1) view.Model._delete_many.assert_called_once_with( view.get_dbcollection_with_es(), view.request) assert result == 123 def test_update_many(self): view = self._test_view() view.Model = Mock(__name__='Foo') view.Model._update_many.return_value = 123 view.get_dbcollection_with_es = Mock() result = view.update_many(foo=1) view.get_dbcollection_with_es.assert_called_once_with(foo=1) view.Model._update_many.assert_called_once_with( view.get_dbcollection_with_es(), {'foo2': 'bar2'}, view.request) assert result == 123 class TestItemSubresourceBaseView(ViewTestBase): view_cls = views.ItemSubresourceBaseView def test_get_context_key(self): view = self._test_view() parent = Mock(id_name='foobar') resource = Mock() resource.parent = parent view._resource = resource assert view._get_context_key(foobar=1, foo=2) == '1' def test_get_item(self): view = self._test_view() view._parent_queryset = Mock(return_value=[1, 2]) view.reload_context = Mock() view.context = 1 assert view.get_item(foo=4) == 1 view._parent_queryset.assert_called_once_with() view.reload_context.assert_called_once_with(es_based=False, foo=4) class TestItemAttributeView(ViewTestBase): view_cls = views.ItemAttributeView request_kwargs = dict( method='GET', accept=[''], path='user/1/settings' ) def test_init(self): view = self._test_view() assert view.value_type is None assert view.unique assert view.attr == 'settings' def test_index(self): view = self._test_view() view.get_item = Mock() resp = view.index(foo=1) view.get_item.assert_called_once_with(foo=1) assert resp == view.get_item().settings def test_create(self): view = self._test_view() view.get_item = Mock() resp = view.create(foo=1) view.get_item.assert_called_once_with(foo=1) obj = view.get_item() obj.update_iterables.assert_called_once_with( {'foo2': 'bar2'}, 'settings', unique=True, value_type=None, request=view.request) assert resp == obj.settings class TestItemSingularView(ViewTestBase): view_cls = views.ItemSingularView request_kwargs = dict( method='GET', accept=[''], path='user/1/profile', url='http://example.com', ) def test_init(self): view = self._test_view() assert view.attr == 'profile' def test_show(self): view = self._test_view() view.get_item = Mock() resp = view.show(foo=1) view.get_item.assert_called_once_with(foo=1) assert resp == view.get_item().profile def test_create(self): view = self._test_view() view.set_object_acl = Mock() view.request.registry._root_resources = { 'foo': Mock(auth=False) } view.get_item = Mock() view.Model = Mock() resp = view.create(foo=1) view.get_item.assert_called_once_with(foo=1) view.Model.assert_called_once_with(foo2='bar2') child = view.Model() child.save.assert_called_once_with(view.request) parent = view.get_item() parent.update.assert_called_once_with( {'profile': child.save()}, view.request) assert view.set_object_acl.call_count == 1 assert resp == child.save() def test_update(self): view = self._test_view() view.get_item = Mock() resp = view.update(foo=1) view.get_item.assert_called_once_with(foo=1) child = view.get_item().profile child.update.assert_called_once_with( {'foo2': 'bar2'}, view.request) assert resp == child def test_replace(self): view = self._test_view() view.update = Mock() resp = view.replace(foo=1) view.update.assert_called_once_with(foo=1) assert resp == view.update() def test_delete(self): view = self._test_view() view.attr = 'profile' view.get_item = Mock() resp = view.delete(foo=1) assert resp is None view.get_item.assert_called_once_with(foo=1) parent = view.get_item() parent.profile.delete.assert_called_once_with( view.request) class TestRestViewGeneration(object): @patch('ramses.views.NefertariBaseView._run_init_actions') def test_only_provided_attrs_are_available(self, run_init): config = config_mock() view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show', 'foobar'], es_based=True, attr_view=False, singular=False) view_cls._json_encoder = 'foo' assert issubclass(view_cls, views.ESCollectionView) request = Mock(**ViewTestBase.request_kwargs) view = view_cls(request=request, **ViewTestBase.view_kwargs) assert not hasattr(view_cls, 'foobar') try: view.show() except JHTTPMethodNotAllowed: raise Exception('Unexpected error') except Exception: pass with pytest.raises(JHTTPMethodNotAllowed): view.delete_many() with pytest.raises(JHTTPMethodNotAllowed): view.create() with pytest.raises(JHTTPMethodNotAllowed): view.delete() with pytest.raises(JHTTPMethodNotAllowed): view.update_many() with pytest.raises(JHTTPMethodNotAllowed): view.index() def test_singular_view(self): config = config_mock() view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show'], es_based=True, attr_view=False, singular=True) view_cls._json_encoder = 'foo' assert issubclass(view_cls, views.ItemSingularView) def test_attribute_view(self): config = config_mock() view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show'], es_based=True, attr_view=True, singular=False) view_cls._json_encoder = 'foo' assert issubclass(view_cls, views.ItemAttributeView) def test_escollection_view(self): config = config_mock() view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show'], es_based=True, attr_view=False, singular=False) view_cls._json_encoder = 'foo' assert issubclass(view_cls, views.ESCollectionView) assert issubclass(view_cls, views.CollectionView) def test_dbcollection_view(self): config = config_mock() view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show'], es_based=False, attr_view=False, singular=False) view_cls._json_encoder = 'foo' assert not issubclass(view_cls, views.ESCollectionView) assert issubclass(view_cls, views.CollectionView) def test_default_values(self): config = config_mock() view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show']) view_cls._json_encoder = 'foo' assert issubclass(view_cls, views.ESCollectionView) assert issubclass(view_cls, views.CollectionView) assert view_cls.Model == 'foo' def test_database_acls_option(self): from nefertari_guards.view import ACLFilterViewMixin config = config_mock() config.registry.database_acls = False view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show'], es_based=False, attr_view=False, singular=False) assert not issubclass( view_cls, ACLFilterViewMixin) assert not issubclass( view_cls, views.SetObjectACLMixin) config.registry.database_acls = True view_cls = views.generate_rest_view( config, model_cls='foo', attrs=['show'], es_based=False, attr_view=False, singular=False) assert issubclass(view_cls, views.SetObjectACLMixin) assert issubclass(view_cls, ACLFilterViewMixin) ================================================ FILE: tox.ini ================================================ [tox] envlist = py27, py33,py34,py35, [testenv] setenv = PYTHONHASHSEED=0 deps = -rrequirements.dev commands = py.test {posargs:--cov ramses tests} python ramses/scripts/scaffold_test.py -s ramses_starter [testenv:flake8] deps = flake8 commands = flake8 ramses