├── .checkignore ├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── ripozo-logo.png │ ├── _templates │ └── sidebarlogo.html │ ├── changelog.rst │ ├── conf.py │ ├── examples │ ├── index.rst │ └── resource.rst │ ├── index.rst │ ├── philosophy.rst │ └── topics │ ├── adapters.rst │ ├── dispatchers.rst │ ├── fields.rst │ ├── index.rst │ ├── managers.rst │ ├── processors.rst │ ├── relationships_and_links.rst │ ├── resources.rst │ ├── rest_mixins.rst │ └── utilities.rst ├── examples ├── __init__.py ├── basic_relationships.py ├── hello_world.py └── preprocessors.py ├── logos ├── ripozo-logo-black.png ├── ripozo-logo-white.png └── ripozo-logo.png ├── pylintrc ├── ripozo ├── __init__.py ├── adapters │ ├── __init__.py │ ├── base.py │ ├── basic_json.py │ ├── hal.py │ ├── jsonapi.py │ └── siren.py ├── decorators.py ├── dispatch_base.py ├── exceptions.py ├── manager_base.py ├── resources │ ├── __init__.py │ ├── constants │ │ ├── __init__.py │ │ └── input_categories.py │ ├── constructor.py │ ├── fields │ │ ├── __init__.py │ │ ├── base.py │ │ ├── common.py │ │ ├── field.py │ │ └── validations.py │ ├── relationships │ │ ├── __init__.py │ │ ├── list_relationship.py │ │ └── relationship.py │ ├── request.py │ ├── resource_base.py │ └── restmixins.py └── utilities.py ├── ripozo_profiling ├── __init__.py ├── adapters.py ├── bits_and_pieces.py ├── end_to_end.py └── restmixins.py ├── ripozo_tests ├── __init__.py ├── bases │ ├── __init__.py │ ├── field.py │ └── manager.py ├── helpers │ ├── __init__.py │ ├── dispatcher.py │ ├── hello_world_viewset.py │ ├── inmemory_manager.py │ ├── profile.py │ └── util.py ├── integration │ ├── __init__.py │ ├── manager_base.py │ ├── relationships.py │ ├── resources.py │ └── restmixins.py └── unit │ ├── __init__.py │ ├── decorators.py │ ├── dispatch │ ├── __init__.py │ ├── adapters │ │ ├── __init__.py │ │ ├── base.py │ │ ├── boring_json.py │ │ ├── hal.py │ │ ├── jsonapi.py │ │ └── siren.py │ └── dispatch_base.py │ ├── exceptions.py │ ├── managers │ ├── __init__.py │ └── base.py │ ├── resources │ ├── __init__.py │ ├── base.py │ ├── constructor.py │ ├── fields │ │ ├── __init__.py │ │ ├── base.py │ │ ├── common.py │ │ ├── field.py │ │ └── validations.py │ ├── relationships │ │ ├── __init__.py │ │ ├── list_relationship.py │ │ └── relationship.py │ ├── request.py │ └── restmixins.py │ ├── tests │ ├── __init__.py │ └── inmemorymanager.py │ └── tests_utilities.py ├── setup.py └── tox.ini /.checkignore: -------------------------------------------------------------------------------- 1 | # An ignore file for quantified code 2 | ripozo_tests/* 3 | examples/* 4 | docs/* 5 | ripozo_profiling/* 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | .eggs/ 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | docs/build 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # PyCharm IDE 61 | .idea/ 62 | 63 | .git.bfg-report/2015-02-05/19-57-11/cache-stats.txt 64 | 65 | .git.bfg-report/2015-02-05/19-57-11/object-id-map.old-new.txt 66 | 67 | # PyPi private file 68 | .pypirc 69 | 70 | setup.cfg 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.4" 5 | - "3.3" 6 | - "2.7" 7 | - "2.6" 8 | - "pypy" 9 | - "pypy3" 10 | install: 11 | - pip install coveralls 12 | - python setup.py -q install 13 | script: 14 | - coverage run --source=ripozo setup.py test 15 | after_success: 16 | coveralls 17 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGELOG.rst 3 | include LICENSE.txt 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ripozo.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ripozo.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ripozo" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ripozo" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\ripozo.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\ripozo.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/source/_static/ripozo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertical-knowledge/ripozo/e648db2967c4fbf4315860c21a8418f384db4454/docs/source/_static/ripozo-logo.png -------------------------------------------------------------------------------- /docs/source/_templates/sidebarlogo.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | ./resource.rst -------------------------------------------------------------------------------- /docs/source/examples/resource.rst: -------------------------------------------------------------------------------- 1 | Resource Examples 2 | ================= 3 | 4 | URL construction 5 | ^^^^^^^^^^^^^^^^ 6 | 7 | .. testcode:: basic_url 8 | 9 | from ripozo import ResourceBase 10 | 11 | class MyResource(ResourceBase): 12 | pks = ['id'] 13 | 14 | 15 | .. doctest:: basic_url 16 | 17 | >>> print(MyResource.base_url) 18 | /my_resource/ 19 | >>> resource = MyResource(properties={'id': 1}) 20 | >>> print(resource.url) 21 | /my_resource/1 22 | 23 | URL construction 2 24 | ^^^^^^^^^^^^^^^^^^ 25 | 26 | .. testcode:: url2 27 | 28 | from ripozo import ResourceBase 29 | 30 | class MyResource(ResourceBase): 31 | namespace = '/api' 32 | pks = ['id'] 33 | resource_name = 'resource/' 34 | 35 | 36 | .. doctest:: url2 37 | 38 | >>> print(MyResource.base_url) 39 | /api/resource/ 40 | >>> resource = MyResource(properties={'id': 1}) 41 | >>> print(resource.url) 42 | /api/resource/1 43 | 44 | Minimal Request 45 | ^^^^^^^^^^^^^^^ 46 | 47 | .. testcode:: minimal 48 | 49 | from ripozo import RequestContainer, ResourceBase, apimethod 50 | 51 | class MyResource(ResourceBase): 52 | namespace = '/api' 53 | pks = ['id'] 54 | resource_name = 'resource' 55 | 56 | @apimethod(methods=['GET']) 57 | def hello_world(cls, request): 58 | id = request.url_params['id'] 59 | return cls(properties={'id': id, 'hello': 'world'}) 60 | 61 | 62 | .. doctest:: minimal 63 | 64 | >>> request = RequestContainer(url_params={'id': 2}) 65 | >>> resource = MyResource.hello_world(request) 66 | >>> print(resource.url) 67 | /api/resource/2 68 | >>> resource.properties 69 | {'id': 2, 'hello': 'world'} 70 | 71 | Using Fields 72 | ^^^^^^^^^^^^ 73 | 74 | .. testcode:: fields 75 | 76 | from ripozo import apimethod, translate, fields, ResourceBase 77 | 78 | class MyResource(ResourceBase): 79 | namespace = '/api' 80 | pks = ['id'] 81 | resource_name = 'resource' 82 | 83 | @apimethod(methods=['GET']) 84 | @translate(fields=[fields.IntegerField('id', required=True)], validate=True) 85 | def hello_world(cls, request): 86 | id = request.url_params['id'] 87 | return cls(properties={'id': id, 'hello': 'world'}) 88 | 89 | 90 | .. doctest:: fields 91 | :options: IGNORE_EXCEPTION_DETAIL 92 | 93 | >>> from ripozo import RequestContainer 94 | >>> request = RequestContainer() 95 | >>> resource = MyResource.hello_world(request) 96 | Traceback (most recent call last): 97 | ... 98 | ValidationException: The field "id" is required and cannot be None 99 | 100 | Relationships 101 | ^^^^^^^^^^^^^ 102 | 103 | .. testcode:: relationship 104 | 105 | from ripozo import apimethod, translate, fields, ResourceBase, Relationship 106 | 107 | class MyResource(ResourceBase): 108 | namespace = '/api' 109 | pks = ['id'] 110 | resource_name = 'resource' 111 | _relationships = [ 112 | Relationship('related', relation='RelatedResource') 113 | ] 114 | 115 | @apimethod(methods=['GET']) 116 | @translate(fields=[fields.IntegerField('id', required=True)], validate=True) 117 | def hello_world(cls, request): 118 | id = request.url_params['id'] 119 | return cls(properties={'id': id, 'hello': 'world'}) 120 | 121 | class RelatedResource(ResourceBase): 122 | pks = ['pk'] 123 | 124 | 125 | .. doctest:: relationship 126 | 127 | >>> properties = dict(id=1, related=dict(pk=2)) 128 | >>> resource = MyResource(properties=properties) 129 | >>> resource.properties 130 | {'id': 1} 131 | >>> print(resource.related_resources[0].name) 132 | related 133 | >>> related_resource = resource.related_resources[0].resource 134 | >>> related_resource.properties 135 | {'pk': 2} 136 | >>> print(related_resource.url) 137 | /related_resource/2 138 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. ripozo documentation master file, created by 2 | sphinx-quickstart on Fri Feb 6 18:01:27 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ripozo's documentation! 7 | ================================== 8 | 9 | .. image:: ./_static/ripozo-logo.png 10 | :target: http://ripozo.org 11 | :alt: ripozo logo 12 | 13 | Contents: 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | 18 | topics/index.rst 19 | examples/index.rst 20 | philosophy.rst 21 | changelog.rst 22 | 23 | 24 | 25 | .. include:: ../../README.rst 26 | 27 | 28 | 29 | Indices and tables 30 | ================== 31 | 32 | * :ref:`genindex` 33 | * :ref:`modindex` 34 | * :ref:`search` 35 | 36 | -------------------------------------------------------------------------------- /docs/source/philosophy.rst: -------------------------------------------------------------------------------- 1 | Philosophy and Architecture 2 | =========================== 3 | 4 | This page contains information on why and how ripozo 5 | is architected. 6 | 7 | Philosophy 8 | ---------- 9 | 10 | Hypermedia 11 | ^^^^^^^^^^ 12 | 13 | Ripozo is designed first and foremost behind the belief that Hypermedia 14 | APIs are the way everything should be developed. With hypermedia APIs and 15 | good client consumers, boilerplate code is removed and a developer is free 16 | to focus on making the difficult decisions and writing the business logic, 17 | not the boiler plate code. It should be noted that ripozo goes beyond 18 | CRUD+L. CRUD+L is a good step in the right direction, but it still requires 19 | too much knowledge on the client side still. With true Hypermedia, the 20 | client doesn't need to how to construct URLs or what the actions available 21 | on a given resource are. The server can simply tell you. 22 | 23 | Unopinionated (unless you want it to be) 24 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | 26 | Ripozo doesn't make design decisions for you. Ripozo is a tool that is capable 27 | of being used in any existing web framework. It can be used with any database. 28 | Any Hypermedia protocol can be used. If you really wanted to, though we don't 29 | recommend it, you could even render raw HTML with a tool like Jinja2. You could 30 | even theoretically skip the web application all together and use it directly instead 31 | of through a web api. Long story short, Ripozo doesn't care how it is used. 32 | It can be plugged in for a small part of your application or used exclusively. 33 | 34 | Extensible and Pluggable 35 | ^^^^^^^^^^^^^^^^^^^^^^^^ 36 | 37 | Ripozo is extensible/pluggable. Just because ripozo itself doesn't make design decisions 38 | for you doesn't mean that it shouldn't be batteries included, we just believe 39 | that the batteries included versions should be split out into other packages. 40 | Hence its extensibility. There are already packages that make it easy to use 41 | both Cassandra and SQL databases via cqlengine and SQLAlchemy. There's an 42 | adaption of ripozo for integration with Flask. We didn't want to make design 43 | decisions for you. But we figured that if you wanted something more opinionated, 44 | we'd make it easy to extend ripozo. As a side note, we really hope that you 45 | open source any ripozo based packages that you create and let us know. After all, 46 | let's keep our open source world DRY. 47 | 48 | Basic Concepts 49 | -------------- 50 | 51 | Alright, so you're probably tired of hearing about all our self righteous talk 52 | about ripozo's philosophy and want to know how you can actually use it to start 53 | building awesome APIs. Ripozo has four basic pieces: Dispatchers, Adapters, Resources, 54 | and Managers. tl:dr; version: Dispatchers handle incoming requests and direct 55 | them to Resources. Resources perform the business logic, optionally using a 56 | Manager to interact with a database. The Resource returns an instance of itself 57 | to the dispatcher which determines which Adapter to use. The adapter then takes the 58 | Resource instance and formats it into an appropriate response. The dispatcher then 59 | returns this response. 60 | 61 | Now for the more detailed descriptions... 62 | 63 | Dispatchers 64 | ^^^^^^^^^^^ 65 | 66 | In this base ripozo package, the dispatcher is simply an abstract base class 67 | with a few convience methods. Since the handling of incoming requests is 68 | dependent on the framework you are using, and we don't want to make design 69 | decisions for you, we thought that this would be a bad place for making opinionated 70 | decisions. However, the upside is that it is very easy to create dispatchers. 71 | In fact, a Flask dispatcher has already been created and is only one file less than 72 | 100 lines long. In the future we will be adding more webframework specific 73 | dispatchers and plan on making a framework of our own that is specific to ripozo. 74 | 75 | Resources 76 | ^^^^^^^^^ 77 | 78 | Resources are the bread and butter of ripozo. They determine the business logic 79 | of an application. Resources should be reusable across all ripozo dispatch and 80 | manager extensions. In other words, you should be able to take your ripozo 81 | resources that you originally developed for Flask and plug them into a Django 82 | application. 83 | 84 | Managers 85 | ^^^^^^^^ 86 | 87 | Managers are more or less the state keepers of the application. They are primarily 88 | responsible for providing a common interface for basic CRUD+L actions exposed in the 89 | restmixins module. Additionally, they should be capable of inspecting the record being 90 | persisted and providing default fields for that resource. 91 | 92 | Adapters 93 | ^^^^^^^^ 94 | 95 | Adapters determine the format in which to return a response. They take a resource 96 | instance and generate what the response should look like. For example, you could 97 | have an adapter that returns a SIREN response and another adapter that returns a HAL 98 | response. The best part is, that these are entirely reusable. That means that 99 | you can support as many adapters as are written by anyone in the world with no extra 100 | work on your part outside of installing the extra adapter packages. This is extemely 101 | useful because you can write your logic once and not have to worry about duplicating 102 | your code so that the front-end web team can use SIREN and the mobile team can use 103 | basic CRUD+L. -------------------------------------------------------------------------------- /docs/source/topics/adapters.rst: -------------------------------------------------------------------------------- 1 | Adapters 2 | ======== 3 | 4 | Adapters are responsible for transforming a 5 | Resource instance into a request body string. 6 | For example, in the SirenAdapter, they would 7 | put the properties in a dictionary called "properties", 8 | the related resource in a list called "entities", 9 | and so on and so forth. All adapters instantiation 10 | requires at least a resource instance. 11 | 12 | Finally, adapters are independent of web framework 13 | and database/managers used. This means that you can 14 | completely reuse adapters across all of your applications. 15 | 16 | Building your own adapter 17 | ------------------------- 18 | 19 | At some point or another, you will probably 20 | want to roll out your own adapter. Maybe you 21 | are using a front-end framework that is expecting 22 | a specific format or maybe you're porting over 23 | from django-rest-framework and don't want to 24 | have to change your front-end. Either way, you 25 | want some flexibility in how the resource gets formatted. 26 | 27 | For an example of a basic adapter check out the built in 28 | `JSON adapter. `_ 29 | 30 | Required pieces 31 | ^^^^^^^^^^^^^^^ 32 | 33 | .. py:attribute:: formats 34 | 35 | A tuple or list of available formats in unicode/str form. 36 | These are the formats that are used to match the requests 37 | accept-types with the appropriate adapter. 38 | 39 | 40 | .. autoattribute:: ripozo.adapters.base.AdapterBase.formatted_body 41 | 42 | 43 | .. autoattribute:: ripozo.adapters.base.AdapterBase.extra_headers 44 | 45 | 46 | Optional pieces 47 | ^^^^^^^^^^^^^^^ 48 | 49 | These methods do not have to be implemented in the subclass 50 | but in some cases it may make more sense. 51 | 52 | .. automethod:: ripozo.adapters.base.AdapterBase.format_exception 53 | 54 | 55 | Adapters API 56 | ------------ 57 | 58 | Built in adapters 59 | ^^^^^^^^^^^^^^^^^ 60 | 61 | .. autoclass:: ripozo.adapters.siren.SirenAdapter 62 | :members: 63 | 64 | .. autoclass:: ripozo.adapters.jsonapi.JSONAPIAdapter 65 | :members: 66 | 67 | .. autoclass:: ripozo.adapters.hal.HalAdapter 68 | :members: 69 | 70 | .. autoclass:: ripozo.adapters.basic_json.BasicJSONAdapter 71 | :members: 72 | 73 | Base Adapter 74 | ^^^^^^^^^^^^ 75 | 76 | .. autoclass:: ripozo.adapters.base.AdapterBase 77 | :members: 78 | -------------------------------------------------------------------------------- /docs/source/topics/dispatchers.rst: -------------------------------------------------------------------------------- 1 | Dispatchers 2 | =========== 3 | 4 | Dispatchers are responsible for translating incoming requests 5 | and dispatching them to the appropriate resources. They then 6 | take the response from the ResourceBase subclass and uses to appropriate 7 | adapter to create a response. Dispatchers are responsible for coupling ripozo 8 | with a web framework. As such, the dispatcher is not fully implemented in 9 | ripozo. Rather there is an abstract base class that must be implemented 10 | for the specific framework that is being used. 11 | 12 | Example 13 | ------- 14 | 15 | When using a ripozo implementation for your preferred framework, registering 16 | the resource classes is very easy. 17 | 18 | .. code-block:: python 19 | 20 | # Import your resource classes 21 | from my_resources import MyResource, MyOtherResource 22 | 23 | # Import the adapters that you want to use 24 | from ripozo.dispatch.adapters import SirenAdapter, HalAdapter 25 | 26 | # Initialize your Dispatcher (this will be different for 27 | # different web frameworks. Please look at the specific documentation 28 | # for that framework. 29 | # For example, in flask-ripozo it would be 30 | # app = Flask(app) 31 | # dispatcher = FlaskDispatcher(app) 32 | 33 | # register your adapters, the first adapter is the default adapter 34 | dispatcher.register_adapters(SirenAdapter, HalAdapter) 35 | 36 | # Register your resource classes 37 | dispatcher.register_resources(MyResource, MyOtherResource) 38 | 39 | I wasn't lying, it's pretty basic. 40 | 41 | Implementing a Dispatcher 42 | ------------------------- 43 | 44 | So you've just discovered the next great python web 45 | framework. Unfortunately, nobody has developed a ripozo 46 | dispatcher implementation for it yet. Don't despair! Implementing 47 | a ripozo dispatcher is theoretically quite simple. There is only 48 | one method and one property you need to implement. 49 | 50 | The first is the base_url property. This is base_url 51 | that is used when constructing urls. For example, it might 52 | include the the host, port and base path (e.g. ``'http://localhost:5000/api'``). 53 | If you absolutely need to make it a method, you will need to overrided the dispatch 54 | method as well, since it expects it to be a property. 55 | 56 | .. autoattribute:: ripozo.dispatch_base.DispatcherBase.base_url 57 | 58 | 59 | The second piece you'll need to implement is the register_route 60 | method. This is the method that is responsible for taking 61 | an individual route and relaying it to the webframework. 62 | 63 | .. automethod:: ripozo.dispatch_base.DispatcherBase.register_route 64 | 65 | 66 | For a real life example of implementing a dispatcher check out the 67 | `flask dispatcher. `_ 68 | 69 | 70 | Dispatcher API 71 | -------------- 72 | 73 | .. autoclass:: ripozo.dispatch_base.DispatcherBase 74 | :members: 75 | :special-members: 76 | -------------------------------------------------------------------------------- /docs/source/topics/fields.rst: -------------------------------------------------------------------------------- 1 | Fields, Translation, and Validation 2 | =================================== 3 | 4 | This topic describes how to translate and validate parameters 5 | from a request. 6 | 7 | Translation is the process of converting the incoming parameter (typically 8 | a string) into the desired ``type``. For example, there may be a 9 | request such as the following: ``http://mydomain.com/resource?count=20``. 10 | The count parameter would be a string, however we would want to use 11 | it as an integer. Therefore, we would cast/translate it to an integer. 12 | This can encapsulate more than just casting (e.g. turning a JSON string 13 | into a ``dict``. 14 | 15 | Validation takes the translated parameter and ensures that it fulfills 16 | a certain set of expectations. In the previous example, we may wish 17 | to validate that the ``count`` query argument is always greater than 0. 18 | 19 | Simple Example 20 | -------------- 21 | 22 | This example demonstrates how to perform simple translation and 23 | validation on an count parameter 24 | 25 | .. testsetup:: * 26 | 27 | from ripozo import translate, fields, apimethod, ResourceBase, RequestContainer 28 | 29 | .. testcode:: simpleexample 30 | 31 | from ripozo import translate, fields, apimethod, ResourceBase, RequestContainer 32 | 33 | class MyResource(ResourceBase): 34 | 35 | @apimethod(methods=['GET']) 36 | @translate(fields=[fields.IntegerField('count', minimum=1)], validate=True) 37 | def hello(cls, request): 38 | count = request.get('count') 39 | return cls(properties=dict(count=count)) 40 | 41 | The ``translate`` decorator translates the inputs using a series of fields. If 42 | we pass ``validate=True`` to the ``translate`` decorator, it will perform validation 43 | as well. The list of fields passed to the ``fields`` parameter must all be instances 44 | of :py:class:`BaseField` . ``BaseField`` classes perform 45 | the actual translation and validation for a given field. 46 | 47 | .. doctest:: simpleexample 48 | 49 | >>> req = RequestContainer(query_args=dict(count='3')) 50 | >>> res = MyResource.hello(req) 51 | >>> res.properties['count'] == 3 52 | True 53 | >>> req = RequestContainer(query_args=dict(count='0')) 54 | >>> res = MyResource.hello(req) 55 | Traceback (most recent call last): 56 | ... 57 | ValidationException: The field "count" is required and cannot be None 58 | >>> req = RequestContainer(query_args=dict(count='not an integer')) 59 | >>> res = MyResource.hello(req) 60 | Traceback (most recent call last): 61 | ... 62 | TranslationException: Not a valid integer type: not an integer 63 | 64 | We can see that the string is appropriately cast in the first attempt and passes validation. 65 | The second example raises a ``ValidationException`` because it was id<1 which 66 | we specified as the minimum. Finally, the last example raises a TranslationException 67 | because the ``id`` parameter was not a valid integer. 68 | 69 | Specifying the location 70 | ----------------------- 71 | 72 | By default, the ``translate`` decorator will look in the url parameters, query arguments 73 | and body arguments when finding the parameter to check. For example, 74 | the following still works 75 | 76 | .. doctest:: simpleexample 77 | 78 | >>> req = RequestContainer(body_args=dict(count='3')) 79 | >>> res = MyResource.hello(req) 80 | >>> res.properties['count'] == 3 81 | True 82 | 83 | However, sometimes we may require that a parameter is only allowed in a 84 | specific location. With ripozo, this is very simple. 85 | 86 | .. testcode:: simpleexample 87 | 88 | from ripozo.resources.constants.input_categories import QUERY_ARGS 89 | 90 | # we'll declare the fields here for cleanliness 91 | # Note the arg_type parameter which specifies to 92 | # only look in the query args for this field 93 | hello_fields = [fields.IntegerField('count', required=True, minimum=1, arg_type=QUERY_ARGS)] 94 | 95 | class MyResource(ResourceBase): 96 | 97 | @apimethod(methods=['GET']) 98 | @translate(fields=hello_fields, validate=True) 99 | def hello(cls, request): 100 | count = request.get('count') 101 | return cls(properties=dict(count=count)) 102 | 103 | The previous example, will no longer work since the field is 104 | only allowed to be from the query arguments. 105 | 106 | .. doctest:: simpleexample 107 | 108 | >>> req = RequestContainer(body_args=dict(count='3')) 109 | >>> res = MyResource.hello(req) 110 | Traceback (most recent call last): 111 | ... 112 | ValidationException: The field "count" is required and cannot be None 113 | >>> req = RequestContainer(query_args=dict(count='3')) 114 | >>> res = MyResource.hello(req) 115 | >>> res.properties['count'] == 3 116 | True 117 | 118 | With this method, we could even translate and validate two fields with the 119 | same name as long as they are in different locations. 120 | 121 | Creating special fields 122 | ----------------------- 123 | 124 | While there are plenty of fields available in :ref:`all-fields`, 125 | sometimes, you need something more specific. In this example we'll show you how 126 | to create an email validation field. 127 | 128 | To create a new field, you simply need to inherit from :py:class:`ripozo.resources.fields.base.BaseField` and 129 | override the necessary methods, in particular the ``_translate`` and ``_validate`` 130 | methods. 131 | 132 | .. testcode:: newfield 133 | 134 | from ripozo import fields 135 | from ripozo.exceptions import ValidationException 136 | 137 | class EmailField(fields.BaseField): 138 | def _validate(self, obj, **kwargs): 139 | # perform the standard validation such as whether it is required. 140 | obj = super(EmailField, self)._validate(obj, **kwargs) 141 | if obj is None: # In case it wasn't a required field. 142 | return obj 143 | if '@' not in obj: 144 | raise ValidationException('"{0}" is not a valid email address'.format(obj)) 145 | return obj 146 | 147 | We could then test this by running the following 148 | 149 | .. doctest:: newfield 150 | 151 | >>> field = EmailField('email_address', required=True) 152 | >>> field.translate('some@email.com', validate=True) 153 | 'some@email.com' 154 | >>> field.translate('not an email', validate=True) 155 | Traceback (most recent call last): 156 | ... 157 | ValidationException: "not an email" is not a valid email address 158 | 159 | We can then use this new field like the ``IntegerField`` we used previously. 160 | 161 | 162 | 163 | API Documentation 164 | ================= 165 | 166 | Translation and Validation Decorators 167 | ------------------------------------- 168 | 169 | .. autoclass:: ripozo.decorators.translate 170 | :members: 171 | :special-members: 172 | 173 | .. autoclass:: ripozo.decorators.manager_translate 174 | :members: 175 | :special-members: 176 | 177 | .. _all-fields: 178 | 179 | Field types 180 | ----------- 181 | 182 | .. automodule:: ripozo.resources.fields.common 183 | :members: 184 | :special-members: 185 | 186 | .. automodule:: ripozo.resources.fields.base 187 | :members: 188 | :special-members: 189 | -------------------------------------------------------------------------------- /docs/source/topics/index.rst: -------------------------------------------------------------------------------- 1 | Topics 2 | ====== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | adapters.rst 10 | dispatchers.rst 11 | fields.rst 12 | managers.rst 13 | processors.rst 14 | relationships_and_links.rst 15 | resources.rst 16 | rest_mixins.rst 17 | utilities.rst 18 | 19 | -------------------------------------------------------------------------------- /docs/source/topics/managers.rst: -------------------------------------------------------------------------------- 1 | Managers 2 | ======== 3 | 4 | Managers are the persistence abstraction layer in ripozo. 5 | They are responsible for all session to session persistence in 6 | a ripozo based application. Typically, we can assume that the persistence 7 | layer is a database. However, the actual method of persistence is 8 | irrelevant to ripozo. There are ripozo extensions that are specifically 9 | for certain database types. At the most basic level, all ripozo ManagerBase 10 | subclasses must implement basic CRUD+L. That is they must implement a create, 11 | retrieve, update, delete an retrieve_list methods. 12 | 13 | Managers in ripozo are not required and are more intended as a way to create 14 | a common interface for database interactions. Additionally, they are intended, 15 | to abstract away the database since individual database interactions should not 16 | be on the resource classes. The one caveat is that the restmixins will not 17 | work without a manager defined. 18 | 19 | Creating a manager extensions 20 | ----------------------------- 21 | 22 | Manager extensions in ripozo can be somewhat tricky to 23 | implement correctly. Fortunately, it only needs to be 24 | done once (and hopefully shared in the community). 25 | 26 | There is only one absolutely critical piece that needs 27 | to be implements for a manager to minimally work. The get_field_type 28 | method helps the translation and validation work. If not implemented, 29 | your manager will not work in any case. This method determines what 30 | the python type is for the specified field. 31 | 32 | *note* get_field_type is a classmethod. That means you need to decorate 33 | it with the builtin ``@classmethod`` decorator. 34 | 35 | .. automethod:: ripozo.manager_base.BaseManager.get_field_type 36 | 37 | Additionally, there are 5 basic CRUD+L operations. Not all 38 | of them need to be implemented (with some databases it may not 39 | make sense). However, the method should still be overridden 40 | and a ``NotImplementedError`` exception should be raised if 41 | it is called. Also, keep in mind that if any methods are 42 | not implemented then at least some of the restmixins will 43 | not be available. 44 | 45 | .. automethod:: ripozo.manager_base.BaseManager.create 46 | 47 | 48 | .. automethod:: ripozo.manager_base.BaseManager.retrieve 49 | 50 | 51 | .. automethod:: ripozo.manager_base.BaseManager.retrieve_list 52 | 53 | 54 | .. automethod:: ripozo.manager_base.BaseManager.update 55 | 56 | 57 | .. automethod:: ripozo.manager_base.BaseManager.delete 58 | 59 | 60 | 61 | Base Manager API 62 | ---------------- 63 | 64 | .. autoclass:: ripozo.manager_base.BaseManager 65 | :members: 66 | :special-members: 67 | -------------------------------------------------------------------------------- /docs/source/topics/processors.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _preprocessors and postprocessors: 3 | 4 | Preprocessors and Postprocessors 5 | ================================ 6 | 7 | Sometimes, you want to run a certain piece of code before/after every 8 | request to a resource. For example, maybe the resource is only accessible 9 | to authenticated users. This can be done easily with preprocessors and postprocessors. 10 | The preprocessors and postprocessors lists are the functions that are called before 11 | and after the ``apimethod`` decorated function runs. They are run in the order in which 12 | they are described in the list. 13 | 14 | .. testcode:: processors, picky_processors 15 | 16 | from ripozo import ResourceBase, apimethod, RequestContainer 17 | 18 | def pre1(cls, function_name, request): 19 | print('In pre1') 20 | 21 | def pre2(cls, function_name, request): 22 | print('In pre2') 23 | 24 | def post1(cls, function_name, request, resource): 25 | print('In post1') 26 | 27 | class MyResource(ResourceBase): 28 | preprocessors = [pre1, pre2] 29 | postprocessors = [post1] 30 | 31 | @apimethod(methods=['GET']) 32 | def say_hello(cls, request): 33 | print('hello') 34 | return cls(properties=dict(hello='world')) 35 | 36 | .. doctest:: processors 37 | 38 | >>> res = MyResource.say_hello(RequestContainer()) 39 | In pre1 40 | In pre2 41 | hello 42 | In post1 43 | 44 | These can be used to perform any sort of common functionality across 45 | all requests to this resource. Preprocessors always get the class as 46 | the first argument and the request as the second. Postprocessors get an 47 | additional resource argument as the third. The resource object is the return 48 | value of the apimethod. 49 | 50 | 51 | The picky_processor 52 | """"""""""""""""""" 53 | 54 | Sometimes you only want to run pre/postprocessors 55 | for specific methods. In those cases you can use 56 | the picky_processor. The picky_processor allows you 57 | to pick which methods you or don't want to run the 58 | pre/postprocessors 59 | 60 | .. testcode:: picky_processors 61 | 62 | from ripozo import picky_processor 63 | 64 | class PickyResource(ResourceBase): 65 | # only runs for the specified methods 66 | preprocessors = [picky_processor(pre1, include=['say_hello']), 67 | picky_processor(pre2, exclude=['say_hello'])] 68 | 69 | @apimethod(methods=['GET']) 70 | def say_hello(cls, request): 71 | print('hello') 72 | return cls(properties=dict(hello='world')) 73 | 74 | @apimethod(route='goodbye', methods=['GET']) 75 | def say_goodbye(cls, request): 76 | print('goodbye') 77 | return cls(properties=dict(goodbye='world')) 78 | 79 | The picky_processor allows you to pick which 80 | methods to run the preprocessor by looking at the name 81 | of the processor. If it's not in the exclude list or 82 | in the include list it will be run. Otherwise 83 | the preprocessor will be skipped for that method. 84 | 85 | 86 | .. doctest:: picky_processors 87 | 88 | >>> res = PickyResource.say_hello(RequestContainer()) 89 | In pre1 90 | hello 91 | >>> res = PickyResource.say_goodbye(RequestContainer()) 92 | In pre2 93 | goodbye 94 | 95 | Picky Processor 96 | ^^^^^^^^^^^^^^^ 97 | 98 | .. autofunction:: ripozo.utilities.picky_processor 99 | -------------------------------------------------------------------------------- /docs/source/topics/rest_mixins.rst: -------------------------------------------------------------------------------- 1 | Rest Mixins 2 | =========== 3 | 4 | We get it. Most of the time, you want some basic 5 | CRUD+L (Create, Retrieve, Update, Delete and Lists). 6 | Also, you don't want to have to write basic code 7 | for every single resource. Ripozo, implements the basics 8 | for you so that you don't have to rewrite them everytime and 9 | can focus on the interesting parts of your api. 10 | 11 | .. code-block:: python 12 | 13 | from mymanagers import MyManager 14 | 15 | from ripozo.resources.restmixins import Create 16 | 17 | class MyResource(Create): 18 | resource_name = 'my_resource' 19 | manager = MyManager() 20 | 21 | This would create a endpoint '/my_resource' that if posted 22 | to would create a resource using the ``MyManager().create`` 23 | method. It would then serialize and return an instance of MyResource. 24 | 25 | All of the rest mixins are subclasses of ResourceBase. The following 26 | base mixins are available in addition to various combinations of them. 27 | They can be mixed and matched as you please. 28 | 29 | - Create 30 | - Retrieve 31 | - RetrieveList 32 | - Update 33 | - Delete 34 | - CRUD (Create, Retrieve, Update, Delete) 35 | - CRUDL (Create, Retrieve, RetrieveList, Update, Delete) 36 | 37 | .. code-block:: python 38 | 39 | from mymanagers import MyManager 40 | 41 | from ripozo import ListRelationship, Relationship, restmixins 42 | 43 | class MyResourceList(restmixins.CRUDL): 44 | resource_name = 'resource' 45 | manager = MyManager() 46 | pks = ('id',) 47 | 48 | 49 | Rest Mixins API 50 | --------------- 51 | 52 | .. automodule:: ripozo.resources.restmixins 53 | :members: 54 | :special-members: 55 | -------------------------------------------------------------------------------- /docs/source/topics/utilities.rst: -------------------------------------------------------------------------------- 1 | Utitilities API 2 | =============== 3 | 4 | .. automodule:: ripozo.utilities 5 | :members: 6 | :special-members: 7 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /examples/basic_relationships.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo import ResourceBase, apimethod, Relationship 7 | 8 | class MyResource(ResourceBase): 9 | _namespace = '/api' 10 | _resource_name = 'resource' 11 | _relationships = [ 12 | Relationship('related', relation='RelatedResource') 13 | ] 14 | 15 | @apimethod(methods=['GET']) 16 | def say_hello(cls, request): 17 | return cls(properties=request.body_args) 18 | 19 | class RelatedResource(ResourceBase): 20 | _namespace = '/api' 21 | _resource_name = 'related' 22 | _pks = ['id'] 23 | 24 | 25 | if __name__ == '__main__': 26 | from ripozo import RequestContainer 27 | 28 | request = RequestContainer(body_args={'say': 'hello', 'related': {'id': 1}}) 29 | resource = MyResource.say_hello(request) 30 | 31 | # prints {'say': 'hello'} 32 | print(resource.properties) 33 | 34 | resource_tuple = resource.related_resources[0] 35 | 36 | # prints 'related' 37 | print(resource_tuple.name) 38 | 39 | # prints '/api/related/1' 40 | print(resource_tuple.resource.url) 41 | 42 | # prints {'id': 1} 43 | print(resource_tuple.resource.properties) 44 | -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | 7 | from ripozo import ResourceBase, apimethod 8 | 9 | class MyResource(ResourceBase): 10 | _namespace = '/api' 11 | _resource_name = 'resource' 12 | 13 | @apimethod(methods=['GET']) 14 | def say_hello(cls, request): 15 | return cls(properties=request.body_args) 16 | 17 | class MyOtherResource(ResourceBase): 18 | _namespace = '/api' 19 | _resource_name = 'resource2' 20 | _pks = ['id', 'pk'] 21 | 22 | @apimethod(methods=['GET']) 23 | def say_hello(cls, request): 24 | props = request.body_args 25 | props.update(request.url_params) 26 | return cls(properties=props) 27 | 28 | 29 | if __name__ == '__main__': 30 | from ripozo import RequestContainer 31 | 32 | request = RequestContainer(body_args={'say': 'hello'}) 33 | resource = MyResource.say_hello(request) 34 | 35 | # Prints '/api/resource' 36 | print(MyResource.base_url) 37 | 38 | # Prints {'say': 'hello'} 39 | print(resource.properties) 40 | 41 | request = RequestContainer(url_params={'id': 1, 'pk': 2}, body_args={'say': 'hello'}) 42 | other_resource = MyOtherResource.say_hello(request) 43 | 44 | # Prints '/api/resource2// 45 | print(MyOtherResource.base_url) 46 | 47 | # prints '/api/resource2/1/2 48 | print(other_resource.url) 49 | 50 | # prints the url parameters and body args 51 | # {'id': 1, 'pk': 2, 'say': 'hello'} 52 | print(other_resource.properties) 53 | -------------------------------------------------------------------------------- /examples/preprocessors.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo import ResourceBase, apimethod, RequestContainer, picky_processor 7 | 8 | 9 | def pre1(cls, function_name, request): 10 | print('In pre1') 11 | 12 | def pre2(cls, function_name, request): 13 | print('In pre2') 14 | 15 | def post1(cls, function_name, request, resource): 16 | print('In post1') 17 | 18 | class MyResource(ResourceBase): 19 | # preprocessors run before the apimethod 20 | _preprocessors = [pre1, pre2] 21 | # and postprocessors run after. 22 | _postprocessors = [post1] 23 | 24 | @apimethod(methods=['GET']) 25 | def say_hello(cls, request): 26 | print('hello') 27 | return cls(properties=dict(hello='world')) 28 | 29 | 30 | class PickyResource(ResourceBase): 31 | # only runs for the specified methods 32 | _preprocessors = [picky_processor(pre1, include=['say_hello']), 33 | picky_processor(pre2, exclude=['say_hello'])] 34 | 35 | @apimethod(methods=['GET']) 36 | def say_hello(cls, request): 37 | print('hello') 38 | return cls(properties=dict(hello='world')) 39 | 40 | @apimethod(route='goodbye', methods=['GET']) 41 | def say_goodbye(cls, request): 42 | print('goodbye') 43 | return cls(properties=dict(goodbye='world')) 44 | 45 | 46 | if __name__ == '__main__': 47 | req = RequestContainer() 48 | MyResource.say_hello(req) 49 | # Prints: 50 | # In pre1 51 | # In pre2 52 | # hello 53 | # In post1 54 | 55 | PickyResource.say_hello(req) 56 | # Prints 57 | # In pre1 58 | # hello 59 | 60 | PickyResource.say_goodbye(req) 61 | # Prints: 62 | # In pre2 63 | # goodbye 64 | -------------------------------------------------------------------------------- /logos/ripozo-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertical-knowledge/ripozo/e648db2967c4fbf4315860c21a8418f384db4454/logos/ripozo-logo-black.png -------------------------------------------------------------------------------- /logos/ripozo-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertical-knowledge/ripozo/e648db2967c4fbf4315860c21a8418f384db4454/logos/ripozo-logo-white.png -------------------------------------------------------------------------------- /logos/ripozo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vertical-knowledge/ripozo/e648db2967c4fbf4315860c21a8418f384db4454/logos/ripozo-logo.png -------------------------------------------------------------------------------- /ripozo/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A python package for quickly building RESTful/Hypermedia/HATEOAS 3 | applications in any web framework, with any database, using 4 | any protocol. 5 | """ 6 | 7 | from ripozo.decorators import apimethod, translate 8 | from ripozo.resources import fields, restmixins 9 | from ripozo.resources.relationships.list_relationship import ListRelationship 10 | from ripozo.resources.relationships.relationship import Relationship, FilteredRelationship 11 | from ripozo.resources.resource_base import ResourceBase 12 | from ripozo.resources.request import RequestContainer 13 | from ripozo.utilities import picky_processor 14 | -------------------------------------------------------------------------------- /ripozo/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the builtin adapters as well 3 | as the AdapterBase abstract base class. 4 | Inherit from that to ensure a proper implementation 5 | of the adapter. 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import division 9 | from __future__ import print_function 10 | from __future__ import unicode_literals 11 | 12 | from .base import AdapterBase 13 | from .siren import SirenAdapter 14 | from .hal import HalAdapter 15 | from .basic_json import BasicJSONAdapter 16 | from .jsonapi import JSONAPIAdapter 17 | -------------------------------------------------------------------------------- /ripozo/adapters/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module containing the base adapter. 3 | """ 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | from abc import ABCMeta, abstractproperty 10 | from warnings import warn 11 | 12 | from ripozo.utilities import join_url_parts 13 | 14 | import json 15 | import six 16 | 17 | 18 | @six.add_metaclass(ABCMeta) 19 | class AdapterBase(object): 20 | """ 21 | The adapter base is responsible for specifying how 22 | a resource should be translated for the client. For 23 | example, you may want to specify a specific hypermedia 24 | protocol or format it in a manner that is specific to 25 | your client (though you should probably avoid that) 26 | 27 | :param list formats: A list of strings that indicate which Content-Types 28 | will match with this adapter. For example, you might include 29 | 'application/vnd.siren+json' in the formats for a SIREN adapter. 30 | This means that any request with that content type will be responded 31 | to in the appropriate manner. Any of the strings in the list will be 32 | considered the appropriate format for the adapter on which they are 33 | specified. 34 | """ 35 | formats = None 36 | 37 | def __init__(self, resource, base_url=''): 38 | """ 39 | Simple sets the resource on the instance. 40 | 41 | :param resource: The resource that is being formatted. 42 | :type resource: rest.viewsets.resource_base.ResourceBase 43 | """ 44 | self.base_url = base_url 45 | self.resource = resource 46 | 47 | @abstractproperty 48 | def formatted_body(self): 49 | """ 50 | This property is the fully qualified and formatted response. 51 | For example, you might return a Hypermedia formatted response 52 | body such as the SIREN hypermedia protocol or HAL. This 53 | must be overridden by any subclass. Additionally, it is 54 | a property and must be decorated as such. 55 | 56 | :return: The formatted response body. 57 | :rtype: unicode 58 | """ 59 | raise NotImplementedError 60 | 61 | @abstractproperty 62 | def extra_headers(self): 63 | """ 64 | Headers that should be added to response. For example it might be 65 | the content-type etc... This must be overridden by any 66 | subclass since it raises a NotImplementedError. It can 67 | also be overridden as a class attribute if it will not 68 | be dynamic. 69 | 70 | :return: A dictionary of the headers to return. 71 | :rtype: dict 72 | """ 73 | raise NotImplementedError 74 | 75 | def combine_base_url_with_resource_url(self, resource_url): 76 | """ 77 | Does exactly what it says it does. Uses ``join_url_parts`` 78 | with the ``self.base_url`` and ``resource_url`` argument 79 | together. 80 | 81 | :param unicode resource_url: The part to join with the ``self.base_url`` 82 | :return: The joined url 83 | :rtype: unicode 84 | """ 85 | return join_url_parts(self.base_url, resource_url) 86 | 87 | @classmethod 88 | def format_exception(cls, exc): 89 | """ 90 | Takes an exception and appropriately formats 91 | the response. By default it just returns a json dump 92 | of the status code and the exception message. 93 | Any exception that does not have a status_code attribute 94 | will have a status_code of 500. 95 | 96 | :param Exception exc: The exception to format. 97 | :return: A tuple containing: response body, format, 98 | http response code 99 | :rtype: tuple 100 | """ 101 | warn('format_exception will be an abstract method in release 2.0.0. ' 102 | 'You will need to implement this method in your adapter.', 103 | PendingDeprecationWarning) 104 | status_code = getattr(exc, 'status_code', 500) 105 | body = json.dumps(dict(status=status_code, message=six.text_type(exc))) 106 | return body, cls.formats[0], status_code 107 | 108 | @classmethod 109 | def format_request(cls, request): 110 | """ 111 | Takes a request and appropriately reformats 112 | the request. For example, jsonAPI requires a 113 | specific request format that must be transformed 114 | to work with ripozo. For this base implementation 115 | it simply returns the request without any additional 116 | formating. 117 | 118 | :param RequestContainer request: The request to reformat. 119 | :return: The formatted request. 120 | :rtype: RequestContainer 121 | """ 122 | warn('format_request will be an abstractmethod in release 2.0. ' 123 | 'You will need to implement this method in your adapter', 124 | PendingDeprecationWarning) 125 | return request 126 | 127 | @property 128 | def status_code(self): 129 | """ 130 | :return: Returns the status code of the resource if 131 | it is available. If it is not it assumes a 200. 132 | :rtype: int 133 | """ 134 | return self.resource.status_code or 200 135 | -------------------------------------------------------------------------------- /ripozo/adapters/basic_json.py: -------------------------------------------------------------------------------- 1 | """ 2 | Boring json which is just a basic 3 | dump of the resource into json format. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | from ripozo.adapters import AdapterBase 11 | 12 | import json 13 | import six 14 | 15 | _CONTENT_TYPE = 'application/json' 16 | 17 | 18 | class BasicJSONAdapter(AdapterBase): 19 | """ 20 | Just a plain old JSON dump of the properties. 21 | Nothing exciting. 22 | 23 | Format: 24 | 25 | .. code-block:: javascript 26 | 27 | : { 28 | field1: "value" 29 | field2: "value" 30 | relationship: { 31 | relationship_field: "value" 32 | } 33 | list_relationship: [ 34 | { 35 | relationship_field: "value" 36 | } 37 | { 38 | relationship_field: "value" 39 | } 40 | ] 41 | } 42 | """ 43 | formats = ['json', _CONTENT_TYPE] 44 | extra_headers = {'Content-Type': _CONTENT_TYPE} 45 | 46 | @property 47 | def formatted_body(self): 48 | """ 49 | :return: The formatted body that should be returned. 50 | It's just a ``json.dumps`` of the properties and 51 | relationships 52 | :rtype: unicode 53 | """ 54 | response = dict() 55 | parent_properties = self.resource.properties.copy() 56 | self._append_relationships_to_list(response, self.resource.related_resources) 57 | self._append_relationships_to_list(response, self.resource.linked_resources) 58 | response.update(parent_properties) 59 | return json.dumps({self.resource.resource_name: response}) 60 | 61 | @staticmethod 62 | def _append_relationships_to_list(rel_dict, relationships): 63 | """ 64 | Dumps the relationship resources provided into 65 | a json ready list of dictionaries. Side effect 66 | of updating the dictionary with the relationships 67 | 68 | :param dict rel_dict: 69 | :param list relationships: 70 | :return: A list of the resources in dictionary format. 71 | :rtype: list 72 | """ 73 | for resource, name, embedded in relationships: 74 | if name not in rel_dict: 75 | rel_dict[name] = [] 76 | if isinstance(resource, (list, tuple)): 77 | for res in resource: 78 | rel_dict[name].append(res.properties) 79 | continue 80 | rel_dict[name].append(resource.properties) 81 | 82 | @classmethod 83 | def format_exception(cls, exc): 84 | """ 85 | Takes an exception and appropriately formats 86 | the response. By default it just returns a json dump 87 | of the status code and the exception message. 88 | Any exception that does not have a status_code attribute 89 | will have a status_code of 500. 90 | 91 | :param Exception exc: The exception to format. 92 | :return: A tuple containing: response body, format, 93 | http response code 94 | :rtype: tuple 95 | """ 96 | status_code = getattr(exc, 'status_code', 500) 97 | body = json.dumps(dict(status=status_code, message=six.text_type(exc))) 98 | return body, cls.formats[0], status_code 99 | 100 | @classmethod 101 | def format_request(cls, request): 102 | """ 103 | Simply returns request 104 | 105 | :param RequestContainer request: The request to handler 106 | :rtype: RequestContainer 107 | """ 108 | return request 109 | -------------------------------------------------------------------------------- /ripozo/adapters/hal.py: -------------------------------------------------------------------------------- 1 | """ 2 | HAL adapter. See `HAL Specification `_ 3 | """ 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | from ripozo.adapters import AdapterBase 10 | 11 | import json 12 | import six 13 | 14 | _CONTENT_TYPE = 'application/hal+json' 15 | 16 | 17 | class HalAdapter(AdapterBase): 18 | """ 19 | An adapter that formats the response in the HAL format. 20 | A description of the HAL format can be found here: 21 | `HAL Specification `_ 22 | """ 23 | formats = ['hal', _CONTENT_TYPE] 24 | extra_headers = {'Content-Type': _CONTENT_TYPE} 25 | 26 | @property 27 | def formatted_body(self): 28 | """ 29 | :return: The response body for the resource. 30 | :rtype: unicode 31 | """ 32 | response = self._construct_resource(self.resource) 33 | return json.dumps(response) 34 | 35 | def _construct_resource(self, resource): 36 | """ 37 | Constructs a full resource. This can be used 38 | for either the primary resource or embedded resources 39 | 40 | :param ripozo.resources.resource_base.ResourceBase resource: The resource 41 | that will be constructed. 42 | :return: The resource represented according to the 43 | Hal specification 44 | :rtype: dict 45 | """ 46 | resource_url = self.combine_base_url_with_resource_url(resource.url) 47 | parent_properties = resource.properties.copy() 48 | 49 | embedded, links = self.generate_relationship(resource.related_resources) 50 | embedded2, links2 = self.generate_relationship(resource.linked_resources) 51 | embedded.update(embedded2) 52 | links.update(links2) 53 | links.update(dict(self=dict(href=resource_url))) 54 | 55 | response = dict(_links=links, _embedded=embedded) 56 | response.update(parent_properties) 57 | return response 58 | 59 | def generate_relationship(self, relationship_list): 60 | """ 61 | Generates an appropriately formated embedded relationship 62 | in the HAL format. 63 | 64 | :param ripozo.viewsets.relationships.relationship.BaseRelationship relationship: The 65 | relationship that an embedded version is being created for. 66 | :return: If it is a ListRelationship it will return a list/collection of the 67 | embedded resources. Otherwise it returns a dictionary as specified 68 | by the HAL specification. 69 | :rtype: list|dict 70 | """ 71 | # TODO clean this shit up. 72 | embedded_dict = {} 73 | links_dict = {} 74 | for relationship, field_name, embedded in relationship_list: 75 | rel = self._generate_relationship(relationship, embedded) 76 | if not rel: 77 | continue 78 | if embedded: 79 | embedded_dict[field_name] = rel 80 | else: 81 | links_dict[field_name] = rel 82 | return embedded_dict, links_dict 83 | 84 | def _generate_relationship(self, relationship, embedded): 85 | """ 86 | Properly formats the relationship in a HAL ready format. 87 | 88 | :param ResourceBase|list[ResourceBase] relationship: The 89 | ResourceBase instance or list of resource bases. 90 | :param bool embedded: Whether or not the related resource 91 | should be embedded. 92 | :return: A list of dictionaries or dictionary representing 93 | the relationship(s) 94 | :rtype: list|dict 95 | """ 96 | if isinstance(relationship, list): 97 | response = [] 98 | for res in relationship: 99 | if not res.has_all_pks: 100 | continue 101 | response.append(self._generate_relationship(res, embedded)) 102 | return response 103 | if not relationship.has_all_pks: 104 | return 105 | if embedded: 106 | return self._construct_resource(relationship) 107 | else: 108 | return dict(href=relationship.url) 109 | 110 | @classmethod 111 | def format_exception(cls, exc): 112 | """ 113 | Takes an exception and appropriately formats 114 | the response. By default it just returns a json dump 115 | of the status code and the exception message. 116 | Any exception that does not have a status_code attribute 117 | will have a status_code of 500. 118 | 119 | :param Exception exc: The exception to format. 120 | :return: A tuple containing: response body, format, 121 | http response code 122 | :rtype: tuple 123 | """ 124 | status_code = getattr(exc, 'status_code', 500) 125 | body = json.dumps(dict(status=status_code, message=six.text_type(exc), 126 | _embedded={}, _links={})) 127 | return body, cls.formats[0], status_code 128 | 129 | @classmethod 130 | def format_request(cls, request): 131 | """ 132 | Simply returns request 133 | 134 | :param RequestContainer request: The request to handler 135 | :rtype: RequestContainer 136 | """ 137 | return request 138 | -------------------------------------------------------------------------------- /ripozo/adapters/siren.py: -------------------------------------------------------------------------------- 1 | """ 2 | Siren protocol adapter. See `SIREN specification `_. 3 | """ 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | from ripozo.adapters import AdapterBase 10 | from ripozo.utilities import titlize_endpoint 11 | from ripozo.resources.resource_base import create_url 12 | from ripozo.resources.constants import input_categories 13 | 14 | import json 15 | import six 16 | 17 | 18 | _CONTENT_TYPE = 'application/vnd.siren+json' 19 | 20 | 21 | class SirenAdapter(AdapterBase): 22 | """ 23 | An adapter that formats the response in the SIREN format. 24 | A description of a SIREN format can be found here: 25 | `SIREN specification `_ 26 | """ 27 | formats = [_CONTENT_TYPE, 'siren'] 28 | extra_headers = {'Content-Type': _CONTENT_TYPE} 29 | 30 | @property 31 | def formatted_body(self): 32 | """ 33 | Gets the formatted body of the response in unicode form. 34 | If ``self.status_code == 204`` then this will 35 | return an empty string. 36 | 37 | :return: The siren formatted response body 38 | :rtype: unicode 39 | """ 40 | # 204's are supposed to be empty responses 41 | if self.status_code == 204: 42 | return '' 43 | 44 | links = self.generate_links() 45 | 46 | entities = self.get_entities() 47 | response = dict(properties=self.resource.properties, actions=self._actions, 48 | links=links, entities=entities) 49 | 50 | # need to do this separately since class is a reserved keyword 51 | response['class'] = [self.resource.resource_name] 52 | return json.dumps(response) 53 | 54 | @property 55 | def _actions(self): 56 | """ 57 | Gets the list of actions in an appropriately SIREN format 58 | 59 | :return: The list of actions 60 | :rtype: list 61 | """ 62 | actions = [] 63 | for endpoint, options in six.iteritems(self.resource.endpoint_dictionary()): 64 | options = options[0] 65 | all_methods = options.get('methods', ('GET',)) 66 | meth = all_methods[0] if all_methods else 'GET' 67 | base_route = options.get('route', self.resource.base_url) 68 | route = create_url(base_route, **self.resource.properties) 69 | route = self.combine_base_url_with_resource_url(route) 70 | fields = self.generate_fields_for_endpoint_funct(options.get('endpoint_func')) 71 | actn = dict(name=endpoint, title=titlize_endpoint(endpoint), 72 | method=meth, href=route, fields=fields) 73 | actions.append(actn) 74 | return actions 75 | 76 | def generate_fields_for_endpoint_funct(self, endpoint_func): 77 | """ 78 | Returns the action's fields attribute in a SIREN 79 | appropriate format. 80 | 81 | :param apimethod endpoint_func: 82 | :return: A dictionary of action fields 83 | :rtype: dict 84 | """ 85 | return_fields = [] 86 | fields_method = getattr(endpoint_func, 'fields', None) 87 | if not fields_method: 88 | return [] 89 | fields = fields_method(self.resource.manager) 90 | 91 | for field in fields: 92 | if field.arg_type is input_categories.URL_PARAMS: 93 | continue 94 | field_dict = dict(name=field.name, type=field.field_type.__name__, 95 | location=field.arg_type, required=field.required) 96 | return_fields.append(field_dict) 97 | return return_fields 98 | 99 | def generate_links(self): 100 | """ 101 | Generates the Siren links for the resource. 102 | 103 | :return: The list of Siren formatted links. 104 | :rtype: list 105 | """ 106 | href = self.combine_base_url_with_resource_url(self.resource.url) 107 | links = [dict(rel=['self'], href=href)] 108 | for link, link_name, embedded in self.resource.linked_resources: 109 | links.append(dict(rel=[link_name], 110 | href=self.combine_base_url_with_resource_url(link.url))) 111 | return links 112 | 113 | def get_entities(self): 114 | """ 115 | Gets a list of related entities in an appropriate SIREN format 116 | 117 | :return: A list of entities 118 | :rtype: list 119 | """ 120 | entities = [] 121 | for resource, name, embedded in self.resource.related_resources: 122 | for ent in self.generate_entity(resource, name, embedded): 123 | entities.append(ent) 124 | return entities 125 | 126 | def generate_entity(self, resource, name, embedded): 127 | """ 128 | A generator that yields entities 129 | """ 130 | if isinstance(resource, list): 131 | for res in resource: 132 | for ent in self.generate_entity(res, name, embedded): 133 | yield ent 134 | else: 135 | if not resource.has_all_pks: 136 | return 137 | ent = {'class': [resource.resource_name], 'rel': [name]} 138 | resource_url = self.combine_base_url_with_resource_url(resource.url) 139 | if not embedded: 140 | ent['href'] = resource_url 141 | else: 142 | ent['properties'] = resource.properties 143 | ent['links'] = [dict(rel=['self'], href=resource_url)] 144 | yield ent 145 | 146 | @classmethod 147 | def format_exception(cls, exc): 148 | """ 149 | Takes an exception and appropriately formats it 150 | in the siren format. Mostly. It doesn't return 151 | a self in this circumstance. 152 | 153 | :param Exception exc: The exception to format. 154 | :return: A tuple containing: response body, format, 155 | http response code 156 | :rtype: tuple 157 | """ 158 | status_code = getattr(exc, 'status_code', 500) 159 | body = {'class': ['exception', exc.__class__.__name__], 160 | 'actions': [], 'entities': [], 'links': [], 161 | 'properties': dict(status=status_code, message=six.text_type(exc))} 162 | return json.dumps(body), cls.formats[0], status_code 163 | 164 | @classmethod 165 | def format_request(cls, request): 166 | """ 167 | Simply returns request 168 | 169 | :param RequestContainer request: The request to handler 170 | :rtype: RequestContainer 171 | """ 172 | return request 173 | -------------------------------------------------------------------------------- /ripozo/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the exceptions 3 | that ripozo explicitly uses. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | __author__ = 'Tim Martin' 11 | 12 | 13 | class RestException(Exception): 14 | """ 15 | The base exception for any of the package 16 | specific exceptions 17 | """ 18 | def __init__(self, message=None, status_code=500, *args, **kwargs): 19 | super(RestException, self).__init__(message, *args, **kwargs) 20 | self.status_code = status_code 21 | 22 | 23 | class ManagerException(RestException): 24 | """ 25 | A base exception for when the manager has an exception specific 26 | to it. For example, not finding a model. 27 | """ 28 | pass 29 | 30 | 31 | class NotFoundException(ManagerException): 32 | """ 33 | This exception is raised when the manager can't 34 | find a model that was requested. 35 | """ 36 | def __init__(self, message, status_code=404, *args, **kwargs): 37 | super(NotFoundException, self).__init__(message, status_code=status_code, *args, **kwargs) 38 | 39 | 40 | class FieldException(RestException, ValueError): 41 | """ 42 | An exception specifically for Field errors. Specifically, 43 | when validation or casting fail. 44 | """ 45 | def __init__(self, message, status_code=400, *args, **kwargs): 46 | super(FieldException, self).__init__(message, status_code=status_code, *args, **kwargs) 47 | 48 | 49 | class ValidationException(FieldException): 50 | """ 51 | An exception for when validation fails on a field. 52 | """ 53 | pass 54 | 55 | 56 | class TranslationException(ValidationException): 57 | """ 58 | An exception that is raised when casting fails on 59 | a field. 60 | """ 61 | pass 62 | 63 | 64 | class DispatchException(RestException): 65 | """ 66 | An exception for when something is wrong with the Dispatcher 67 | """ 68 | pass 69 | 70 | 71 | class AdapterFormatAlreadyRegisteredException(DispatchException): 72 | """ 73 | An exception that is raised when an adapter format has already 74 | been register with the adapter instance. This is done 75 | to prevent accidental overrides of format types. 76 | """ 77 | pass 78 | 79 | 80 | class JSONAPIFormatException(RestException): 81 | """ 82 | This exception is raised when a request is not 83 | properly formatted according to the 84 | `JSON API specification `_ 85 | """ 86 | def __init__(self, message, status_code=400, *args, **kwargs): 87 | super(JSONAPIFormatException, self).__init__(message, status_code=status_code, *args, **kwargs) 88 | -------------------------------------------------------------------------------- /ripozo/resources/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The resource package is where the bulk of 3 | the ripozo specific stuff is. 4 | """ 5 | from ripozo.resources.resource_base import ResourceBase 6 | -------------------------------------------------------------------------------- /ripozo/resources/constants/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module for the various constant values 3 | used in ripozo. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | __author__ = 'Tim Martin' 11 | -------------------------------------------------------------------------------- /ripozo/resources/constants/input_categories.py: -------------------------------------------------------------------------------- 1 | """ 2 | The various argument type names. 3 | These refer to request argument types. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | QUERY_ARGS = 'query_args' 11 | BODY_ARGS = 'body_args' 12 | URL_PARAMS = 'url_params' 13 | -------------------------------------------------------------------------------- /ripozo/resources/constructor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The class constructor for resources. 3 | """ 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | import logging 10 | import warnings 11 | 12 | _logger = logging.getLogger(__name__) 13 | 14 | 15 | class ResourceMetaClass(type): 16 | """ 17 | A metaclass that is used for registering ResourceBase 18 | and its subclasses It is primarily responsible 19 | for creating a registry of the available Resources 20 | so that they can map relationships and links. 21 | 22 | :param dict registered_resource_classes: A dictionary mapping 23 | the classes instantiated by this meta class to their 24 | base_urls 25 | :param dict registered_names_map: A dictionary mapping the names 26 | of the classes to the actual instances of this meta class 27 | """ 28 | registered_resource_classes = {} 29 | registered_names_map = {} 30 | registered_resource_names_map = {} 31 | 32 | def __new__(mcs, name, bases, attrs): 33 | """ 34 | The instantiator for the metaclass. This 35 | is responsible for creating the actual class 36 | itself. 37 | 38 | :return: The class 39 | :rtype: type 40 | """ 41 | _logger.debug('ResourceMetaClass "%s" class being created', name) 42 | klass = super(ResourceMetaClass, mcs).__new__(mcs, name, bases, attrs) 43 | if attrs.get('__abstract__', False) is True: # Don't register endpoints of abstract classes 44 | _logger.debug('ResourceMetaClass "%s" is abstract. Not being registered', name) 45 | return klass 46 | mcs.register_class(klass) 47 | 48 | _logger.debug('ResourceMetaClass "%s" successfully registered', name) 49 | return klass 50 | 51 | @classmethod 52 | def register_class(mcs, klass): 53 | """ 54 | Checks if the class is in the registry 55 | and adds it to the registry if the classes base_url 56 | is not in it. Otherwise it raises a BaseRestEndpointAlreadyExists 57 | exception so as not to offer multiple endpoints for the same base_url 58 | 59 | :param klass: The class to register 60 | :raises: BaseRestEndpointAlreadyExists 61 | """ 62 | mcs.registered_resource_classes[klass] = klass.base_url 63 | if klass.__name__ in mcs.registered_names_map: 64 | warnings.warn('A class with the name {0} has already been registered.' 65 | 'Overwriting that class'.format(klass.__name__), UserWarning) 66 | mcs.registered_names_map[klass.__name__] = klass 67 | resource_name = getattr(klass, 'resource_name', klass.__name__) 68 | mcs.registered_resource_names_map[resource_name] = klass 69 | -------------------------------------------------------------------------------- /ripozo/resources/fields/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the classes for converting 3 | incoming fields into their appropriate 4 | type and validating them. 5 | """ 6 | from __future__ import absolute_import 7 | from __future__ import division 8 | from __future__ import print_function 9 | from __future__ import unicode_literals 10 | 11 | from ripozo.resources.fields.base import BaseField 12 | from ripozo.resources.fields.common import StringField, IntegerField, \ 13 | BooleanField, DateTimeField, FloatField, ListField, DictField 14 | -------------------------------------------------------------------------------- /ripozo/resources/fields/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the base field that 3 | every other field should inherits from.. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | import warnings 11 | 12 | from ripozo.resources.fields.validations import validate_type, validate_size,\ 13 | translate_iterable_to_single, validate_required, basic_validation 14 | 15 | 16 | class BaseField(object): 17 | """ 18 | The BaseField class is simply an abstract base class 19 | that defines the necessary methods for casting and 20 | validating a field. 21 | """ 22 | field_type = object 23 | 24 | def __init__(self, name, required=False, maximum=None, 25 | minimum=None, arg_type=None, error_message=None): 26 | warnings.warn('The BaseField class is deprecated and will' 27 | 'be removed in v2.0.0. Please use' 28 | ' the ripozo.resources.fields.field.Field class.', DeprecationWarning) 29 | self.name = name 30 | self.required = required 31 | self.maximum = maximum 32 | self.minimum = minimum 33 | self.arg_type = arg_type 34 | self.error_message = error_message 35 | 36 | def translate(self, obj, skip_required=False, validate=False): 37 | """ 38 | A shortcut method to _translate and _validate the object 39 | that is being passed in. It returns this object 40 | or raises a ValueError. 41 | 42 | :param object obj: 43 | :return: The translated and validated object 44 | :rtype: object 45 | :raises: ripozo.exceptions.ValidationsException 46 | :raises: ripozo.exceptions.TranslationException 47 | """ 48 | obj = self._translate(obj, skip_required=skip_required) 49 | if validate: 50 | obj = self._validate(obj, skip_required=skip_required) 51 | return obj 52 | 53 | def _translate(self, obj, skip_required=False): 54 | """ 55 | This method is responsible for translating an input 56 | string or object. 57 | 58 | :param object obj: The input from the request 59 | that is being translated 60 | :param bool skip_required: This is being ignored for now 61 | :return: The object in the appropriate form 62 | :rtype: object 63 | :raises: ripozo.exceptions.TranslationException 64 | """ 65 | return translate_iterable_to_single(obj) 66 | 67 | def _validate(self, obj, skip_required=False): 68 | """ 69 | An object to be validated 70 | 71 | :param object obj: 72 | :param bool skip_required: If this is set to True 73 | then the required statement will be skipped 74 | regardless of whether the field is actually 75 | required or not. This is useful for circumstances 76 | where you want validations to run and the field is 77 | normally required but not in this case. See the 78 | restmixins.Update for an example. 79 | :return: The same exact object simple validated. 80 | :rtype: object 81 | :raises: ripozo.exceptions.ValidationException 82 | """ 83 | return basic_validation(self, obj, skip_required=False) 84 | 85 | def _validate_required(self, obj, skip_required=False): 86 | """ 87 | Deteremines whether validation should be skipped because 88 | the input is None and the field is not required. 89 | 90 | :param object obj: 91 | :param bool skip_required: This validation 92 | will be skipped if skip_required is ``True`` 93 | :return: The original object unmodified 94 | :rtype: object 95 | """ 96 | return validate_required(self, obj, skip_required=skip_required) 97 | 98 | def _validate_size(self, obj, obj_size, msg=None): 99 | """ 100 | Validates the size of the object. 101 | 102 | :param Sized obj_size: The size of the object. This must be an object 103 | that is comparable, i.e. it must be comparable via ">" and "<" 104 | operators 105 | :param msg: The message to display if the object fails validation 106 | :type msg: unicode 107 | :returns: The validated object 108 | :rtype: object 109 | :raises: ValidationException 110 | """ 111 | return validate_size(self, obj, obj_size, 112 | minimum=self.minimum, 113 | maximum=self.maximum, 114 | msg=msg) 115 | 116 | def _validate_type(self, obj, msg=None): 117 | """ 118 | Validates that is object matches the classes field_type. 119 | 120 | :param object obj: 121 | :return: The validated object 122 | :rtype: object 123 | """ 124 | return validate_type(self, self.field_type, obj, msg=msg) 125 | 126 | 127 | def translate_fields(request, fields=None, skip_required=False, validate=False): 128 | """ 129 | Performs the specified action on the field. The action can be a string of 130 | either _translate, _validate, or translate. 131 | 132 | :param RequestContainer request: The request that you are attempting to 133 | translate from. 134 | :param list fields: The list of BaseField instances that are supposed 135 | to be validated. Only items in this list will be translated 136 | and validated 137 | :param bool skip_required: A flag that indicates the required fields 138 | are not required. This is helpful for updates where fields are not 139 | usually required. 140 | :param bool validate: A flag that indicates whether the field validations 141 | should be run. If not, it will just translate the fields. 142 | :return: Returns the translated url_params, query_args and body_args 143 | :rtype: tuple 144 | :raises: RestException 145 | :raises: ValidationException 146 | :raises: TranslationException 147 | """ 148 | updated_url_params = request.url_params 149 | updated_query_args = request.query_args 150 | updated_body_args = request.body_args 151 | fields = fields or [] 152 | for field in fields: 153 | field_name_in_request = field.name in request 154 | if not field_name_in_request and skip_required: 155 | continue 156 | field_value = field.translate(request.get(field.name, None, location=field.arg_type), 157 | skip_required=skip_required, validate=validate) 158 | if field_name_in_request: 159 | request.set(field.name, field_value, location=field.arg_type) 160 | 161 | return updated_url_params, updated_query_args, updated_body_args 162 | -------------------------------------------------------------------------------- /ripozo/resources/fields/field.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import abc 7 | 8 | import six 9 | 10 | from ripozo.resources.fields.validations import basic_validation, translate_iterable_to_single 11 | 12 | 13 | @six.add_metaclass(abc.ABCMeta) 14 | class IField(object): 15 | """ 16 | The BaseField class is simply an abstract base class 17 | that defines the necessary methods for casting and 18 | validating a field. 19 | """ 20 | field_type = object 21 | 22 | def __init__(self, name, required=False, error_message=None, arg_type=None): 23 | self.name = name 24 | self.required = required 25 | self.error_message = error_message 26 | self.arg_type = arg_type 27 | 28 | def translate(self, obj, skip_required=False, validate=False): 29 | """ 30 | A shortcut method to _translate and _validate the object 31 | that is being passed in. It returns this object 32 | or raises a ValueError. 33 | 34 | :param object obj: The object to translate and validate 35 | if validate is ``True`` 36 | :param bool skip_required: A boolean indicating whether 37 | the required validation should be skipped 38 | :param bool validate: If ``True`` the validations 39 | will be run. Otherwise the field will not be 40 | validated. 41 | :return: The translated and validated object 42 | :rtype: object 43 | :raises: ripozo.exceptions.ValidationsException 44 | :raises: ripozo.exceptions.TranslationException 45 | """ 46 | if obj is not None: # None objects should be handled by validation 47 | obj = self._translate(obj, skip_required=skip_required) 48 | if validate: 49 | obj = self._validate(obj, skip_required=skip_required) 50 | return obj 51 | 52 | @abc.abstractmethod 53 | def _translate(self, obj, skip_required=False): 54 | raise NotImplementedError 55 | 56 | @abc.abstractmethod 57 | def _validate(self, obj, skip_required=False): 58 | raise NotImplementedError 59 | 60 | 61 | class Field(IField): 62 | """ 63 | The BaseField class is simply an abstract base class 64 | that defines the necessary methods for casting and 65 | validating a field. 66 | """ 67 | field_type = object 68 | 69 | def _translate(self, obj, skip_required=False): 70 | """ 71 | This method is responsible for translating an input 72 | string or object. 73 | 74 | :param object obj: The input from the request 75 | that is being translated 76 | :param bool skip_required: This is being ignored for now 77 | :return: The object in the appropriate form 78 | :rtype: object 79 | :raises: ripozo.exceptions.TranslationException 80 | """ 81 | return translate_iterable_to_single(obj) 82 | 83 | def _validate(self, obj, skip_required=False): 84 | """ 85 | An object to be validated 86 | 87 | :param object obj: 88 | :param bool skip_required: If this is set to True 89 | then the required statement will be skipped 90 | regardless of whether the field is actually 91 | required or not. This is useful for circumstances 92 | where you want validations to run and the field is 93 | normally required but not in this case. See the 94 | restmixins.Update for an example. 95 | :return: The same exact object simple validated. 96 | :rtype: object 97 | :raises: ripozo.exceptions.ValidationException 98 | """ 99 | return basic_validation(self, obj, skip_required=skip_required) 100 | -------------------------------------------------------------------------------- /ripozo/resources/fields/validations.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.exceptions import ValidationException 7 | 8 | 9 | def translate_iterable_to_single(obj): 10 | """ 11 | Retrieves the first item from the list if the object 12 | is a list or set. Otherwise, it simply returns the object. 13 | 14 | :param object obj: 15 | :return: The original object if it was not a list 16 | or set, otherwise the first item in the list or set 17 | :rtype: object 18 | """ 19 | if isinstance(obj, (list, set)): 20 | return obj[0] if obj else None 21 | return obj 22 | 23 | 24 | def validate_required(field, obj, skip_required=False): 25 | """ 26 | Validates if the item is not None or not required 27 | 28 | :param object obj: The object to validate 29 | :return: The original object 30 | :rtype: object 31 | :raises: ValidationException 32 | """ 33 | if field.required and obj is None and skip_required is False: 34 | raise ValidationException(field.error_message or 'The field "{0}" is required ' 35 | 'and cannot be None'.format(field.name)) 36 | return obj 37 | 38 | 39 | def validate_type(field, field_type, obj, msg=None): 40 | """ 41 | Validates that is object matches the classes field_type. 42 | 43 | :param ripozo.resource.fields.base.BaseField field: The field 44 | that specifies the validation 45 | :param type field_type: The type that the object must be 46 | :param object obj: The object that is being validated 47 | :param unicode msg: The error message to use if field.error_message 48 | is ``None``. 49 | :return: The validated object 50 | :rtype: object 51 | """ 52 | if isinstance(obj, field_type): 53 | return obj 54 | if msg is None: 55 | msg = ("obj is not a valid type for field {0}. A type of" 56 | " {1} is required.".format(field.name, field_type)) 57 | raise ValidationException(field.error_message or msg) 58 | 59 | 60 | def validate_size(field, obj, obj_size, minimum=None, maximum=None, msg=None): 61 | """ 62 | Validates the size of the object. 63 | 64 | :param ripozo.resources.fields.field.IField field: The field that 65 | is being validated 66 | :param object obj: The object that needs to match the specifications 67 | of the field 68 | :param Sized obj_size: The size of the object. This must be an object 69 | that is comparable, i.e. it must be comparable via ">" and "<" 70 | operators 71 | :param Sized minimum: The minimum value for the object 72 | :param Sized maximum: The maximum value for the object 73 | :param unicode msg: The message to display if the object fails validation 74 | :returns: The validated object 75 | :rtype: object 76 | :raises: ValidationException 77 | """ 78 | if minimum and obj_size < minimum: 79 | if not msg: 80 | msg = ('The input was too small for the field {2}: ' 81 | '{0} < {1}'.format(obj_size, minimum, field.name)) 82 | raise ValidationException(field.error_message or msg) 83 | if maximum and obj_size > maximum: 84 | if not msg: 85 | msg = ('The input was too large for the field {2}: ' 86 | '{0} > {1}'.format(obj_size, maximum, field.name)) 87 | raise ValidationException(field.error_message or msg) 88 | return obj 89 | 90 | 91 | def validate_regex(field, obj, regex=None): 92 | """ 93 | Validates a string against a regular expression. 94 | The regular expression must be compiled. This 95 | validation is skipped if the regex is None 96 | 97 | :param ripozo.resources.fields.field.IField field: 98 | :param _sre.SRE_Pattern regex: A compiled regular expression that must 99 | match at least once. 100 | :param unicode obj: The string to validate 101 | :return: The original object 102 | :rtype: object 103 | """ 104 | if regex and not regex.match(obj): 105 | msg = field.error_message or ('The input string for the field ' 106 | '{2} did not match the' 107 | ' required regex: {0} != {1}' 108 | ''.format(obj, regex.pattern, field.name)) 109 | raise ValidationException(msg) 110 | return obj 111 | 112 | 113 | def basic_validation(field, obj, skip_required=False): 114 | """ 115 | Validates if the object exists for a required field 116 | and if the object is of the correct type. 117 | 118 | :param ripozo.resources.fields.field.IField field: The field object 119 | that contains the necessary parameters for validating a field. 120 | :param object obj: 121 | :param bool skip_required: If this is set to True 122 | then the required statement will be skipped 123 | regardless of whether the field is actually 124 | required or not. This is useful for circumstances 125 | where you want validations to run and the field is 126 | normally required but not in this case. See the 127 | restmixins.Update for an example. 128 | :return: The same exact object simple validated. 129 | :rtype: object 130 | :raises: ripozo.exceptions.ValidationException 131 | """ 132 | obj = validate_required(field, obj, skip_required=skip_required) 133 | if obj is None: 134 | return obj 135 | obj = validate_type(field, field.field_type, obj) 136 | return obj 137 | -------------------------------------------------------------------------------- /ripozo/resources/relationships/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The package containing the various relationship 3 | types. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | from ripozo.resources.relationships.list_relationship import ListRelationship 11 | from ripozo.resources.relationships.relationship import Relationship, FilteredRelationship 12 | -------------------------------------------------------------------------------- /ripozo/resources/relationships/list_relationship.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the list relationship class. 3 | Extends the relationship class slightly. 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | from ripozo.resources.relationships.relationship import Relationship 11 | from ripozo.utilities import get_or_pop 12 | 13 | 14 | class ListRelationship(Relationship): 15 | """ 16 | Special case for a list of relationships. 17 | """ 18 | 19 | def construct_resource(self, properties): 20 | """ 21 | Takes a list of properties and returns a generator that 22 | yields Resource instances. These related ResourceBase subclass 23 | will be asked to construct an instance with the keyword argument 24 | properties equal to each item in the list of properties provided to 25 | this function. 26 | 27 | :param dict properties: A dictionary of the properties 28 | on the parent model. The list_name provided in the construction 29 | of an instance of this class is used to find the list that will be 30 | iterated over to generate the resources. 31 | :return: A generator that yields the relationships. 32 | :rtype: types.GeneratorType 33 | """ 34 | objects = get_or_pop(properties, self.name, [], pop=self.remove_properties) 35 | resources = [] 36 | for obj in objects: 37 | res = self.relation(properties=obj, query_args=self.query_args, 38 | include_relationships=self.embedded) 39 | resources.append(res) 40 | return resources 41 | -------------------------------------------------------------------------------- /ripozo/resources/request.py: -------------------------------------------------------------------------------- 1 | """ 2 | The RequestContainer. 3 | """ 4 | from __future__ import absolute_import 5 | from __future__ import division 6 | from __future__ import print_function 7 | from __future__ import unicode_literals 8 | 9 | from ripozo.resources.constants import input_categories 10 | 11 | 12 | class RequestContainer(object): 13 | """ 14 | An object that represents an incoming request. 15 | This is done primarily to keep the data in one 16 | place and to make a generically accessible object. 17 | It should be assumed that no parameter is required 18 | and no property is guaranteed. 19 | """ 20 | 21 | def __init__(self, url_params=None, query_args=None, body_args=None, headers=None, method=None): 22 | """ 23 | Create a new request container. Typically this is constructed 24 | in the dispatcher. 25 | 26 | :param dict url_params: The url parameters that are a part of the 27 | request. These are the variable parts of the url. For example, 28 | a request with /resource/ would have the id as a url_param 29 | :param dict query_args: The query args are were in the request. They 30 | should be a adictionary 31 | :param dict body_args: The arguments in the body. 32 | :param dict headers: A dictionary of the headers and their values 33 | :param unicode method: The method that was used to make 34 | the request. 35 | """ 36 | self._url_params = url_params or {} 37 | self._query_args = query_args or {} 38 | self._body_args = body_args or {} 39 | self._headers = headers or {} 40 | self.method = method 41 | 42 | @property 43 | def url_params(self): 44 | """ 45 | :return: A copy of the url_params dictionary 46 | :rtype: dict 47 | """ 48 | return self._url_params.copy() 49 | 50 | @url_params.setter 51 | def url_params(self, value): 52 | self._url_params = value 53 | 54 | @property 55 | def query_args(self): 56 | """ 57 | :return: A copy of the query_args 58 | :rtype: dict 59 | """ 60 | return self._query_args.copy() 61 | 62 | @query_args.setter 63 | def query_args(self, value): 64 | self._query_args = value 65 | 66 | @property 67 | def body_args(self): 68 | """ 69 | :return: a copy of the body_args 70 | :rtype: dict 71 | """ 72 | return self._body_args.copy() 73 | 74 | @body_args.setter 75 | def body_args(self, value): 76 | self._body_args = value 77 | 78 | @property 79 | def headers(self): 80 | """ 81 | :return: A copy of the headers dict 82 | :rtype: dict 83 | """ 84 | return self._headers.copy() 85 | 86 | @headers.setter 87 | def headers(self, value): 88 | self._headers = value 89 | 90 | @property 91 | def content_type(self): 92 | """ 93 | :return: The Content-Type header or None if it is not available in 94 | the headers property on this request object. 95 | :rtype: unicode 96 | """ 97 | return self._headers.get('Content-Type') 98 | 99 | @content_type.setter 100 | def content_type(self, value): 101 | self._headers['Content-Type'] = value 102 | 103 | def get(self, name, default=None, location=None): 104 | """ 105 | Attempts to retrieve the parameter with the 106 | name in the url_params, query_args and then 107 | body_args in that order. Returns the default 108 | if not found. 109 | 110 | :param unicode name: The name of the parameter 111 | to retrieve. From the request 112 | :return: The requested attribute if found 113 | otherwise the default if specified. 114 | :rtype: object 115 | :raises: KeyError 116 | """ 117 | if not location and name in self._url_params or location == input_categories.URL_PARAMS: 118 | return self.url_params.get(name) 119 | elif not location and name in self._query_args or location == input_categories.QUERY_ARGS: 120 | return self._query_args.get(name) 121 | elif not location and name in self._body_args or location == input_categories.BODY_ARGS: 122 | return self._body_args.get(name, default) 123 | return default 124 | 125 | def set(self, name, value, location=None): 126 | """ 127 | Attempts to set the field with the specified name. 128 | in the location specified. Searches through all 129 | the fields if location is not specified. Raises 130 | a KeyError if no location is set and the name is 131 | not found in any of the locations. 132 | 133 | :param unicode name: The name of the field 134 | :param unicode location: The location of the 135 | field to get. I.e. QUERY_ARGS. 136 | :return: The field that was requestedor None. 137 | :rtype: object 138 | """ 139 | if not location and name in self._url_params or location == input_categories.URL_PARAMS: 140 | self._url_params[name] = value 141 | return 142 | elif not location and name in self._query_args or location == input_categories.QUERY_ARGS: 143 | self._query_args[name] = value 144 | return 145 | elif not location and name in self._body_args or location == input_categories.BODY_ARGS: 146 | self._body_args[name] = value 147 | return 148 | raise KeyError('Location was not specified and the parameter {0} ' 149 | 'could not be found on the request object'.format(name)) 150 | 151 | def __contains__(self, item): 152 | """ 153 | Checks if the item is available in any of 154 | the url_params, body_args, or query_args 155 | 156 | :param unicode item: The key to look for in the 157 | various parameter dictionaries. 158 | :return: Whether the object was actually found. 159 | :rtype: bool 160 | """ 161 | if item in self._url_params or item in self._body_args or item in self._query_args: 162 | return True 163 | return False 164 | -------------------------------------------------------------------------------- /ripozo/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Various utilities that ripozo 3 | uses. More or less a junk drawer 4 | """ 5 | from __future__ import absolute_import 6 | from __future__ import division 7 | from __future__ import print_function 8 | from __future__ import unicode_literals 9 | 10 | from functools import wraps 11 | 12 | import datetime 13 | import decimal 14 | import re 15 | import six 16 | 17 | 18 | _FIRST_CAP_RE = re.compile('(.)([A-Z][a-z]+)') 19 | _ALL_CAP_RE = re.compile('([a-z0-9])([A-Z])') 20 | 21 | 22 | def convert_to_underscore(toconvert): 23 | """ 24 | Converts a string from CamelCase to underscore. 25 | 26 | .. code-block:: python 27 | 28 | >>> convert_to_underscore('CamelCase') 29 | 'camel_case' 30 | 31 | :param toconvert: The string to convert from CamelCase to underscore (i.e. camel_case) 32 | :type toconvert: str 33 | :return: The converted string 34 | :rtype: str 35 | """ 36 | intermediate = _FIRST_CAP_RE.sub(r'\1_\2', toconvert) 37 | return _ALL_CAP_RE.sub(r'\1_\2', intermediate).lower() 38 | 39 | 40 | def titlize_endpoint(endpoint): 41 | """ 42 | Capitalizes the endpoint and makes it look like a title 43 | Just to prettify the output of the actions. It capitalizes 44 | the first letter of every word and replaces underscores with 45 | spaces. It is opinionated in how it determines words. It 46 | simply looks for underscores and splits based on that. 47 | 48 | :param unicode endpoint: The endpoint name on the resource 49 | :return: The prettified endpoint name 50 | :rtype: unicode 51 | """ 52 | parts = endpoint.split('_') 53 | parts = (p.capitalize() for p in parts) 54 | endpoint = ' '.join(parts) 55 | return endpoint.strip() 56 | 57 | 58 | def join_url_parts(*parts): 59 | """ 60 | Joins each of the parts with a '/'. 61 | Additionally, it prevents something like 'something/' and 62 | '/another' from turning into 'something//another' instead 63 | it will return 'something/another'. 64 | 65 | .. code-block:: python 66 | 67 | >>> join_url_parts('first', 'second', 'last') 68 | 'first/second/last' 69 | 70 | :param list[unicode|str|int] parts: a list of strings to join together with a '/' 71 | :return: The url 72 | :rtype: unicode 73 | """ 74 | url = None 75 | if not parts: 76 | return '' 77 | parts = [six.text_type(part) for part in parts] 78 | for part in parts: 79 | if url is None: # first case 80 | url = part 81 | continue 82 | url = url.rstrip('/') 83 | part = part.lstrip('/') 84 | url = '{0}/{1}'.format(url, part) 85 | return url 86 | 87 | 88 | def picky_processor(processor, include=None, exclude=None): 89 | """ 90 | A wrapper for pre and post processors that selectively runs 91 | pre and post processors. If the include keyword argument is set, 92 | then any method on the Resource that has the same name as the 93 | processor will be run. Otherwise it will not be run. On the 94 | other hand, if the exclude keyword argument is set then any 95 | method on then this preprocessor will not be run for any method on 96 | the resource that does have the same name as the strings in the 97 | exclude list 98 | 99 | .. code-block:: python 100 | 101 | def my_preprocessor(resource_class, func_name, request): 102 | # Do something 103 | 104 | class MyResource(CRUD): 105 | # Only gets run on create and delete 106 | _preprocessors = [picky_processor(my_preprocessor, include=['create', 'delete'])] 107 | 108 | :param method processor: A pre or post processor on a ResourceBase subclass. 109 | This is the function that will be run if the it passes the include 110 | and exclude parameters 111 | :param list include: A list of name strings that are methods on the class that 112 | for which this processor will be run. 113 | :param list exclude: 114 | :return: The wrapped function that only runs if the include and 115 | exclude parameters are fulfilled. 116 | :rtype: method 117 | """ 118 | @wraps(processor) 119 | def wrapped(cls, function_name, *args, **kwargs): 120 | """ 121 | Selectively runs the preprocessor 122 | """ 123 | run = True 124 | if include and function_name not in include: 125 | run = False 126 | elif exclude and function_name in exclude: 127 | run = False 128 | if run: 129 | return processor(cls, function_name, *args, **kwargs) 130 | return wrapped 131 | 132 | 133 | def make_json_safe(obj): 134 | """ 135 | Makes an object json serializable. 136 | This is designed to take a list or dictionary, 137 | and is fairly limited. This is primarily for 138 | the managers when creating objects. 139 | 140 | :param object obj: 141 | :return: The json safe dictionary. 142 | :rtype: object|six.text_type|list|dict 143 | """ 144 | if isinstance(obj, dict): 145 | for key, value in six.iteritems(obj): 146 | obj[key] = make_json_safe(value) 147 | elif isinstance(obj, (list, set, tuple,)): 148 | response = [] 149 | for val in obj: 150 | response.append(make_json_safe(val)) 151 | return response 152 | elif isinstance(obj, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)): 153 | obj = six.text_type(obj) 154 | elif isinstance(obj, decimal.Decimal): 155 | obj = float(obj) 156 | return obj 157 | 158 | 159 | def get_or_pop(dictionary, key, default=None, pop=False): 160 | """A simple helper for getting or popping a property 161 | from a dictionary depending on the ```pop``` parameter. 162 | 163 | This is a helper method for relationships to easily 164 | update whether they keep or remove items from 165 | the parent 166 | 167 | :param dict dictionary: The dictionary to check 168 | :param object key: The key to look for in the dictionary 169 | :param object default: The default value to return if nothing 170 | is found 171 | :param bool pop: A boolean that removes the item from 172 | the dictionary if true. Otherwise it just gets 173 | the value from the dictionary 174 | :returns: The value of the requested object or the 175 | default if it was not found. 176 | :rtype: object 177 | """ 178 | if pop: 179 | return dictionary.pop(key, default) 180 | return dictionary.get(key, default) 181 | -------------------------------------------------------------------------------- /ripozo_profiling/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /ripozo_profiling/adapters.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.restmixins import CRUDL 7 | from ripozo import fields, RequestContainer 8 | from ripozo.adapters import SirenAdapter, HalAdapter, BoringJSONAdapter 9 | 10 | from ripozo_tests.helpers.inmemory_manager import InMemoryManager 11 | from ripozo_tests.helpers.profile import profileit 12 | 13 | import logging 14 | import six 15 | import unittest2 16 | 17 | class TestRestMixinProfile(unittest2.TestCase): 18 | runs = 10000 19 | 20 | def setUp(self): 21 | logging.disable('DEBUG') 22 | 23 | class MyManager(InMemoryManager): 24 | _fields = ('id', 'first', 'second',) 25 | _field_validators = { 26 | 'first': fields.IntegerField('first', required=True), 27 | 'second': fields.IntegerField('second', required=True) 28 | } 29 | 30 | class MyClass(CRUDL): 31 | resource_name = 'myresource' 32 | manager = MyManager() 33 | 34 | self.resource_class = MyClass 35 | self.manager = MyClass.manager 36 | for i in six.moves.range(100): 37 | self.manager.objects[i] = dict(id=i, first=1, second=2) 38 | self.resource = MyClass.retrieve_list(RequestContainer()) 39 | 40 | @profileit 41 | def test_siren_adapter_formatted_body(self): 42 | adapter = SirenAdapter(self.resource) 43 | for i in six.moves.range(self.runs): 44 | x = adapter.formatted_body 45 | 46 | @profileit 47 | def test_hal_adapter_formatted_body(self): 48 | adapter = HalAdapter(self.resource) 49 | for i in six.moves.range(self.runs): 50 | x = adapter.formatted_body 51 | 52 | # @profileit 53 | # def test_json_adapter_formatted_body(self): 54 | # adapter = BasicJSONAdapter(self.resource) 55 | # for i in range(self.runs): 56 | # x = adapter.formatted_body 57 | -------------------------------------------------------------------------------- /ripozo_profiling/bits_and_pieces.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.utilities import convert_to_underscore 7 | 8 | from ripozo_tests.helpers.profile import profileit 9 | 10 | import six 11 | import unittest2 12 | 13 | 14 | class TestBitsAndPieces(unittest2.TestCase): 15 | runs = 1000000 16 | 17 | @profileit 18 | def test_convert_to_underscore(self): 19 | for i in six.moves.range(self.runs): 20 | convert_to_underscore('MyThingYeah') 21 | -------------------------------------------------------------------------------- /ripozo_profiling/end_to_end.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.restmixins import CRUDL 7 | from ripozo import fields, RequestContainer 8 | from ripozo.adapters import SirenAdapter, HalAdapter, BoringJSONAdapter 9 | 10 | from ripozo_tests.helpers.inmemory_manager import InMemoryManager 11 | from ripozo_tests.helpers.profile import profileit 12 | from ripozo_tests.helpers.dispatcher import FakeDispatcher 13 | 14 | import logging 15 | import six 16 | import unittest2 17 | 18 | 19 | class TestEndToEnd(unittest2.TestCase): 20 | runs = 10000 21 | 22 | def setUp(self): 23 | logging.disable('DEBUG') 24 | 25 | class MyManager(InMemoryManager): 26 | _fields = ('id', 'first', 'second',) 27 | _field_validators = { 28 | 'first': fields.IntegerField('first', required=True), 29 | 'second': fields.IntegerField('second', required=True) 30 | } 31 | 32 | class MyClass(CRUDL): 33 | resource_name = 'myresource' 34 | manager = MyManager() 35 | 36 | self.resource_class = MyClass 37 | self.manager = MyClass.manager 38 | for i in six.moves.range(100): 39 | self.manager.objects[i] = dict(id=i, first=1, second=2) 40 | self.dispatcher = FakeDispatcher() 41 | self.dispatcher.register_adapters(SirenAdapter, HalAdapter, BoringJSONAdapter) 42 | 43 | @profileit 44 | def test_retrieve_list(self): 45 | for i in six.moves.range(100): 46 | self.manager.objects[i] = dict(id=i, first=1, second=2) 47 | req = RequestContainer() 48 | for i in six.moves.range(self.runs): 49 | self.dispatcher.dispatch(self.resource_class.retrieve_list, ['blah', 'blah', 'blah'], req) 50 | -------------------------------------------------------------------------------- /ripozo_profiling/restmixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.restmixins import CRUDL 7 | from ripozo import fields, RequestContainer 8 | 9 | from ripozo_tests.helpers.inmemory_manager import InMemoryManager 10 | from ripozo_tests.helpers.profile import profileit 11 | 12 | import logging 13 | import six 14 | import unittest2 15 | 16 | 17 | class TestRestMixinProfile(unittest2.TestCase): 18 | runs = 10000 19 | 20 | def setUp(self): 21 | logging.disable('DEBUG') 22 | 23 | class MyManager(InMemoryManager): 24 | _fields = ('id', 'first', 'second',) 25 | _field_validators = { 26 | 'first': fields.IntegerField('first', required=True), 27 | 'second': fields.IntegerField('second', required=True) 28 | } 29 | 30 | class MyClass(CRUDL): 31 | manager = MyManager() 32 | resource_name = 'myresource' 33 | 34 | self.resource_class = MyClass 35 | self.manager = MyClass.manager 36 | 37 | @profileit 38 | def test_retrieve(self): 39 | self.manager.objects['1'] = dict(id='1', first=1, second=2) 40 | request = RequestContainer(url_params=dict(id='1')) 41 | for i in six.moves.range(self.runs): 42 | self.resource_class.retrieve(request) 43 | 44 | @profileit 45 | def test_retrieve_list(self): 46 | for i in six.moves.range(100): 47 | self.manager.objects[i] = dict(id=i, first=1, second=2) 48 | req = RequestContainer() 49 | for i in six.moves.range(self.runs): 50 | self.resource_class.retrieve_list(req) 51 | 52 | @profileit 53 | def test_create(self): 54 | req = RequestContainer(body_args=dict(first='1', second='2')) 55 | for i in six.moves.range(self.runs): 56 | self.resource_class.create(req) 57 | 58 | @profileit 59 | def test_update(self): 60 | self.manager.objects['1'] = dict(id='1', first=1, second=2) 61 | req = RequestContainer(url_params=dict(id='1')) 62 | for i in six.moves.range(self.runs): 63 | req._body_args['first'] = i 64 | self.resource_class.update(req) 65 | 66 | def test_delete(self): 67 | ids = six.moves.range(self.runs) 68 | req = RequestContainer(body_args=dict(first='1', second='2')) 69 | for i in ids: 70 | self.manager.objects[i] = dict(id=i, first=1, second=2) 71 | self.actually_delete(ids) 72 | 73 | @profileit 74 | def actually_delete(self, ids): 75 | req = RequestContainer() 76 | for id_ in ids: 77 | req._url_params['id'] = id_ 78 | self.resource_class.delete(req) 79 | -------------------------------------------------------------------------------- /ripozo_tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tim Martin' 2 | import logging 3 | 4 | logging.basicConfig(level=logging.DEBUG, 5 | format='%(asctime)s %(name)s - [%(module)s - %(filename)s ' 6 | '- %(lineno)d - %(levelname)s]: %(message)s') 7 | logger = logging.getLogger(__name__) 8 | 9 | from ripozo_tests import unit, integration -------------------------------------------------------------------------------- /ripozo_tests/bases/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /ripozo_tests/bases/field.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | 7 | class FieldTestBase(object): 8 | field_type = None 9 | instance_type = None 10 | translation_failures = [] 11 | translation_success = [] 12 | validation_exception = None 13 | translation_exception = None 14 | 15 | def test_name(self): 16 | field_name = 'field' 17 | f = self.field_type(field_name) 18 | self.assertEqual(f.name, field_name) 19 | 20 | def test_not_required(self): 21 | f = self.field_type('field', required=False) 22 | obj = f.translate(None, validate=True) 23 | self.assertIsNone(obj) 24 | obj = f.translate(None) 25 | self.assertIsNone(obj) 26 | obj = f.translate(None, validate=True) 27 | self.assertIsNone(obj) 28 | 29 | def test_required(self): 30 | f = self.field_type('field', required=True) 31 | self.assertRaises(self.validation_exception, f.translate, None, validate=True) 32 | self.assertIsNone(f.translate(None)) 33 | 34 | def size_test_helper(self, too_small, valid, too_large, minimum=5, maximum=10): 35 | f = self.field_type('field', minimum=minimum, maximum=maximum) 36 | self.assertRaises(self.validation_exception, f.translate, too_small, validate=True) 37 | 38 | obj = f.translate(valid, validate=True) 39 | self.assertEqual(valid, obj) 40 | 41 | self.assertRaises(self.validation_exception, f.translate, too_large, validate=True) 42 | 43 | def test_translation_failure(self): 44 | f = self.field_type('field') 45 | for failure in self.translation_failures: 46 | self.assertRaises(self.translation_exception, f.translate, failure) 47 | self.assertRaises(self.translation_exception, f.translate, failure, validate=True) 48 | 49 | def test_translation_success(self): 50 | f = self.field_type('field') 51 | for success in self.translation_success: 52 | new = f.translate(success) 53 | self.assertIsInstance(new, self.instance_type) 54 | 55 | def test_translate_none(self): 56 | """Tests whether the field can appropriately handle None, False, etc""" 57 | f = self.field_type('field') 58 | output = f.translate(None) 59 | self.assertIsNone(output) 60 | -------------------------------------------------------------------------------- /ripozo_tests/bases/manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.exceptions import NotFoundException 7 | 8 | import logging 9 | import random 10 | import six 11 | import string 12 | import uuid 13 | 14 | 15 | def logger(): 16 | return logging.getLogger(__name__) 17 | 18 | 19 | def generate_random_name(): 20 | return ''.join(random.choice(string.ascii_letters) for _ in range(15)) 21 | 22 | 23 | class TestManagerMixin(object): 24 | """ 25 | manager, does_not_exist_exception, and all_person_models proeprties need to be implemented 26 | get_person_model_by_id method needs to be implemented 27 | """ 28 | 29 | @property 30 | def manager(self): 31 | """ 32 | 33 | :return: 34 | :rtype: ripozo.managers.base.BaseManager 35 | """ 36 | raise NotImplementedError 37 | 38 | @property 39 | def model_pks(self): 40 | """ 41 | 42 | :return: 43 | :rtype: dict 44 | """ 45 | raise NotImplementedError 46 | 47 | def assertValuesEqualModel(self, model, values): 48 | # TODO docs 49 | raise NotImplementedError 50 | 51 | def assertValuesNotEqualsModel(self, model, values): 52 | # TODO docs 53 | raise NotImplementedError 54 | 55 | def create_model(self, values=None): 56 | # TODO docs 57 | raise NotImplementedError 58 | 59 | def get_model(self, values): 60 | # TODO docs 61 | # Should raise exception when not found 62 | raise NotImplementedError 63 | 64 | def get_model_pks(self, model): 65 | # TODO docs 66 | raise NotImplementedError 67 | 68 | def get_values(self, defaults=None): 69 | # TODO docs 70 | raise NotImplementedError 71 | 72 | def assertResponseValid(self, resp, init_values, valid_fields=None): 73 | valid_fields = valid_fields or self.manager.fields 74 | for key, value in six.iteritems(init_values): 75 | if key not in valid_fields: 76 | self.assertTrue(key not in resp) 77 | else: 78 | self.assertEqual(resp[key], init_values[key]) 79 | 80 | def get_random_pks(self): 81 | pks_dict = dict() 82 | for pk in self.model_pks: 83 | pks_dict[pk] = uuid.uuid4() 84 | return pks_dict 85 | 86 | def test_create(self): 87 | """ 88 | Tests that the model is appropriately created 89 | """ 90 | new_values = self.get_values() 91 | resp = self.manager.create(new_values) 92 | self.assertResponseValid(resp, new_values, valid_fields=self.manager.create_fields) 93 | 94 | def test_retrieve(self): 95 | """ 96 | Tests that a model can be appropriately retrieved 97 | """ 98 | new_values = self.get_values() 99 | model = self.create_model(values=new_values) 100 | pks = self.get_model_pks(model) 101 | resp = self.manager.retrieve(pks) 102 | self.assertResponseValid(resp, new_values) 103 | 104 | def test_retrieve_not_found(self): 105 | """ 106 | Tests that NotFoundException is raised when 107 | a model is not found. 108 | """ 109 | self.assertRaises(NotFoundException, self.manager.retrieve, self.get_random_pks()) 110 | 111 | def test_update(self): 112 | """ 113 | Tests that a model is appropriately updated 114 | """ 115 | values = self.get_values() 116 | old_values = values.copy() 117 | model = self.create_model(values=values) 118 | updated_values = self.get_values() 119 | resp = self.manager.update(self.get_model_pks(model), updated_values) 120 | self.assertValuesEqualModel(model, updated_values) 121 | self.assertValuesNotEqualsModel(model, old_values) 122 | self.assertResponseValid(resp, updated_values) 123 | 124 | def test_update_not_exists(self): 125 | """ 126 | Tests that a DoesNotExistException exception 127 | is raised if the model does not exists 128 | """ 129 | new_values = self.get_values() 130 | pks_dict = self.get_random_pks() 131 | self.assertRaises(NotFoundException, self.manager.update, pks_dict, new_values) 132 | 133 | def test_retrieve_list(self): 134 | """ 135 | Tests that the retrieve_list appropriately 136 | returns a list of resources 137 | """ 138 | new_count = 10 139 | for i in range(new_count): 140 | self.create_model() 141 | resp, meta = self.manager.retrieve_list({}) 142 | if new_count > self.manager.paginate_by: 143 | self.assertEqual(len(resp), self.manager.paginate_by) 144 | else: 145 | self.assertGreaterEqual(len(resp), new_count) 146 | for r in resp: 147 | for key, value in six.iteritems(r): 148 | self.assertIn(key, self.manager.list_fields) 149 | 150 | def test_retrieve_empty_list(self): 151 | """ 152 | Tests that an empty list is returned if 153 | no results match. 154 | """ 155 | pks = self.get_random_pks() 156 | resp, meta = self.manager.retrieve_list(pks) 157 | self.assertEqual(len(resp), 0) 158 | 159 | def test_retrieve_filtering(self): 160 | """ 161 | Tests that the basic filtering works 162 | for the manager 163 | """ 164 | raise NotImplementedError 165 | 166 | def test_retrieve_list_paginations(self): 167 | """ 168 | Tests that the pagination works correctly with 169 | retrieve_list 170 | """ 171 | paginate_by = 3 172 | original = self.manager.paginate_by 173 | self.manager.paginate_by = paginate_by 174 | try: 175 | for i in range(paginate_by * 3 + 1): 176 | self.create_model() 177 | resp, meta = self.manager.retrieve_list({}) 178 | self.assertEqual(len(resp), paginate_by) 179 | links = meta['links'] 180 | self.assertIn('next', links) 181 | next_count = 0 182 | while 'next' in links: 183 | next_count += 1 184 | resp, meta = self.manager.retrieve_list(links['next']) 185 | self.assertLessEqual(len(resp), paginate_by) 186 | links = meta['links'] 187 | 188 | prev_count = 0 189 | self.assertIn('previous', links) 190 | while 'previous' in links: 191 | prev_count += 1 192 | resp, meta = self.manager.retrieve_list(links['previous']) 193 | self.assertLessEqual(len(resp), paginate_by) 194 | links = meta['links'] 195 | 196 | self.assertEqual(prev_count, next_count) 197 | finally: 198 | self.manager.paginate_by = original 199 | 200 | def test_delete(self): 201 | """ 202 | Tests that a resource is deleted appropriately. 203 | """ 204 | model = self.create_model() 205 | model_pks = self.get_model_pks(model) 206 | resp = self.manager.delete(model_pks) 207 | self.assertRaises(Exception, self.get_model, model_pks) 208 | # TODO assert response value 209 | 210 | def test_delete_not_exists(self): 211 | """ 212 | Tests that a DoesNotExistException exception 213 | is raised if the model does not exists 214 | """ 215 | self.assertRaises(NotFoundException, self.manager.delete, self.get_random_pks()) 216 | 217 | def test_get_field(self): 218 | """ 219 | Tests that the 220 | :return: 221 | :rtype: 222 | """ 223 | -------------------------------------------------------------------------------- /ripozo_tests/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /ripozo_tests/helpers/dispatcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.dispatch_base import DispatcherBase 7 | 8 | 9 | class FakeDispatcher(DispatcherBase): 10 | def __init__(self, *args, **kwargs): 11 | self.routes = {} 12 | super(FakeDispatcher, self).__init__(*args, **kwargs) 13 | 14 | def register_route(self, endpoint, **options): 15 | super(FakeDispatcher, self).register_route(endpoint, **options) 16 | current_route = self.routes.get(endpoint) or [] 17 | current_route.append(options) 18 | self.routes[endpoint] = current_route 19 | 20 | @property 21 | def base_url(self): 22 | # Just to get coverage on empty abstract methods 23 | x = super(FakeDispatcher, self).base_url 24 | return 'http://127.0.0.1:7000/' -------------------------------------------------------------------------------- /ripozo_tests/helpers/hello_world_viewset.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.decorators import apimethod, translate 7 | from ripozo.resources.relationships.relationship import Relationship 8 | from ripozo.resources.fields.common import StringField 9 | from ripozo.resources.resource_base import ResourceBase 10 | from ripozo.resources.constructor import ResourceMetaClass 11 | from ripozo_tests.helpers.inmemory_manager import InMemoryManager 12 | 13 | 14 | class MM1(InMemoryManager): 15 | model = 'Something' 16 | _model_name = 'modelname' 17 | 18 | name_space = '/mynamspace/' 19 | 20 | def get_helloworld_viewset(): 21 | class HelloWorldViewset(ResourceBase): 22 | namespace = name_space 23 | manager = MM1() 24 | resource_name = 'myresource' 25 | _relationships = [ 26 | Relationship('related', property_map={'related': 'id'}, relation='ComplimentaryViewset') 27 | ] 28 | 29 | @apimethod(methods=['GET']) 30 | @translate(fields=[StringField('content')], validate=True) 31 | def hello(cls, request, *args, **kwargs): 32 | return cls(properties=request.query_args) 33 | return HelloWorldViewset 34 | 35 | 36 | def get_complementary_viewset(): 37 | class ComplimentaryViewset(ResourceBase): 38 | namespace = name_space 39 | manager = MM1() 40 | resource_name = 'other_resource' 41 | pks = ['id'] 42 | return ComplimentaryViewset 43 | 44 | 45 | def get_refreshed_helloworld_viewset(): 46 | ResourceMetaClass.registered_names_map = {} 47 | ResourceMetaClass.registered_resource_classes = {} 48 | HelloWorldViewset = get_helloworld_viewset() 49 | ComplimentaryViewset = get_complementary_viewset() 50 | return HelloWorldViewset -------------------------------------------------------------------------------- /ripozo_tests/helpers/inmemory_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.exceptions import NotFoundException 7 | from ripozo.manager_base import BaseManager 8 | from ripozo.resources.fields.base import BaseField 9 | from uuid import uuid1 10 | import six 11 | 12 | __author__ = 'Tim Martin' 13 | 14 | 15 | class InMemoryManager(BaseManager): 16 | # TODO redo this suite taking into account the 17 | # TODO TestManagerMixin (i.e. don't use the InMemoryManager) 18 | objects = None 19 | _model_name = 'Fake' 20 | 21 | def __init__(self): 22 | super(InMemoryManager, self).__init__() 23 | self.objects = {} 24 | 25 | def create(self, values, *args, **kwargs): 26 | super(InMemoryManager, self).create(values, *args, **kwargs) 27 | new_id = uuid1() 28 | values['id'] = new_id 29 | self.queryset[new_id] = values 30 | return values 31 | 32 | def retrieve_list(self, filters, *args, **kwargs): 33 | super(InMemoryManager, self).retrieve_list(filters, *args, **kwargs) 34 | pagination_page, filters = self.get_pagination_pks(filters) 35 | if not pagination_page: 36 | pagination_page = 0 37 | pagination_count, filters = self.get_pagination_count(filters) 38 | original_page = pagination_page 39 | values = list(six.itervalues(self.queryset)) 40 | first = pagination_page * pagination_count 41 | last = first + pagination_count 42 | no_next = False 43 | no_prev = False 44 | if last >= len(values): 45 | values = values[first:] 46 | no_next = True 47 | else: 48 | values = values[first:last] 49 | 50 | if first <= 0: 51 | no_prev = True 52 | 53 | links = dict() 54 | if not no_prev: 55 | links[self.pagination_prev] = {self.pagination_pk_query_arg: original_page - 1, 56 | self.pagination_count_query_arg: pagination_count} 57 | if not no_next: 58 | links[self.pagination_next] = {self.pagination_pk_query_arg: original_page + 1, 59 | self.pagination_count_query_arg: pagination_count} 60 | 61 | return values, {'links': links} 62 | 63 | @property 64 | def queryset(self): 65 | return self.objects 66 | 67 | def retrieve(self, lookup_keys, *args, **kwargs): 68 | super(InMemoryManager, self).retrieve(lookup_keys, *args, **kwargs) 69 | return self._get_model(lookup_keys) 70 | 71 | @property 72 | def model_name(self): 73 | return self._model_name 74 | 75 | def update(self, lookup_keys, updates, *args, **kwargs): 76 | super(InMemoryManager, self).update(lookup_keys, updates, *args, **kwargs) 77 | obj = self._get_model(lookup_keys) 78 | for key, value in six.iteritems(updates): 79 | obj[key] = value 80 | self.queryset[lookup_keys['id']] = obj 81 | return obj 82 | 83 | @classmethod 84 | def get_field_type(cls, name): 85 | super(InMemoryManager, cls).get_field_type(name) 86 | return BaseField(name) 87 | 88 | def delete(self, lookup_keys, *args, **kwargs): 89 | super(InMemoryManager, self).delete(lookup_keys, *args, **kwargs) 90 | if 'id' not in lookup_keys or not lookup_keys['id'] in self.queryset: 91 | raise NotFoundException('Blah...') 92 | self.queryset.pop(lookup_keys['id']) 93 | return None 94 | 95 | def _get_model(self, model_id): 96 | obj = self.queryset.get(model_id['id'], None) 97 | if not obj: 98 | raise NotFoundException('Blah...') 99 | return obj 100 | 101 | 102 | class PersonInMemoryManager(InMemoryManager): 103 | model = 'Something' 104 | _model_name = 'Person' -------------------------------------------------------------------------------- /ripozo_tests/helpers/profile.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import cProfile 7 | import pstats 8 | 9 | 10 | def profileit(func): 11 | """ 12 | Decorator straight up stolen from stackoverflow 13 | """ 14 | def wrapper(*args, **kwargs): 15 | datafn = func.__name__ + ".profile" # Name the data file sensibly 16 | prof = cProfile.Profile() 17 | prof.enable() 18 | retval = prof.runcall(func, *args, **kwargs) 19 | prof.disable() 20 | stats = pstats.Stats(prof) 21 | stats.sort_stats('tottime').print_stats(20) 22 | print() 23 | print() 24 | stats.sort_stats('cumtime').print_stats(20) 25 | return retval 26 | 27 | return wrapper 28 | -------------------------------------------------------------------------------- /ripozo_tests/helpers/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import random 7 | import string 8 | 9 | 10 | def random_string(length=50): 11 | return ''.join(random.choice(string.ascii_letters) for _ in range(length)) 12 | -------------------------------------------------------------------------------- /ripozo_tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tim Martin' 2 | 3 | from . import resources -------------------------------------------------------------------------------- /ripozo_tests/integration/manager_base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import mock 7 | import six 8 | import unittest2 9 | 10 | from ripozo.resources.fields.base import BaseField 11 | from ripozo.manager_base import BaseManager 12 | from ripozo_tests.helpers.inmemory_manager import InMemoryManager 13 | 14 | 15 | class TestManager(unittest2.TestCase): 16 | @property 17 | def manager(self): 18 | """ 19 | Return the serializer for the specific implementation 20 | 21 | :rtype: InMemoryManager 22 | """ 23 | return InMemoryManager() 24 | 25 | @property 26 | def does_not_exist_exception(self): 27 | """ 28 | return the exception type that is raised when the model does not exist 29 | """ 30 | return KeyError 31 | 32 | @property 33 | def all_person_models(self): 34 | """ 35 | :return: Every single person model 36 | :rtype: list 37 | """ 38 | return list(six.itervalues(self._manager.objects)) 39 | 40 | def get_person_model_by_id(self, person_id): 41 | """ 42 | Directly query the data base for a person model with the id specified 43 | """ 44 | return self._manager.objects[person_id] 45 | 46 | def test_field_validators(self): 47 | """ 48 | Test the field_validators class property 49 | 50 | :param mock.MagicMock mck: 51 | """ 52 | class MyManager(InMemoryManager): 53 | get_field_type = mock.MagicMock() 54 | pass 55 | self.assertIsNone(MyManager._field_validators) 56 | self.assertListEqual(MyManager.field_validators, []) 57 | 58 | class MyManager(MyManager): 59 | fields = [1, 2, 3] 60 | 61 | fields = MyManager.fields 62 | 63 | mck_list = MyManager.field_validators 64 | self.assertIsInstance(mck_list, list) 65 | self.assertEqual(MyManager.get_field_type.call_count, len(fields)) 66 | args = list((x[0][0] for x in MyManager.get_field_type.call_args_list)) 67 | self.assertListEqual(args, fields) 68 | 69 | def test_list_fields_property(self): 70 | """ 71 | Tests whether the list_fields property appropriately 72 | gets the list_fields in all circumstances 73 | """ 74 | _fields = [1, 2, 3] 75 | _list_fields = [4, 5] 76 | 77 | class M1(InMemoryManager): 78 | fields = _fields 79 | list_fields = _list_fields 80 | 81 | self.assertListEqual(_list_fields, M1.list_fields) 82 | self.assertListEqual(_fields, M1.fields) 83 | self.assertNotEqual(M1.list_fields, M1.fields) 84 | 85 | class M2(InMemoryManager): 86 | fields = _fields 87 | 88 | self.assertListEqual(_fields, M2.fields) 89 | self.assertListEqual(_fields, M2.list_fields) 90 | self.assertListEqual(M2.fields, M2.list_fields) 91 | 92 | def test_abstact_method_pissing_me_off(self): 93 | class Manager(InMemoryManager): 94 | pass 95 | 96 | self.assertIsInstance(super(Manager, Manager()).get_field_type('blah'), BaseField) 97 | 98 | def test_update_fields(self): 99 | """ 100 | Tests the update_fields class property 101 | """ 102 | 103 | class Manager(InMemoryManager): 104 | fields = ('id', 'another', 'final',) 105 | 106 | self.assertTupleEqual(Manager.fields, Manager.update_fields) 107 | -------------------------------------------------------------------------------- /ripozo_tests/integration/relationships.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest2 7 | 8 | from ripozo.resources.relationships import Relationship, ListRelationship, FilteredRelationship 9 | from ripozo.resources.resource_base import ResourceBase 10 | 11 | 12 | class TestRelationships(unittest2.TestCase): 13 | def test_link_to_list_of_child_resources(self): 14 | """Tests whether there can be a relationship 15 | That simply provides a link to a list of the children of 16 | the resource. In otherwords, they are not embedded in any 17 | way. Not even the child links""" 18 | class Parent(ResourceBase): 19 | pks = 'id', 20 | _relationships = Relationship('children', relation='Child', 21 | property_map=dict(id='parent_id'), 22 | query_args=['parent_id'], no_pks=True, 23 | remove_properties=False), 24 | 25 | class Child(ResourceBase): 26 | resource_name = 'child' 27 | pks = 'id', 28 | 29 | props = dict(id=1, name='something') 30 | parent = Parent(properties=props) 31 | link_to_children = parent.related_resources[0].resource 32 | self.assertEqual(link_to_children.url, '/child?parent_id=1') 33 | self.assertEqual(parent.url, '/parent/1') 34 | 35 | def test_filter_relationship(self): 36 | """Same as `test_link_to_list_of_children` but using the 37 | `FilteredRelationship` class""" 38 | class Parent(ResourceBase): 39 | pks = 'id', 40 | _relationships = FilteredRelationship('children', relation='Child', 41 | property_map=dict(id='parent_id')), 42 | 43 | class Child(ResourceBase): 44 | resource_name = 'child' 45 | pks = 'id', 46 | 47 | props = dict(id=1, name='something') 48 | parent = Parent(properties=props) 49 | link_to_children = parent.related_resources[0].resource 50 | self.assertEqual(link_to_children.url, '/child?parent_id=1') 51 | self.assertEqual(parent.url, '/parent/1') 52 | -------------------------------------------------------------------------------- /ripozo_tests/integration/resources.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.decorators import translate, apimethod 7 | from ripozo.exceptions import ValidationException 8 | from ripozo.resources.fields import BaseField 9 | from ripozo.resources.relationships import ListRelationship, Relationship 10 | from ripozo.resources.request import RequestContainer 11 | from ripozo.resources.resource_base import ResourceBase 12 | 13 | import mock 14 | import unittest2 15 | 16 | 17 | class TestResourceIntegration(unittest2.TestCase): 18 | def test_validate_with_manager_field_validators(self): 19 | fake_manager = mock.MagicMock() 20 | fake_manager.field_validators = [BaseField('first', required=True), BaseField('second', required=False)] 21 | 22 | class TestValidateIntegrations(ResourceBase): 23 | manager = fake_manager 24 | @apimethod(methods=['GET']) 25 | @translate(manager_field_validators=True, validate=True) 26 | def hello(cls, request, *args, **kwargs): 27 | return cls(properties=request.body_args) 28 | 29 | self.help_test_validate_with_manager_field_validators(TestValidateIntegrations) 30 | 31 | def test_validate_with_manager_field_validators_inherited(self): 32 | """ 33 | Same as test_validate_with_manager_field_validators, 34 | except that it checks it on an inherited class. This was 35 | done to ensure no regressions in regards to issue #10 on github. 36 | """ 37 | fake_manager = mock.MagicMock() 38 | fake_manager.field_validators = [BaseField('first', required=True), BaseField('second', required=False)] 39 | 40 | class TestValidateIntegrationsParent(ResourceBase): 41 | manager = fake_manager 42 | @apimethod(methods=['GET']) 43 | @translate(manager_field_validators=True, validate=True) 44 | def hello(cls, request, *args, **kwargs): 45 | return cls(properties=request.body_args) 46 | 47 | self.help_test_validate_with_manager_field_validators(TestValidateIntegrationsParent) 48 | 49 | class TestValidateIntegrationsInherited(TestValidateIntegrationsParent): 50 | resource_name = 'another' 51 | pass 52 | 53 | self.help_test_validate_with_manager_field_validators(TestValidateIntegrationsInherited) 54 | 55 | def test_relationships_resource_instance(self): 56 | """ 57 | Tests whether the relationships are appropriately created. 58 | """ 59 | lr = ListRelationship('resource_list', relation='Resource') 60 | class ResourceList(ResourceBase): 61 | resource_name = 'resource_list' 62 | _relationships = [ 63 | lr, 64 | ] 65 | 66 | class Resource(ResourceBase): 67 | pks = ['id'] 68 | 69 | self.assertEqual(ResourceList._relationships, [lr]) 70 | props = dict(resource_list=[dict(id=1), dict(id=2)]) 71 | res = ResourceList(properties=props) 72 | self.assertEqual(len(res.related_resources), 1) 73 | resources = res.related_resources[0][0] 74 | self.assertIsInstance(resources, list) 75 | self.assertEqual(len(resources), 2) 76 | 77 | def help_test_validate_with_manager_field_validators(self, klass): 78 | """ 79 | Helper for the couple test_validate_with_manager_field_validators. 80 | Just checks that it gets the right responses. Abstracted out poorly 81 | to reduce code duplication and because I hate typing. 82 | 83 | :param type klass: The class that you are checking. 84 | """ 85 | request = RequestContainer(body_args=dict(first=[1], second=[2])) 86 | response = klass.hello(request) 87 | self.assertDictEqual(dict(first=1, second=2), response.properties) 88 | self.assertDictEqual(request.body_args, response.properties) 89 | 90 | # Without list inputs since requests are mutable 91 | response = klass.hello(request) 92 | self.assertDictEqual(dict(first=1, second=2), response.properties) 93 | self.assertDictEqual(request.body_args, response.properties) 94 | 95 | request2 = RequestContainer(body_args=dict(second=[2])) 96 | self.assertRaises(ValidationException, klass.hello, request2) 97 | 98 | request3 = RequestContainer(body_args=dict(first=[1])) 99 | response = klass.hello(request3) 100 | self.assertDictEqual(dict(first=1), response.properties) 101 | 102 | def test_self_referential_templated(self): 103 | """ 104 | Tests a self referential templated link. 105 | """ 106 | class MyResource(ResourceBase): 107 | pks = ('id',) 108 | _links = (Relationship('my_resource', relation='MyResource', 109 | templated=True, embedded=True),) 110 | 111 | res = MyResource() 112 | self.assertEqual(len(res.linked_resources), 1) 113 | self.assertEqual(res.linked_resources[0].resource.url, '/my_resource/') -------------------------------------------------------------------------------- /ripozo_tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | from . import dispatch, managers, resources, decorators, exceptions, tests_utilities, tests -------------------------------------------------------------------------------- /ripozo_tests/unit/dispatch/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo_tests.unit.dispatch import adapters, dispatch_base 7 | -------------------------------------------------------------------------------- /ripozo_tests/unit/dispatch/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo_tests.unit.dispatch.adapters import base, boring_json, hal, siren -------------------------------------------------------------------------------- /ripozo_tests/unit/dispatch/adapters/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest2 7 | import json 8 | 9 | from ripozo import RequestContainer 10 | from ripozo.adapters.base import AdapterBase 11 | from ripozo.resources.relationships import Relationship, ListRelationship 12 | from ripozo.resources.resource_base import ResourceBase 13 | from ripozo.exceptions import RestException 14 | 15 | 16 | class TestAdapter(AdapterBase): 17 | __abstract__ = True 18 | formats = ['blah'] 19 | 20 | @property 21 | def formatted_body(self): 22 | return super(TestAdapter, self).formatted_body 23 | 24 | @property 25 | def extra_headers(self): 26 | return super(TestAdapter, self).extra_headers 27 | 28 | 29 | class TestAdapterBase(unittest2.TestCase): 30 | def get_related_resource_class(self): 31 | class Related(ResourceBase): 32 | _resource_name = 'related' 33 | _pks = ['id'] 34 | return Related 35 | 36 | def get_relationship_resource_class(self): 37 | class RelationshipResource(ResourceBase): 38 | _resource_name = 'relationship_resource' 39 | _relationships = { 40 | 'relationship': Relationship(property_map=dict(id='id'), relation='Related') 41 | } 42 | return RelationshipResource 43 | 44 | def get_list_relationship_resource_class(self): 45 | class RelationshipResource(ResourceBase): 46 | _resource_name = 'relationship_resource' 47 | _relationships = { 48 | 'relationship': ListRelationship('list_relationship', relation='Related') 49 | } 50 | return RelationshipResource 51 | 52 | def get_link_resource_class(self): 53 | class LinkResource(ResourceBase): 54 | _resource_name = 'link_resource' 55 | _links = { 56 | 'link': Relationship('linked', relation='Related') 57 | } 58 | return LinkResource 59 | 60 | def get_link_list_resource_class(self): 61 | class LinkListResource(ResourceBase): 62 | _resource_name = 'link_list_resource' 63 | _links = { 64 | 'link': Relationship('linked_list', relation='Related') 65 | } 66 | return LinkListResource 67 | 68 | def test_coverage_annoyance(self): 69 | """ 70 | Just hitting the NotImplementErrors so I don't get 71 | annoyed anymore. 72 | """ 73 | adapter = TestAdapter(None) 74 | try: 75 | x = adapter.formatted_body 76 | assert False 77 | except NotImplementedError: 78 | assert True 79 | 80 | try: 81 | x = adapter.extra_headers 82 | assert False 83 | except NotImplementedError: 84 | assert True 85 | 86 | def test_format_exception(self): 87 | """ 88 | Tests the format_exception class method. 89 | """ 90 | exc = RestException('blah blah', status_code=458) 91 | json_dump, content_type, status_code = TestAdapter.format_exception(exc) 92 | data = json.loads(json_dump) 93 | self.assertEqual(TestAdapter.formats[0], content_type) 94 | self.assertEqual(status_code, 458) 95 | self.assertEqual(data['message'], 'blah blah') 96 | 97 | def test_format_request(self): 98 | """Dumb test for format_request""" 99 | request = RequestContainer() 100 | response = TestAdapter.format_request(request) 101 | self.assertIs(response, request) -------------------------------------------------------------------------------- /ripozo_tests/unit/dispatch/adapters/boring_json.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import json 7 | 8 | import six 9 | import unittest2 10 | 11 | from ripozo import ResourceBase 12 | from ripozo.adapters import BasicJSONAdapter 13 | from ripozo.exceptions import RestException 14 | from ripozo.resources.request import RequestContainer 15 | from ripozo_tests.helpers.hello_world_viewset import get_refreshed_helloworld_viewset 16 | 17 | 18 | class TestBoringJSONAdapter(unittest2.TestCase): 19 | """ 20 | Tests whether the BasicJSONAdapter appropriately creates 21 | a response for a resource 22 | """ 23 | # TODO this definitely needs to be fleshed out. All of it. 24 | 25 | def setUp(self): 26 | HelloWorldViewset = get_refreshed_helloworld_viewset() 27 | 28 | self.properties = {'content': 'hello'} 29 | self.resource = HelloWorldViewset.hello(RequestContainer(query_args=dict(content='hello', 30 | related='world'))) 31 | self.adapter = BasicJSONAdapter(self.resource) 32 | self.data = json.loads(self.adapter.formatted_body) 33 | 34 | def test_properties_available(self): 35 | data = self.data[self.resource.resource_name] 36 | for key, value in six.iteritems(self.properties): 37 | self.assertIn(key, data) 38 | self.assertEqual(value, data[key]) 39 | 40 | for relationship, field_name, embedded in self.resource.related_resources: 41 | self.assertIn(field_name, data) 42 | 43 | def test_content_header(self): 44 | adapter = BasicJSONAdapter(None) 45 | self.assertEqual(adapter.extra_headers, {'Content-Type': 'application/json'}) 46 | 47 | def test_format_exception(self): 48 | """ 49 | Tests the format_exception class method. 50 | """ 51 | exc = RestException('blah blah', status_code=458) 52 | json_dump, content_type, status_code = BasicJSONAdapter.format_exception(exc) 53 | data = json.loads(json_dump) 54 | self.assertEqual(BasicJSONAdapter.formats[0], content_type) 55 | self.assertEqual(status_code, 458) 56 | self.assertEqual(data['message'], 'blah blah') 57 | 58 | def test_format_request(self): 59 | """Dumb test for format_request""" 60 | request = RequestContainer() 61 | response = BasicJSONAdapter.format_request(request) 62 | self.assertIs(response, request) 63 | 64 | def test_append_relationships_to_list_list_relationship(self): 65 | """ 66 | Tests whether the relationships are appropriately 67 | added to the response 68 | """ 69 | class MyResource(ResourceBase): 70 | pass 71 | 72 | relationship_list = [MyResource(properties=dict(id=1)), MyResource(properties=dict(id=2))] 73 | relationships = [(relationship_list, 'name', True)] 74 | rel_dict = {} 75 | BasicJSONAdapter._append_relationships_to_list(rel_dict, relationships) 76 | self.assertDictEqual(dict(name=[dict(id=1), dict(id=2)]), rel_dict) 77 | 78 | def test_append_relationships_to_list_single_relationship(self): 79 | """ 80 | Ensures that a Relationship (not ListRelationship) is properly added 81 | """ 82 | class MyResource(ResourceBase): 83 | pass 84 | 85 | relationships = [(MyResource(properties=dict(id=1)), 'name', True)] 86 | rel_dict = {} 87 | BasicJSONAdapter._append_relationships_to_list(rel_dict, relationships) 88 | self.assertDictEqual(dict(name=[dict(id=1)]), rel_dict) 89 | -------------------------------------------------------------------------------- /ripozo_tests/unit/dispatch/adapters/hal.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import json 7 | import six 8 | import unittest2 9 | 10 | from ripozo import ResourceBase 11 | from ripozo.adapters import HalAdapter 12 | from ripozo.resources.constructor import ResourceMetaClass 13 | from ripozo.resources.request import RequestContainer 14 | from ripozo_tests.helpers.hello_world_viewset import get_refreshed_helloworld_viewset 15 | from ripozo.exceptions import RestException 16 | 17 | 18 | class TestHalAdapter(unittest2.TestCase): 19 | """ 20 | Tests whether the HalAdapter appropriately creates 21 | a response for a resource 22 | """ 23 | 24 | def setUp(self): 25 | ResourceMetaClass.registered_names_map.clear() 26 | ResourceMetaClass.registered_resource_classes.clear() 27 | HelloWorldViewset = get_refreshed_helloworld_viewset() 28 | 29 | self.properties = {'content': 'hello'} 30 | self.resource = HelloWorldViewset.hello(RequestContainer(query_args=dict(content='hello', 31 | related='world'))) 32 | self.adapter = HalAdapter(self.resource) 33 | self.data = json.loads(self.adapter.formatted_body) 34 | 35 | def test_links(self, data=None): 36 | """ 37 | Tests that the _links property is available and properly formatted 38 | """ 39 | if data is None: 40 | data = self.data 41 | self.assertIn('_links', data) 42 | self.assertIn('self', data['_links']) 43 | for key, value in six.iteritems(data['_links']): 44 | self.assertIn('href', data['_links'][key]) 45 | 46 | def test_properties_available(self, data=None): 47 | """ 48 | Tests whether all properties are available 49 | according to the HAL format 50 | """ 51 | if data is None: 52 | data = self.data 53 | for key, value in six.iteritems(self.properties): 54 | self.assertIn(key, data) 55 | self.assertEqual(data[key], value) 56 | 57 | def test_embedded(self): 58 | """ 59 | Tests whether the _embedded property is appropriately set. 60 | """ 61 | self.assertIn('_embedded', self.data) 62 | embedded = self.data['_embedded'] 63 | self.assertIsInstance(embedded, dict) 64 | for key, value in six.iteritems(embedded): 65 | # TODO need to make sure all relationships are available 66 | if isinstance(value, dict): 67 | value = [value] # To simplify testing 68 | for v in value: 69 | self.test_links(data=v) 70 | # TODO need to check if properties available 71 | 72 | def test_embedded_relationships(self): 73 | class Fake(ResourceBase): 74 | pass 75 | props = dict( 76 | _embedded={}, 77 | _links=dict(self=dict(href='/fake')), 78 | val=1, val2=2 79 | ) 80 | rel = Fake(properties=props) 81 | adapter = HalAdapter(None) 82 | resp = adapter._generate_relationship(rel, True) 83 | self.assertEqual(resp, props) 84 | 85 | def test_list_relationships(self): 86 | class Fake(ResourceBase): 87 | pass 88 | props = dict( 89 | _embedded={}, 90 | _links=dict(self=dict(href='/fake')), 91 | val=1, val2=2 92 | ) 93 | props2 = dict( 94 | _embedded={}, 95 | _links=dict(self=dict(href='/fake')), 96 | val=3, val4=4 97 | ) 98 | adapter = HalAdapter(None) 99 | rel1 = Fake(properties=props) 100 | rel2 = Fake(properties=props2) 101 | resp = adapter._generate_relationship([rel1, rel2], True) 102 | self.assertListEqual([props, props2], resp) 103 | 104 | def test_not_all_pks(self): 105 | class Fake(ResourceBase): 106 | pks = ['id'] 107 | 108 | props = dict(val=1) 109 | adapter = HalAdapter(None) 110 | rel = Fake(properties=props) 111 | resp = adapter._generate_relationship(rel, False) 112 | self.assertIsNone(resp) 113 | 114 | def test_content_type(self): 115 | adapter = HalAdapter(None) 116 | self.assertEqual(adapter.extra_headers, {'Content-Type': 'application/hal+json'}) 117 | 118 | def test_list_relationship_not_all_pks(self): 119 | class Fake(ResourceBase): 120 | pks = ['id'] 121 | 122 | adapter = HalAdapter(None) 123 | props1 = dict( 124 | _embedded={}, 125 | _links=dict(self=dict(href='/fake/1')), 126 | id=1, val=2 127 | ) 128 | props2 = dict( 129 | _embedded={}, 130 | _links=dict(self=dict(href='/fake')), 131 | val=1 132 | ) 133 | rel1 = Fake(properties=props1) 134 | rel2 = Fake(properties=props2) 135 | resp = adapter._generate_relationship([rel1, rel2], True) 136 | self.assertEqual(len(resp), 1) 137 | self.assertEqual(resp[0], props1) 138 | 139 | def test_generate_relationship_embedded(self): 140 | """ 141 | Tests that embedded relationships are 142 | appropriately constructed. 143 | """ 144 | class Fake(ResourceBase): 145 | pass 146 | 147 | res = Fake(properties=dict(x=1, y=2)) 148 | 149 | expected_res = res.properties.copy() 150 | expected_res.update(dict(_links=dict(self=dict(href=res.url)), _embedded={})) 151 | relation_list = [(res, 'res', True,)] 152 | adapter = HalAdapter(Fake()) 153 | embedded, links = adapter.generate_relationship(relation_list) 154 | self.assertDictEqual(embedded['res'], expected_res) 155 | 156 | def test_missing_generate_relationship(self): 157 | """ 158 | Tests attempting to generate a relationship 159 | when not all of the pks are available. 160 | """ 161 | class Fake(ResourceBase): 162 | pks = ('id',) 163 | 164 | res = Fake(properties=dict(x=1, y=2)) 165 | relation_list = [(res, 'res', True,)] 166 | adapter = HalAdapter(Fake()) 167 | embedded, links = adapter.generate_relationship(relation_list) 168 | self.assertDictEqual(embedded, {}) 169 | self.assertDictEqual(links, {}) 170 | 171 | def test_format_exception(self): 172 | exc = RestException('blah blah', status_code=458) 173 | json_dump, content_type, status_code = HalAdapter.format_exception(exc) 174 | data = json.loads(json_dump) 175 | self.assertEqual(HalAdapter.formats[0], content_type) 176 | self.assertEqual(status_code, 458) 177 | self.assertIn('_embedded', data) 178 | self.assertIn('_links', data) 179 | self.assertEqual(data['message'], 'blah blah') 180 | 181 | def test_format_request(self): 182 | """Dumb test for format_request""" 183 | request = RequestContainer() 184 | response = HalAdapter.format_request(request) 185 | self.assertIs(response, request) 186 | -------------------------------------------------------------------------------- /ripozo_tests/unit/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.exceptions import RestException, DispatchException, NotFoundException, \ 7 | TranslationException, ValidationException, AdapterFormatAlreadyRegisteredException, FieldException, \ 8 | ManagerException 9 | 10 | import unittest2 11 | 12 | 13 | class TestExceptions(unittest2.TestCase): 14 | """ 15 | Seems stupid, but I've screwed it up multiple times. 16 | Long story short, test coverage, no matter how trivial, 17 | is valuable in case you did something really stupid. 18 | (Like initializing exceptions wrong. 19 | """ 20 | exceptions = [RestException, DispatchException, 21 | NotFoundException, TranslationException, ValidationException, 22 | AdapterFormatAlreadyRegisteredException, FieldException, 23 | ManagerException] 24 | 25 | def test_exceptions(self): 26 | """ 27 | Just run through them, initialize them and check if 28 | they are an instance of RestException 29 | """ 30 | for e in self.exceptions: 31 | exc = e(message='some message', status_code=102) 32 | self.assertEqual(str(exc), 'some message') 33 | self.assertEqual(exc.status_code, 102) 34 | exc = e('message') 35 | self.assertEqual(str(exc), 'message') 36 | -------------------------------------------------------------------------------- /ripozo_tests/unit/managers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import base 2 | -------------------------------------------------------------------------------- /ripozo_tests/unit/managers/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.manager_base import BaseManager 7 | 8 | import six 9 | import unittest2 10 | 11 | 12 | class FakeManager(BaseManager): 13 | def create(self, values, *args, **kwargs): 14 | pass 15 | 16 | def retrieve(self, lookup_keys, *args, **kwargs): 17 | pass 18 | 19 | def retrieve_list(self, filters, *args, **kwargs): 20 | pass 21 | 22 | def update(self, lookup_keys, updates, *args, **kwargs): 23 | pass 24 | 25 | def delete(self, lookup_keys, *args, **kwargs): 26 | pass 27 | 28 | 29 | class TestBaseManager(unittest2.TestCase): 30 | def test_fields(self): 31 | m = FakeManager() 32 | self.assertListEqual(m.fields, []) 33 | 34 | def test_create_fields(self): 35 | m = FakeManager() 36 | self.assertListEqual(m.create_fields, []) 37 | 38 | def test_list_fields(self): 39 | m = FakeManager() 40 | self.assertListEqual(m.list_fields, []) 41 | 42 | def test_update_fields(self): 43 | m = FakeManager() 44 | self.assertListEqual(m.update_fields, []) 45 | 46 | def test_field_validators(self): 47 | class M(FakeManager): 48 | fields = ('x', 'y', 'z',) 49 | _field_validators = dict(x=1, z=2) 50 | 51 | resp = M.field_validators 52 | for x in [None, 1, 2]: 53 | self.assertIn(x, resp) 54 | 55 | def test_valid_fields(self): 56 | """ 57 | Tests the valid fields method. 58 | """ 59 | original_values = dict(x=1, y=2, z=3) 60 | valid_fields = ['x', 'z', 'q'] 61 | resp = BaseManager.valid_fields(original_values, valid_fields) 62 | self.assertDictEqual(resp, dict(x=1, z=3)) 63 | 64 | def test_dot_field_list_to_dict(self): 65 | """ 66 | Tests the appropriate dictionary return 67 | """ 68 | class Manager(FakeManager): 69 | pass 70 | 71 | input_outputs = [ 72 | ([], {}), 73 | (['blah'], {'blah': None}), 74 | (['blah', 'a.b', 'a.c'], {'blah': None, 'a': {'b': None, 'c': None}}) 75 | ] 76 | for method_input, output in input_outputs: 77 | generated = Manager().dot_field_list_to_dict(method_input) 78 | self.assertDictEqual(generated, output) 79 | 80 | def test_get_pagination_pks(self): 81 | m = FakeManager() 82 | self.assertEqual(None, m.get_pagination_pks(dict())[0]) 83 | self.assertEqual(1, m.get_pagination_pks(dict(pagination_pk=1))[0]) 84 | 85 | def test_get_pagination_count(self): 86 | m = FakeManager() 87 | self.assertEqual(m.paginate_by, m.get_pagination_count(dict())[0]) 88 | self.assertEqual(1, m.get_pagination_count(dict(count=1))[0]) 89 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Tim Martin' 2 | 3 | from ripozo_tests.unit.resources import fields, relationships, base, request, restmixins -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/constructor.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.constructor import ResourceMetaClass 7 | 8 | import mock 9 | import six 10 | import unittest2 11 | 12 | 13 | class TestResourceMetaClass(unittest2.TestCase): 14 | def setUp(self): 15 | ResourceMetaClass.registered_names_map.clear() 16 | ResourceMetaClass.registered_resource_classes.clear() 17 | 18 | def test_new_resource_class(self): 19 | """ 20 | Tests that a resource class is instantiated correctly. 21 | """ 22 | class_name = b'MyClass' if six.PY2 else 'MyClass' 23 | klass = ResourceMetaClass(class_name, (object,), dict(base_url='blah')) 24 | self.assertIn(class_name, ResourceMetaClass.registered_names_map) 25 | self.assertIn(klass, ResourceMetaClass.registered_resource_classes) 26 | 27 | def test_new_resource_class_abstract(self): 28 | """ 29 | Tests that a resource class is not instantiated 30 | if it has a new attribute __abstract__ = True 31 | """ 32 | class_name = b'MyClass' if six.PY2 else 'MyClass' 33 | klass = ResourceMetaClass(class_name, (object,), dict(__abstract__=True)) 34 | self.assertNotIn(class_name, ResourceMetaClass.registered_names_map) 35 | self.assertNotIn(klass, ResourceMetaClass.registered_resource_classes) 36 | 37 | def test_register_class_registration_dicts(self): 38 | """ 39 | Tests that the side effects of registering 40 | a class works appropriately. 41 | """ 42 | name = b'name' if six.PY2 else 'name' 43 | mck = mock.Mock(base_url='blah', __name__=name) 44 | ResourceMetaClass.register_class(mck) 45 | self.assertEqual(id(mck), id(ResourceMetaClass.registered_names_map[name])) 46 | self.assertEqual(mck.base_url, ResourceMetaClass.registered_resource_classes[mck]) 47 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from . import base, common 2 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/fields/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest2 7 | 8 | from ripozo.exceptions import ValidationException, TranslationException, RestException 9 | from ripozo.resources.constants import input_categories 10 | from ripozo.resources.fields.base import BaseField, translate_fields 11 | from ripozo import RequestContainer 12 | from ripozo_tests.bases.field import FieldTestBase 13 | 14 | 15 | class FieldTestBase2(FieldTestBase): 16 | validation_exception = ValidationException 17 | translation_exception = TranslationException 18 | 19 | 20 | class TestBaseField(FieldTestBase2, unittest2.TestCase): 21 | field_type = BaseField 22 | instance_type = object 23 | 24 | def test_validate_type(self): 25 | f = BaseField('field', required=False) 26 | original = object() 27 | 28 | # test doesn't raise when valid 29 | new = f._validate_type(None) 30 | self.assertIsNone(new) 31 | new = f._validate_type(original) 32 | self.assertEqual(new, original) 33 | 34 | f.field_type = int 35 | self.assertRaises(ValidationException, f._validate_type, 'something') 36 | 37 | def test_translate_none_like(self): 38 | f = BaseField('field') 39 | output = f._translate(False) 40 | self.assertIsNotNone(output) 41 | self.assertFalse(output) 42 | 43 | def test_empty_list(self): 44 | f = BaseField('field') 45 | output = f._translate([]) 46 | self.assertIsNone(output) 47 | 48 | def test_translate_list(self): 49 | f = BaseField('field') 50 | output = f._translate([1, 2]) 51 | self.assertEqual(output, 1) 52 | 53 | 54 | class TestTranslateFields(unittest2.TestCase): 55 | """ 56 | For testing the translate_fields method. 57 | """ 58 | 59 | def test_required(self): 60 | field = BaseField('field', required=True) 61 | req = RequestContainer() 62 | self.assertRaises(ValidationException, translate_fields, req, 63 | fields=[field], validate=True) 64 | test_body = dict(nothere='this') 65 | req = RequestContainer(body_args=test_body) 66 | url, query, body = translate_fields(req, fields=[field], validate=True, skip_required=True) 67 | self.assertEqual(body, test_body) 68 | 69 | def test_input_category_types(self): 70 | cat = input_categories.URL_PARAMS 71 | test_input = dict(field='something') 72 | 73 | # URL_PARAMS 74 | field = BaseField('field', required=True, arg_type=input_categories.URL_PARAMS) 75 | req = RequestContainer(url_params=test_input) 76 | url, query, body = translate_fields(req, fields=[field], validate=True) 77 | self.assertEqual(url, test_input) 78 | req = RequestContainer(query_args=test_input, body_args=test_input) 79 | self.assertRaises(ValidationException, translate_fields, req, 80 | fields=[field], validate=True) 81 | 82 | # QUERY_ARGS 83 | field = BaseField('field', required=True, arg_type=input_categories.QUERY_ARGS) 84 | req = RequestContainer(query_args=test_input) 85 | url, query, body = translate_fields(req, fields=[field], validate=True) 86 | self.assertEqual(query, test_input) 87 | req = RequestContainer(url_params=test_input, body_args=test_input) 88 | self.assertRaises(ValidationException, translate_fields, req, 89 | fields=[field], validate=True) 90 | 91 | # BODY_ARGS 92 | field = BaseField('field', required=True, arg_type=input_categories.BODY_ARGS) 93 | req = RequestContainer(body_args=test_input) 94 | url, query, body = translate_fields(req, fields=[field], validate=True) 95 | self.assertEqual(body, test_input) 96 | req = RequestContainer(query_args=test_input, url_params=test_input) 97 | self.assertRaises(ValidationException, translate_fields, req, 98 | fields=[field], validate=True) 99 | 100 | # Non-existent input type 101 | field = BaseField('field', required=True, arg_type='fake') 102 | req = RequestContainer(query_args=test_input, url_params=test_input, body_args=test_input) 103 | self.assertRaises(RestException, translate_fields, req, fields=[field], validate=True) 104 | 105 | def test_validate_required(self): 106 | f = BaseField('f', required=True) 107 | self.assertRaises(ValidationException, f._validate_required, None) 108 | resp = f._validate_required(None, skip_required=True) 109 | self.assertIsNone(resp) 110 | 111 | def test_validate_size(self): 112 | f = BaseField('f', minimum=1, maximum=1) 113 | self.assertRaises(ValidationException, f._validate_size, 0, 0) 114 | self.assertRaises(ValidationException, f._validate_size, 2, 2) 115 | resp = f._validate_size(1, 1) 116 | self.assertEqual(resp, 1) 117 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/fields/field.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import mock 7 | import unittest2 8 | 9 | from ripozo.exceptions import ValidationException 10 | from ripozo.resources.fields.field import IField, Field 11 | 12 | 13 | class TestField(unittest2.TestCase): 14 | def test_instantiate_ifield_error(self): 15 | """Ensure that the abstract base class raises a TypeError 16 | if you try to instantiate it directly""" 17 | self.assertRaises(TypeError, IField, 'f') 18 | 19 | def test_field_translate(self): 20 | f = Field('f') 21 | obj = object() 22 | resp = f.translate(obj) 23 | self.assertIs(obj, resp) 24 | 25 | def test_field_translate_none(self): 26 | f = Field('f', required=True) 27 | self.assertRaises(ValidationException, f.translate, None, validate=True) 28 | resp = f.translate(None, validate=False) 29 | self.assertIsNone(resp) 30 | 31 | def test_translate_not_implemented_error(self): 32 | class Temp(IField): 33 | def _translate(self, obj, skip_required=False): 34 | super(Temp, self)._translate(obj, skip_required=skip_required) 35 | 36 | def _validate(self, obj, skip_required=False): 37 | pass 38 | 39 | f = Temp('f') 40 | self.assertRaises(NotImplementedError, f._translate, 'something') 41 | 42 | def test_validate_not_implemented_error(self): 43 | class Temp(IField): 44 | def _translate(self, obj, skip_required=False): 45 | pass 46 | 47 | def _validate(self, obj, skip_required=False): 48 | super(Temp, self)._validate(obj, skip_required=skip_required) 49 | 50 | f = Temp('f') 51 | self.assertRaises(NotImplementedError, f._validate, 'something') 52 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/fields/validations.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import re 7 | import unittest2 8 | 9 | import mock 10 | 11 | from ripozo.exceptions import ValidationException 12 | from ripozo.resources.fields.field import Field 13 | from ripozo.resources.fields.validations import translate_iterable_to_single, \ 14 | validate_required, validate_type, validate_size, validate_regex, \ 15 | basic_validation 16 | 17 | 18 | class TestValidation(unittest2.TestCase): 19 | def test_translate_iterable_to_single(self): 20 | """ 21 | Tests the case where the item is an iterable 22 | """ 23 | resp = translate_iterable_to_single([1, 2]) 24 | self.assertEqual(resp, 1) 25 | 26 | def test_translate_iterable_to_single_not_list(self): 27 | """ 28 | Tests the case where the item is not an iterable 29 | """ 30 | resp = translate_iterable_to_single(1) 31 | self.assertEqual(resp, 1) 32 | 33 | def test_validate_required_success(self): 34 | """ 35 | Tests when the object is required and it is there 36 | """ 37 | f = Field('f', required=True) 38 | resp = validate_required(f, 1) 39 | self.assertEqual(resp, 1) 40 | 41 | def test_validate_required_skipped(self): 42 | """ 43 | Tests when the item is None and the validation is skipped 44 | because skip_required is True 45 | """ 46 | f = Field('f', required=True) 47 | resp = validate_required(f, None, skip_required=True) 48 | self.assertEqual(resp, None) 49 | 50 | def test_validate_required_non_required_field(self): 51 | """ 52 | Tests the the field is validated if the field 53 | is not required and the object is None 54 | """ 55 | f = Field('f', required=False) 56 | resp = validate_required(f, None, skip_required=False) 57 | self.assertEqual(resp, None) 58 | 59 | def test_validate_required_failure(self): 60 | """Tests that it fails when the object is None""" 61 | f = Field('f', required=True) 62 | self.assertRaises(ValidationException, validate_required, f, None) 63 | 64 | def test_validate_type(self): 65 | """Success case for validate_type""" 66 | f = Field('f') 67 | obj = object() 68 | resp = validate_type(f, object, obj) 69 | self.assertIs(resp, obj) 70 | 71 | def test_validate_type_failure(self): 72 | """Failure case for validate_type""" 73 | f = Field('f') 74 | obj = 'blah' 75 | self.assertRaises(ValidationException, validate_type, f, int, obj) 76 | 77 | def test_validate_size(self): 78 | """Success case for validate size""" 79 | f = Field('blah') 80 | obj = 10 81 | resp = validate_size(f, obj, obj, minimum=10, maximum=10) 82 | self.assertEqual(resp, obj) 83 | 84 | def test_validate_size_too_small(self): 85 | f = Field('blah') 86 | obj = 10 87 | self.assertRaises(ValidationException, validate_size, f, obj, obj, minimum=11) 88 | 89 | def test_validate_size_too_large(self): 90 | f = Field('blah') 91 | obj = 10 92 | self.assertRaises(ValidationException, validate_size, f, obj, obj, maximum=9) 93 | 94 | def test_validate_regex(self): 95 | """Success case for validate_regex""" 96 | regex = re.compile(r'[a-z]*something[a-z]*') 97 | f = Field('f') 98 | obj = 'dfgfgfsomethingdfgfgfs' 99 | resp = validate_regex(f, obj, regex) 100 | self.assertEqual(resp, obj) 101 | 102 | def test_validate_regex_failure(self): 103 | """Failure case for validate_regex""" 104 | regex = re.compile(r'something') 105 | f = Field('f') 106 | obj = 'notthatsome' 107 | self.assertRaises(ValidationException, validate_regex, f, obj, regex) 108 | 109 | def test_validate_regex_multiple_matches(self): 110 | """Tests that the validation succeeds if there is more than one match""" 111 | regex = re.compile(r'something') 112 | f = Field('f') 113 | obj = 'somethingsomethingsomething' 114 | resp = validate_regex(f, obj, regex) 115 | self.assertEqual(resp, obj) 116 | 117 | def test_basic_validation(self): 118 | """Success case for basic_validation""" 119 | f = Field('f', required=True) 120 | obj = object() 121 | resp = basic_validation(f, obj, skip_required=False) 122 | self.assertIs(resp, obj) 123 | 124 | def test_basic_validation_skip_required(self): 125 | """Tests when skip_required is True and the object is None""" 126 | f = Field('f', required=True) 127 | resp = basic_validation(f, None, skip_required=True) 128 | self.assertIsNone(resp) 129 | 130 | @mock.patch('ripozo.resources.fields.validations.validate_type') 131 | def test_basic_validation_none_object(self, validate_type): 132 | """Check that validate_type is not called when object is None""" 133 | f = Field('f') 134 | basic_validation(f, None) 135 | self.assertFalse(validate_type.called) 136 | 137 | # Also ensure that it is called 138 | basic_validation(f, object()) 139 | self.assertTrue(validate_type.called) 140 | 141 | def test_basic_validation_required_failure(self): 142 | """Ensure failure when object is None and required""" 143 | f = Field('f', required=True) 144 | self.assertRaises(ValidationException, basic_validation, f, None) 145 | 146 | def test_basic_validation_type_mismatch(self): 147 | """Ensure failure when object is the wrong type""" 148 | class Temp(Field): 149 | field_type = dict 150 | 151 | f = Temp('f') 152 | self.assertRaises(ValidationException, basic_validation, f, object()) 153 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/relationships/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | 8 | from . import list_relationship, relationship 9 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/relationships/list_relationship.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.relationships.list_relationship import ListRelationship 7 | from ripozo.resources.resource_base import ResourceBase 8 | 9 | import unittest2 10 | 11 | 12 | class TestListRelationship(unittest2.TestCase): 13 | """ 14 | Tests the ListRelationship class 15 | """ 16 | 17 | def test_init(self): 18 | """ 19 | Tests the initialization of the ListRelationship 20 | """ 21 | list_name = 'mylist' 22 | lr = ListRelationship(list_name, relation='MyResource', embedded=True) 23 | self.assertEqual(list_name, lr.name) 24 | self.assertTrue(lr.embedded) 25 | self.assertEqual(lr._relation, 'MyResource') 26 | 27 | def test_construct_resource(self): 28 | class RelatedResource(ResourceBase): 29 | _pks = ['pk'] 30 | 31 | list_name = 'mylist' 32 | lr = ListRelationship(list_name, relation='RelatedResource') 33 | props = {list_name: []} 34 | for i in range(10): 35 | props[list_name].append(dict(pk=i)) 36 | res_list = lr.construct_resource(props) 37 | self.assertIsInstance(res_list, list) 38 | for x in res_list: 39 | # TODO actually check this 40 | pass 41 | self.assertEqual(len(res_list), 10) 42 | 43 | def test_empty_list(self,): 44 | class RelatedResource2(ResourceBase): 45 | pass 46 | list_name = 'mylist' 47 | lr = ListRelationship(list_name, relation='RelatedResource') 48 | res_list = lr.construct_resource(dict(some='thing', another='thing')) 49 | self.assertIsInstance(res_list, list) 50 | self.assertEqual(len(res_list), 0) 51 | 52 | def test_remove_child_properties(self): 53 | """ 54 | Test remove child properties 55 | """ 56 | list_name = 'listname' 57 | properties = {list_name: 'Something', 'another': 'thing'} 58 | lr = ListRelationship(list_name) 59 | post_props = lr.remove_child_resource_properties(properties) 60 | self.assertNotEqual(properties, post_props) 61 | self.assertEqual({'another': 'thing'}, post_props) 62 | 63 | # Just make sure that it still works when the properties aren't there 64 | part2 = lr.remove_child_resource_properties(post_props) 65 | self.assertEqual(part2, post_props) 66 | self.assertNotEqual(id(part2), id(post_props)) 67 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/relationships/relationship.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.exceptions import RestException 7 | from ripozo.resources.relationships.relationship import Relationship 8 | from ripozo.resources.resource_base import ResourceBase 9 | 10 | import mock 11 | import unittest2 12 | 13 | 14 | class TestRelationship(unittest2.TestCase): 15 | """ 16 | A TestCase for testing relationships and 17 | their various properties. 18 | """ 19 | 20 | def test_init(self): 21 | """ 22 | Tests the initialization of a Relationship instance 23 | In particular checking that a property map is 24 | always available. 25 | """ 26 | r = Relationship('related') 27 | self.assertEqual(r.property_map, {}) 28 | x = dict(some='thing') 29 | r = Relationship('related', property_map=x) 30 | self.assertEqual(r.property_map, x) 31 | 32 | def test_relation_property(self): 33 | """ 34 | Tests whether the relation property is appropriately 35 | retrieved from ResourceMetaClass 36 | """ 37 | r = Relationship('related') 38 | try: 39 | x = r.relation 40 | assert False 41 | except KeyError: 42 | assert True 43 | mck = mock.MagicMock(registered_names_map={'SomeClass': True}) 44 | r = Relationship('related', relation='SomeClass') 45 | r._resource_meta_class = mck 46 | assert r.relation is True 47 | 48 | def test_construct_resource(self): 49 | """ 50 | Tests the construction of a related resource 51 | """ 52 | class RelatedResource(ResourceBase): 53 | pass 54 | 55 | property_map = dict(parent='child') 56 | r = Relationship('related', property_map=property_map, relation='RelatedResource') 57 | prop_input = dict(parent='value') 58 | resource = r.construct_resource(prop_input) 59 | 60 | self.assertIsNotNone(resource) 61 | 62 | r.required = True 63 | # This should raise a key error since the field is required 64 | self.assertRaises(RestException, r.construct_resource, {}) 65 | 66 | def test_relationship_generation(self): 67 | """Tests the generation of relationships in the ResourceBase""" 68 | class MyResource3(ResourceBase): 69 | _relationships = [Relationship('related', relation='RelatedResource3')] 70 | 71 | class RelatedResource3(ResourceBase): 72 | _pks = ['pk'] 73 | 74 | resource = MyResource3(properties=dict(id=1, related=dict(pk=2))) 75 | self.assertDictEqual(resource.properties, dict(id=1)) 76 | self.assertEqual(len(resource.related_resources), 1) 77 | relation = resource.related_resources[0] 78 | related_res = relation[0] 79 | self.assertIsInstance(related_res, RelatedResource3) 80 | self.assertDictEqual(related_res.properties, dict(pk=2)) 81 | 82 | def test_relationship_generation_with_dict_literal(self): 83 | """ 84 | For some reason it breaks when you use brackets 85 | to define the dictionary 86 | """ 87 | class MyResource2(ResourceBase): 88 | _relationships = [Relationship('related', relation='RelatedResource2')] 89 | 90 | class RelatedResource2(ResourceBase): 91 | _pks = ['pk'] 92 | 93 | resource = MyResource2(properties={'id': 1, 'related': {'pk': 2}}) 94 | self.assertEqual(resource.properties, dict(id=1)) 95 | self.assertEqual(len(resource.related_resources), 1) 96 | relation = resource.related_resources[0] 97 | related_res = relation[0] 98 | self.assertIsInstance(related_res, RelatedResource2) 99 | self.assertEqual(related_res.properties, dict(pk=2)) 100 | 101 | def test_remove_child_resource_properties(self): 102 | property_map = dict(parent='child', parent2='child2') 103 | original_properties = dict(parent='value', parent2='value2', 104 | parent3='value3', parent4='value4') 105 | r = Relationship('related', property_map=property_map) 106 | updated_properties = r.remove_child_resource_properties(original_properties) 107 | self.assertNotEqual(id(updated_properties), id(original_properties)) 108 | expected = dict(parent3='value3', parent4='value4') 109 | self.assertDictEqual(updated_properties, expected) 110 | 111 | original_properties.pop('parent2') 112 | updated_properties = r.remove_child_resource_properties(original_properties) 113 | self.assertDictEqual(updated_properties, expected) 114 | 115 | def test_return_none(self): 116 | """Tests that the private _should_return_none appropriately 117 | returns according to the expected behavior""" 118 | class MyResource(ResourceBase): 119 | pks = 'id', 120 | 121 | res = MyResource(no_pks=True) 122 | rel = Relationship('related') 123 | self.assertFalse(rel._should_return_none(res)) 124 | res = MyResource(properties=dict(id=1)) 125 | self.assertFalse(rel._should_return_none(res)) 126 | res = MyResource(properties=dict()) 127 | self.assertTrue(rel._should_return_none(res)) 128 | rel.templated = True 129 | self.assertFalse(rel._should_return_none(res)) 130 | 131 | def test_remove_properties(self): 132 | """Tests whether properties are appropriately 133 | kept or removed according to the remove_properties 134 | attribute""" 135 | rel = Relationship('related') 136 | x = dict(related=dict(name='name')) 137 | ret = rel._map_pks(x) 138 | self.assertDictEqual(ret, dict(name='name')) 139 | self.assertDictEqual(x, dict()) 140 | rel.remove_properties = False 141 | x = dict(related=dict(name='name')) 142 | ret = rel._map_pks(x) 143 | self.assertDictEqual(ret, dict(name='name')) 144 | self.assertDictEqual(x, dict(related=dict(name='name'))) 145 | 146 | def test_remove_properties_property_map(self): 147 | """Tests whether removing properties according 148 | to the property_map adheres to the remove_properties 149 | attribute""" 150 | 151 | rel = Relationship('related', property_map=dict(name='name')) 152 | x = dict(name='name') 153 | ret = rel._map_pks(x) 154 | self.assertDictEqual(ret, dict(name='name')) 155 | self.assertDictEqual(x, dict()) 156 | rel.remove_properties = False 157 | x = dict(name='name') 158 | ret = rel._map_pks(x) 159 | self.assertDictEqual(ret, dict(name='name')) 160 | self.assertDictEqual(x, dict(name='name')) 161 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/request.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.constants.input_categories import QUERY_ARGS, BODY_ARGS, URL_PARAMS 7 | from ripozo.resources.request import RequestContainer 8 | 9 | import unittest2 10 | 11 | 12 | class TestRequestContainer(unittest2.TestCase): 13 | """ 14 | Tests for the RequestContainer 15 | """ 16 | 17 | def dict_helper(self, name): 18 | d = dict(some='object') 19 | r = RequestContainer(**{name: d}) 20 | self.assertDictEqual(d, getattr(r, name)) 21 | self.assertNotEqual(id(d), id(getattr(r, name))) 22 | 23 | # Test setting the dict 24 | d2 = dict(another='object') 25 | setattr(r, name, d2) 26 | self.assertNotEqual(d, getattr(r, name)) 27 | self.assertDictEqual(d2, getattr(r, name)) 28 | self.assertNotEqual(id(d2), id(getattr(r, name))) 29 | 30 | # Test empty dict 31 | r = RequestContainer() 32 | self.assertIsInstance(getattr(r, name), dict) 33 | 34 | def test_url_params(self): 35 | self.dict_helper('url_params') 36 | 37 | def test_query_args(self): 38 | self.dict_helper('query_args') 39 | 40 | def test_body_args(self): 41 | self.dict_helper('body_args') 42 | 43 | def test_headers(self): 44 | self.dict_helper('headers') 45 | 46 | def test_content_type(self): 47 | content_type = 'for real;' 48 | headers = {'Content-Type': content_type} 49 | r = RequestContainer(headers=headers) 50 | self.assertEqual(content_type, r.content_type) 51 | r = RequestContainer() 52 | self.assertIsNone(r.content_type) 53 | 54 | # set the content type 55 | r.content_type = 'blah' 56 | self.assertEqual(r.content_type, 'blah') 57 | 58 | def test_get(self): 59 | """ 60 | Tests that the get method appropriately retrieves 61 | paramaters. 62 | """ 63 | r = RequestContainer(url_params=dict(key=1, key2=2)) 64 | self.assertEqual(r.get('key'), 1) 65 | self.assertEqual(r.get('key2'), 2) 66 | 67 | r = RequestContainer(query_args=dict(key=1, key2=2)) 68 | self.assertEqual(r.get('key'), 1) 69 | self.assertEqual(r.get('key2'), 2) 70 | 71 | r = RequestContainer(body_args=dict(key=1, key2=2)) 72 | self.assertEqual(r.get('key'), 1) 73 | self.assertEqual(r.get('key2'), 2) 74 | 75 | r = RequestContainer(url_params=dict(key=1), query_args=dict(key=2), body_args=dict(key=3)) 76 | self.assertEqual(r.get('key'), 1) 77 | 78 | def test_get_not_found(self): 79 | """ 80 | Tests the get with a default value 81 | """ 82 | r = RequestContainer() 83 | self.assertEqual(r.get('fake', 'hey'), 'hey') 84 | self.assertEqual(r.get('fak'), None) 85 | 86 | def test_set_key_error(self): 87 | """ 88 | Asserts that set raises a key error 89 | when the value cannot be found. 90 | """ 91 | req = RequestContainer(query_args=dict(x=1)) 92 | self.assertRaises(KeyError, req.set, 'blah', 'blah') 93 | 94 | def test_set(self): 95 | """ 96 | Tests the basic set on the request 97 | without location specified 98 | """ 99 | original_dict = dict(x=1) 100 | req = RequestContainer(query_args=original_dict) 101 | req.set('x', 2) 102 | self.assertDictEqual(dict(x=2), req.query_args) 103 | 104 | req = RequestContainer(url_params=original_dict) 105 | req.set('x', 2) 106 | self.assertDictEqual(dict(x=2), req.url_params) 107 | 108 | req = RequestContainer(body_args=original_dict) 109 | req.set('x', 2) 110 | self.assertDictEqual(dict(x=2), req.body_args) 111 | 112 | def test_set_location_specified(self): 113 | """ 114 | Tests the set on the request container 115 | with the location specified. 116 | """ 117 | original_dict = dict(x=1) 118 | req = RequestContainer(query_args=original_dict) 119 | req.set('x', 2, QUERY_ARGS) 120 | self.assertDictEqual(dict(x=2), req.query_args) 121 | 122 | req = RequestContainer(url_params=original_dict) 123 | req.set('x', 2, URL_PARAMS) 124 | self.assertDictEqual(dict(x=2), req.url_params) 125 | 126 | req = RequestContainer(body_args=original_dict) 127 | req.set('x', 2, BODY_ARGS) 128 | self.assertDictEqual(dict(x=2), req.body_args) 129 | 130 | def test_set_location_specified_new(self): 131 | """ 132 | Tests the set on the request container 133 | when the key is not already available and 134 | the location is specified. 135 | """ 136 | original_dict = dict(x=1) 137 | req = RequestContainer(url_params=original_dict) 138 | req.set('x', 2, QUERY_ARGS) 139 | self.assertDictEqual(dict(x=2), req.query_args) 140 | self.assertDictEqual(dict(x=1), req.url_params) 141 | 142 | req = RequestContainer(query_args=original_dict) 143 | req.set('x', 2, URL_PARAMS) 144 | self.assertDictEqual(dict(x=2), req.url_params) 145 | self.assertDictEqual(dict(x=1), req.query_args) 146 | 147 | req = RequestContainer(url_params=original_dict) 148 | req.set('x', 2, BODY_ARGS) 149 | self.assertDictEqual(dict(x=2), req.body_args) 150 | self.assertDictEqual(dict(x=1), req.url_params) 151 | -------------------------------------------------------------------------------- /ripozo_tests/unit/resources/restmixins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo import ResourceBase, apimethod, RequestContainer 7 | from ripozo.resources.constructor import ResourceMetaClass 8 | from ripozo.resources.restmixins import Create, Retrieve, Update, \ 9 | Delete, RetrieveRetrieveList, AllOptionsResource 10 | 11 | import mock 12 | import unittest2 13 | 14 | 15 | class TestMixins(unittest2.TestCase): 16 | 17 | def setUp(self): 18 | ResourceMetaClass.registered_names_map = {} 19 | ResourceMetaClass.registered_resource_classes = {} 20 | 21 | def get_fake_manager(self): 22 | manager = mock.MagicMock() 23 | manager.rest_route = False 24 | manager.__rest_route__ = False 25 | return manager 26 | 27 | def test_create(self): 28 | manager2 = mock.MagicMock() 29 | 30 | class T1(Create): 31 | manager = manager2 32 | 33 | request = mock.MagicMock() 34 | response = T1.create(request) 35 | # self.assertEqual(request.translate.call_count, 1) 36 | self.assertEqual(manager2.create.call_count, 1) 37 | self.assertIsInstance(response, T1) 38 | 39 | def test_create_has_apimethod(self): 40 | """ 41 | Tests that the endpoint_dictionary gets the 42 | result 43 | """ 44 | class T1(Create): 45 | manager = self.get_fake_manager() 46 | endpoints = T1.endpoint_dictionary() 47 | self.assertEqual(len(endpoints), 1) 48 | 49 | def test_retrieve_list(self): 50 | manager2 = mock.MagicMock() 51 | manager2.retrieve_list = mock.MagicMock(return_value=(mock.MagicMock(), mock.MagicMock())) 52 | 53 | class T1(RetrieveRetrieveList): 54 | manager = manager2 55 | 56 | request = mock.MagicMock() 57 | response = T1.retrieve_list(request) 58 | self.assertEqual(manager2.retrieve_list.call_count, 1) 59 | self.assertIsInstance(response, T1) 60 | 61 | def test_retrieve(self): 62 | manager2 = mock.MagicMock() 63 | 64 | class T1(Retrieve): 65 | manager = manager2 66 | 67 | request = mock.MagicMock() 68 | response = T1.retrieve(request) 69 | # self.assertEqual(request.translate.call_count, 1) 70 | self.assertEqual(manager2.retrieve.call_count, 1) 71 | self.assertIsInstance(response, T1) 72 | 73 | def test_update(self): 74 | manager2 = mock.MagicMock() 75 | 76 | class T1(Update): 77 | manager = manager2 78 | 79 | request = mock.MagicMock() 80 | response = T1.update(request) 81 | # self.assertEqual(request.translate.call_count, 1) 82 | self.assertEqual(manager2.update.call_count, 1) 83 | self.assertIsInstance(response, T1) 84 | 85 | def test_delete(self): 86 | manager2 = mock.MagicMock() 87 | 88 | class T1(Delete): 89 | manager = manager2 90 | 91 | request = mock.MagicMock() 92 | response = T1.delete(request) 93 | # self.assertEqual(request.translate.call_count, 1) 94 | self.assertEqual(manager2.delete.call_count, 1) 95 | self.assertIsInstance(response, T1) 96 | 97 | def test_all_options_links_no_pks(self): 98 | """ 99 | Tests getting the links for a class with 100 | only no_pks apimethods 101 | """ 102 | class Fake(ResourceBase): 103 | @apimethod(no_pks=True) 104 | def fake(cls, request): 105 | return cls() 106 | 107 | class MyResource(AllOptionsResource): 108 | linked_resource_classes = (Fake,) 109 | 110 | links = MyResource.links 111 | self.assertEqual(len(links), 1) 112 | link = links[0] 113 | self.assertEqual(link.relation, Fake) 114 | self.assertEqual(link.name, 'fake_list') 115 | self.assertTrue(link.no_pks) 116 | 117 | def test_all_options_links_with_pks(self): 118 | """ 119 | Tests getting the links for a class with 120 | only no_pks apimethods 121 | """ 122 | class Fake(ResourceBase): 123 | @apimethod() 124 | def fake(cls, request): 125 | return cls() 126 | 127 | class MyResource(AllOptionsResource): 128 | linked_resource_classes = (Fake,) 129 | 130 | links = MyResource.links 131 | self.assertEqual(len(links), 1) 132 | link = links[0] 133 | self.assertEqual(link.relation, Fake) 134 | self.assertEqual(link.name, 'fake') 135 | self.assertFalse(link.no_pks) 136 | 137 | def test_all_options(self): 138 | """ 139 | Tests calling the all_options route 140 | and constructing all of the links. 141 | """ 142 | class Fake(ResourceBase): 143 | pks = ('id',) 144 | @apimethod() 145 | def fake(cls, request): 146 | return cls() 147 | 148 | @apimethod(no_pks=True) 149 | def fake2(cls, request): 150 | return cls() 151 | 152 | class MyResource(AllOptionsResource): 153 | linked_resource_classes = (Fake,) 154 | 155 | options = MyResource.all_options(RequestContainer()) 156 | self.assertEqual(len(options.linked_resources), 2) 157 | found_fake = False 158 | found_fake_list = False 159 | for l in options.linked_resources: 160 | rel = l.resource 161 | if l.name == 'fake': 162 | self.assertEqual(rel.url, '/fake/') 163 | found_fake = True 164 | if l.name == 'fake_list': 165 | self.assertEqual(rel.url, '/fake') 166 | found_fake_list = True 167 | self.assertTrue(found_fake) 168 | self.assertTrue(found_fake_list) 169 | -------------------------------------------------------------------------------- /ripozo_tests/unit/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | 8 | from . import inmemorymanager -------------------------------------------------------------------------------- /ripozo_tests/unit/tests/inmemorymanager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import unittest2 7 | 8 | from ripozo_tests.bases.manager import TestManagerMixin 9 | from ripozo_tests.helpers.inmemory_manager import InMemoryManager 10 | from ripozo_tests.helpers.util import random_string 11 | 12 | 13 | class InMemoryManagerBaseTestMixin(TestManagerMixin): 14 | @property 15 | def manager(self): 16 | return self._manager 17 | 18 | def random_string(self, length=50): 19 | return random_string(length=length) 20 | 21 | @property 22 | def model_pks(self): 23 | return ['id'] 24 | 25 | def get_model(self, values): 26 | key = values.values()[0] 27 | return self.manager.objects[key] 28 | 29 | def assertValuesEqualModel(self, model, values): 30 | self.assertDictEqual(model, values) 31 | 32 | def assertValuesNotEqualsModel(self, model, values): 33 | self.assertRaises(Exception, self.assertDictEqual, model, values) 34 | 35 | def get_model_pks(self, model): 36 | return dict(id=model['id']) 37 | 38 | def get_values(self, defaults=None): 39 | raise NotImplementedError 40 | 41 | def create_model(self, values=None): 42 | raise NotImplementedError 43 | 44 | 45 | class TestInMemoryManager(InMemoryManagerBaseTestMixin, unittest2.TestCase): 46 | def get_values(self, defaults=None): 47 | values = dict(id=self.random_string(), value1=self.random_string(), value2=self.random_string()) 48 | if defaults: 49 | values.update(defaults) 50 | return values 51 | 52 | def create_model(self, values=None): 53 | values = values or self.get_values() 54 | self.manager.objects[values['id']] = values 55 | return values 56 | 57 | def setUp(self): 58 | class FakeManager(InMemoryManager): 59 | fields = ['id', 'value1', 'value2'] 60 | 61 | self._manager = FakeManager() 62 | 63 | def test_retrieve_filtering(self): 64 | defaults = dict(value1=self.random_string()) 65 | same_value1_count = 5 66 | for i in range(same_value1_count): 67 | self.create_model(values=self.get_values(defaults=defaults)) 68 | resp, meta = self.manager.retrieve_list({}) 69 | self.assertEqual(len(resp), same_value1_count) 70 | for r in resp: 71 | self.assertEqual(defaults['value1'], r['value1']) 72 | 73 | 74 | class TestBaseMixinCoverageFake(unittest2.TestCase): 75 | """ 76 | Just to call the NotImplemented so that 77 | this shit doesn't get annoying. 78 | """ 79 | 80 | def test_not_implemented(self): 81 | manager_mixin = TestManagerMixin() 82 | try: 83 | manager_mixin.manager 84 | except NotImplementedError: 85 | pass 86 | else: 87 | assert False 88 | try: 89 | manager_mixin.model_pks 90 | except NotImplementedError: 91 | pass 92 | else: 93 | assert False 94 | self.assertRaises(NotImplementedError, manager_mixin.assertValuesEqualModel, None, None) 95 | self.assertRaises(NotImplementedError, manager_mixin.assertValuesNotEqualsModel, None, None) 96 | self.assertRaises(NotImplementedError, manager_mixin.create_model) 97 | self.assertRaises(NotImplementedError, manager_mixin.get_model, None) 98 | self.assertRaises(NotImplementedError, manager_mixin.get_model_pks, None) 99 | self.assertRaises(NotImplementedError, manager_mixin.get_values, None) 100 | -------------------------------------------------------------------------------- /ripozo_tests/unit/tests_utilities.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import datetime 7 | import decimal 8 | 9 | import mock 10 | import six 11 | import unittest2 12 | 13 | from ripozo.utilities import titlize_endpoint, join_url_parts, \ 14 | picky_processor, convert_to_underscore, make_json_safe, get_or_pop 15 | 16 | 17 | class UtilitiesTestCase(unittest2.TestCase): 18 | 19 | def test_convert_to_underscore(self): 20 | camel_case_names = ['CamelCase', 'camelCase', 'camel_case', '_CamelCase', 21 | 'APICamelCase', 'CamelCaseAPI', 'Camel2Case', 22 | 'CamelCase2', 'CamelCase2API2'] 23 | 24 | underscore_names = ['camel_case', 'camel_case', 'camel_case', '__camel_case', 25 | 'api_camel_case', 'camel_case_api', 'camel2_case', 26 | 'camel_case2', 'camel_case2_api2'] 27 | if len(camel_case_names) != len(underscore_names): 28 | raise Exception("The number of entries in camel_case_names must match underscore_names") 29 | 30 | for i in range(len(camel_case_names)): 31 | old_name = camel_case_names[i] 32 | new_name = convert_to_underscore(old_name) 33 | self.assertEqual(underscore_names[i], new_name) 34 | 35 | def test_titlelize_endpoint(self): 36 | """ 37 | Tests whether an underscored function name 38 | is properly converted into a title 39 | """ 40 | name = "some_name_or_something" 41 | expected = "Some Name Or Something" 42 | updated = titlize_endpoint(name) 43 | self.assertEqual(updated, expected) 44 | 45 | name = '_some_name_or_something' 46 | updated = titlize_endpoint(name) 47 | self.assertEqual(updated, expected) 48 | 49 | name = 'some_name_or_something_' 50 | updated = titlize_endpoint(name) 51 | self.assertEqual(updated, expected) 52 | 53 | def test_join_url_parts(self): 54 | url = join_url_parts() 55 | self.assertIsInstance(url, six.text_type) 56 | self.assertEqual('', url) 57 | 58 | url = join_url_parts('/something', '/another', '/thing') 59 | self.assertEqual(url, '/something/another/thing') 60 | 61 | url = join_url_parts('something/', '/another/', '/thing') 62 | self.assertEqual(url, 'something/another/thing') 63 | 64 | url = join_url_parts('something//', '/another') 65 | self.assertEqual(url, 'something/another') 66 | 67 | url = join_url_parts('/', '/another') 68 | self.assertEqual('/another', url) 69 | 70 | url = join_url_parts('/', '/') 71 | self.assertEqual('/', url) 72 | 73 | def test_picky_processor(self): 74 | processor = mock.Mock() 75 | if six.PY2: 76 | processor.__name__ = six.binary_type('FAKE') 77 | else: 78 | processor.__name__ = six.text_type('FAKE') 79 | does_run = picky_processor(processor) 80 | does_run(mock.Mock(), 'runs') 81 | self.assertEqual(processor.call_count, 1) 82 | 83 | does_run = picky_processor(processor, include=['runs']) 84 | does_run(mock.Mock(), 'runs') 85 | self.assertEqual(processor.call_count, 2) 86 | 87 | does_run = picky_processor(processor, exclude=['nope']) 88 | does_run(mock.Mock(), 'runs') 89 | self.assertEqual(processor.call_count, 3) 90 | 91 | does_run = picky_processor(processor, include=['runs'], exclude=['nope']) 92 | does_run(mock.Mock(), 'runs') 93 | self.assertEqual(processor.call_count, 4) 94 | 95 | doesnt_run = picky_processor(processor, include=['nope']) 96 | doesnt_run(mock.MagicMock(), 'runs') 97 | self.assertEqual(processor.call_count, 4) 98 | 99 | doesnt_run = picky_processor(processor, exclude=['runs']) 100 | doesnt_run(mock.MagicMock(), 'runs') 101 | self.assertEqual(processor.call_count, 4) 102 | 103 | def test_make_json_safe(self): 104 | """ 105 | Tests whether the make_json_safe method correctly 106 | returns values. 107 | """ 108 | # Test times 109 | resp = make_json_safe(datetime.datetime.now()) 110 | self.assertIsInstance(resp, six.text_type) 111 | resp = make_json_safe(datetime.date.today()) 112 | self.assertIsInstance(resp, six.text_type) 113 | resp = make_json_safe(datetime.time()) 114 | self.assertIsInstance(resp, six.text_type) 115 | resp = make_json_safe(datetime.timedelta(days=1)) 116 | self.assertIsInstance(resp, six.text_type) 117 | 118 | # Test decimals 119 | resp = make_json_safe(decimal.Decimal('1.02')) 120 | self.assertEqual(resp, 1.02) 121 | self.assertIsInstance(resp, float) 122 | 123 | # Test lists 124 | l = [datetime.time(), datetime.date.today(), datetime.datetime.now()] 125 | resp = make_json_safe(l) 126 | self.assertIsInstance(resp, list) 127 | for item in resp: 128 | self.assertIsInstance(item, six.text_type) 129 | 130 | # Test dictionary 131 | d = dict(a=datetime.datetime.now(), b=datetime.time(), c=datetime.date.today()) 132 | resp = make_json_safe(d) 133 | self.assertIsInstance(resp, dict) 134 | for key, value in six.iteritems(resp): 135 | self.assertIsInstance(value, six.text_type) 136 | 137 | def test_join_url_parts_ints(self): 138 | """joining parts when a single int. Ensuring that it is unicode""" 139 | resp = join_url_parts(1) 140 | self.assertEqual(resp, '1') 141 | 142 | def test_get_or_pop(self): 143 | """Simple test to ensure that the get_or_pop 144 | returns the value and appropriately updates the 145 | dictionary if necessary""" 146 | x = dict(x=1) 147 | val = get_or_pop(x, 'x', pop=False) 148 | self.assertDictEqual(x, dict(x=1)) 149 | self.assertEqual(val, 1) 150 | val = get_or_pop(x, 'x', pop=True) 151 | self.assertDictEqual(x, dict()) 152 | self.assertEqual(val, 1) 153 | 154 | def test_get_or_pop_default(self): 155 | """Ensures that a default is returned 156 | if the key is not available""" 157 | x = dict() 158 | val = get_or_pop(x, 'x', pop=False) 159 | self.assertIsNone(val) 160 | val = get_or_pop(x, 'x', pop=True) 161 | self.assertIsNone(val) 162 | val = get_or_pop(x, 'x', default=1, pop=False) 163 | self.assertEqual(val, 1) 164 | val = get_or_pop(x, 'x', default=1, pop=True) 165 | self.assertEqual(val, 1) 166 | 167 | 168 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from setuptools import setup, find_packages 7 | 8 | __author__ = 'Tim Martin' 9 | __pkg_name__ = 'ripozo' 10 | 11 | version = '1.3.1.dev0' 12 | 13 | setup( 14 | author=__author__, 15 | author_email='tim.martin@vertical-knowledge.com', 16 | classifiers=[ 17 | 'Development Status :: 3 - Alpha', 18 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 2', 21 | 'Programming Language :: Python :: 2.6', 22 | 'Programming Language :: Python :: 2.7', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.3', 25 | 'Programming Language :: Python :: 3.4', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: Implementation :: PyPy', 28 | 'Topic :: Software Development :: Libraries :: Python Modules' 29 | ], 30 | description='RESTful API framework with HATEOAS support and compatibility with Flask, Django, SQLAlchemy and more.', 31 | extras_require={ 32 | 'examples': [ 33 | 'flask', 34 | 'requests', 35 | 'sqlalchemy', 36 | ], 37 | 'docs': [ 38 | 'sphinx' 39 | ] 40 | }, 41 | install_requires=[ 42 | 'six>=1.4.1,!=1.7.1' 43 | ], 44 | keywords='REST HATEOAS Hypermedia RESTful SIREN HAL API JSONAPI web framework Django Flask SQLAlchemy Cassandra', 45 | name='ripozo', 46 | packages=find_packages(include=['ripozo', 'ripozo.*']), 47 | tests_require=[ 48 | 'unittest2', 49 | 'tox', 50 | 'mock', 51 | ], 52 | test_suite="ripozo_tests", 53 | url='http://ripozo.readthedocs.org/', 54 | version=version 55 | ) 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py33,py34,pypy,pypy3 3 | 4 | [testenv] 5 | commands = 6 | python setup.py install 7 | python setup.py test 8 | --------------------------------------------------------------------------------