pax_global_header00006660000000000000000000000064131140626540014514gustar00rootroot0000000000000052 comment=0accfd0d1ecec4726d0de350e89380ce442a82db restless-2.1.1/000077500000000000000000000000001311406265400133615ustar00rootroot00000000000000restless-2.1.1/.gitignore000066400000000000000000000002421311406265400153470ustar00rootroot00000000000000.DS_Store *.pyc __pycache__ build dist docs/_build tests/.coverage tests/cover tests/htmlcov .coverage htmlcov *.egg-info .cache/ .eggs/ .tox/ *.swp .idea/ *.whl restless-2.1.1/.travis.yml000066400000000000000000000007051311406265400154740ustar00rootroot00000000000000sudo: false language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "pypy" # cPython 2.7 env: - DJANGO="1.8" - DJANGO="1.9" - DJANGO="1.10" - DJANGO="1.11" matrix: exclude: - python: "3.3" env: DJANGO="1.9" - python: "3.3" env: DJANGO="1.10" - python: "3.3" env: DJANGO="1.11" # command to install dependencies install: - pip install six tox-travis # command to run tests script: - tox restless-2.1.1/AUTHORS000066400000000000000000000006371311406265400144370ustar00rootroot00000000000000Primary authors: * Daniel Lindsley (@toastdriven) Maintainers: * Bruno Marques (@ElSaico) * Sergio Oliveira (@seocam) Contributors: * Matt George * socketubs * mission-liao * frewsxcv * viniciuscainelli * schmitch * karmux * binarydud * issackelly * jmwohl * jphalip * mindflayer * czartur * leonsmith * PabloCastellano * skevy * Ana Carolina (@anacarolinats) * Xiaoli Wang (@Xiaoli) * Tony Bajan (@tonybajan) restless-2.1.1/LICENSE000066400000000000000000000027251311406265400143740ustar00rootroot00000000000000Copyright (c) 2014, Daniel Lindsley All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the restless nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. restless-2.1.1/MANIFEST.in000066400000000000000000000001141311406265400151130ustar00rootroot00000000000000recursive-include docs * include AUTHORS include LICENSE include README.rst restless-2.1.1/README.rst000066400000000000000000000102111311406265400150430ustar00rootroot00000000000000======== restless ======== .. image:: https://travis-ci.org/toastdriven/restless.png?branch=master :target: https://travis-ci.org/toastdriven/restless .. image:: https://coveralls.io/repos/github/toastdriven/restless/badge.svg?branch=master :target: https://coveralls.io/github/toastdriven/restless?branch=master A lightweight REST miniframework for Python. Documentation is at http://restless.readthedocs.org/. Works great with Django_, Flask_, Pyramid_, Tornado_ & Itty_, but should be useful for many other Python web frameworks. Based on the lessons learned from Tastypie_ & other REST libraries. .. _Django: http://djangoproject.com/ .. _Flask: http://flask.pocoo.org/ .. _Pyramid: http://www.pylonsproject.org/ .. _Itty: https://pypi.python.org/pypi/itty .. _Tastypie: http://tastypieapi.org/ .. _Tornado: http://www.tornadoweb.org/ .. _tox: https://tox.readthedocs.io/ Features ======== * Small, fast codebase * JSON output by default, but overridable * RESTful * Python 3.2+ (with shims to make broke-ass Python 2.6+ work) * Django 1.8+ * Flexible Anti-Features ============= (Things that will never be added...) * Automatic ORM integration * Authorization (per-object or not) * Extensive filtering options * XML output (though you can implement your own) * Metaclasses * Mixins * HATEOAS Why? ==== Quite simply, I care about creating flexible & RESTFul APIs. In building Tastypie, I tried to create something extremely complete & comprehensive. The result was writing a lot of hook methods (for easy extensibility) & a lot of (perceived) bloat, as I tried to accommodate for everything people might want/need in a flexible/overridable manner. But in reality, all I really ever personally want are the RESTful verbs, JSON serialization & the ability of override behavior. This one is written for me, but maybe it's useful to you. Manifesto ========= Rather than try to build something that automatically does the typically correct thing within each of the views, it's up to you to implement the bodies of various HTTP methods. Example code: .. code:: python # posts/api.py from django.contrib.auth.models import User from restless.dj import DjangoResource from restless.preparers import FieldsPreparer from posts.models import Post class PostResource(DjangoResource): # Controls what data is included in the serialized output. preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', }) # GET / def list(self): return Post.objects.all() # GET /pk/ def detail(self, pk): return Post.objects.get(id=pk) # POST / def create(self): return Post.objects.create( title=self.data['title'], user=User.objects.get(username=self.data['author']), content=self.data['body'] ) # PUT /pk/ def update(self, pk): try: post = Post.objects.get(id=pk) except Post.DoesNotExist: post = Post() post.title = self.data['title'] post.user = User.objects.get(username=self.data['author']) post.content = self.data['body'] post.save() return post # DELETE /pk/ def delete(self, pk): Post.objects.get(id=pk).delete() Hooking it up: .. code:: python # api/urls.py from django.conf.urls.default import url, include from posts.api import PostResource urlpatterns = [ # The usual suspects, then... url(r'^api/posts/', include(PostResource.urls())), ] Licence ======= BSD Running the Tests ================= The test suite uses tox_ for simultaneous support of multiple versions of both Python and Django. The current versions of Python supported are: * CPython 2.7 * CPython 3.3 * CPython 3.4 * CPython 3.5 * CPython 3.6 * PyPy (Python 2.7) You just need to install the Python interpreters above and the `tox` package (available via `pip`), then run the `tox` command. restless-2.1.1/docs/000077500000000000000000000000001311406265400143115ustar00rootroot00000000000000restless-2.1.1/docs/Makefile000066400000000000000000000151621311406265400157560ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/restless.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/restless.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/restless" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/restless" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." restless-2.1.1/docs/conf.py000066400000000000000000000202711311406265400156120ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # restless documentation build configuration file, created by # sphinx-quickstart on Sat Jan 11 01:04:55 2014. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'restless' copyright = u'2014, Daniel Lindsley' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # import restless # noqa # The full version, including alpha/beta/rc tags. release = restless.VERSION # The short X.Y version. version = restless.VERSION # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'restlessdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'restless.tex', u'restless Documentation', u'Daniel Lindsley', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'restless', u'restless Documentation', [u'Daniel Lindsley'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'restless', u'restless Documentation', u'Daniel Lindsley', 'restless', u'A lightweight REST miniframework for Python.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Blerg. The autodocs for the Django module freak out if this isn't done. from django.conf import settings settings.configure() restless-2.1.1/docs/contributing.rst000066400000000000000000000064641311406265400175640ustar00rootroot00000000000000.. _contributing: ============ Contributing ============ Restless is open-source and, as such, grows (or shrinks) & improves in part due to the community. Below are some guidelines on how to help with the project. Philosophy ========== * Restless is BSD-licensed. All contributed code must be either * the original work of the author, contributed under the BSD, or... * work taken from another project released under a BSD-compatible license. * GPL'd (or similar) works are not eligible for inclusion. * Restless's git master branch should always be stable, production-ready & passing all tests. * Major releases (1.x.x) are commitments to backward-compatibility of the public APIs. Any documented API should ideally not change between major releases. The exclusion to this rule is in the event of a security issue. * Minor releases (x.3.x) are for the addition of substantial features or major bugfixes. * Patch releases (x.x.4) are for minor features or bugfixes. Guidelines For Reporting An Issue/Feature ========================================= So you've found a bug or have a great idea for a feature. Here's the steps you should take to help get it added/fixed in Restless: * First, check to see if there's an existing issue/pull request for the bug/feature. All issues are at https://github.com/toastdriven/restless/issues and pull reqs are at https://github.com/toastdriven/restless/pulls. * If there isn't one there, please file an issue. The ideal report includes: * A description of the problem/suggestion. * How to recreate the bug. * If relevant, including the versions of your: * Python interpreter * Web framework * Restless * Optionally of the other dependencies involved * Ideally, creating a pull request with a (failing) test case demonstrating what's wrong. This makes it easy for us to reproduce & fix the problem. Instructions for running the tests are at :doc:`index` Guidelines For Contributing Code ================================ If you're ready to take the plunge & contribute back some code/docs, the process should look like: * Fork the project on GitHub into your own account. * Clone your copy of Restless. * Make a new branch in git & commit your changes there. * Push your new branch up to GitHub. * Again, ensure there isn't already an issue or pull request out there on it. If there is & you feel you have a better fix, please take note of the issue number & mention it in your pull request. * Create a new pull request (based on your branch), including what the problem/feature is, versions of your software & referencing any related issues/pull requests. In order to be merged into Restless, contributions must have the following: * A solid patch that: * is clear. * works across all supported versions of Python. * follows the existing style of the code base (mostly PEP-8). * comments included as needed. * A test case that demonstrates the previous flaw that now passes with the included patch. * If it adds/changes a public API, it must also include documentation for those changes. * Must be appropriately licensed (see "Philosophy"). * Adds yourself to the AUTHORS file. If your contribution lacks any of these things, they will have to be added by a core contributor before being merged into Restless proper, which may take additional time. restless-2.1.1/docs/cookbook.rst000066400000000000000000000013401311406265400166470ustar00rootroot00000000000000.. _cookbook: ======== Cookbook ======== This is a place for community-contributed patterns & ideas for extending Restless. Authentication ============== If your framework has the concept of a logged-in user (like Django), you can do something like:: class MyResource(DjangoResource): def is_authenticated(self): return self.request.user.is_authenticated() If you need a more fine graned authentication you could check your current endpoint and do something like that:: class MyResource(DjangoResource): def is_authenticated(self): if self.endpoint in ('update', 'create'): return self.request.user.is_authenticated() else: return True restless-2.1.1/docs/extending.rst000066400000000000000000000442571311406265400170440ustar00rootroot00000000000000.. _extending: ================== Extending Restless ================== Restless is meant to handle many simpler cases well & have enough extensibility to handle more complex API tasks. However, a specific goal of the project is to not expand the scope much & simply give you, the expert on your API, the freedom to build what you need. We'll be covering: * Custom endpoints * Customizing data output * Adding data validation * Providing different serialization formats Custom Endpoints ================ Sometimes you need to provide more than just the typical HTTP verbs. Restless allows you to hook up custom endpoints that can take advantage of much of the ``Resource``. Implementing these views requires a couple simple steps: * Writing the method * Adding to the ``Resource.http_methods`` mapping * Adding to your URL routing For instance, if you wanted to added a schema view (``/api/posts/schema/``) that responded to ``GET`` requests, you'd first write the method:: from restless.dj import DjangoResource from restless.resources import skip_prepare class PostResource(DjangoResource): # The usual methods, then... @skip_prepare def schema(self): # Return your schema information. # We're keeping it simple (basic field names & data types). return { 'fields': { 'id': 'integer', 'title': 'string', 'author': 'string', 'body': 'string', }, } The next step is to update the :py:attr:`Resource.http_methods`. This can either be fully written out in your class or (as I prefer) a small extension to your ``__init__``...:: from restless.dj import DjangoResource from restless.resources import skip_prepare class PostResource(DjangoResource): # We'll lightly extend the ``__init__``. def __init__(self, *args, **kwargs): super(PostResource, self).__init__(*args, **kwargs) # Add on a new top-level key, then define what HTTP methods it # listens on & what methods it calls for them. self.http_methods.update({ 'schema': { 'GET': 'schema', } }) # The usual methods, then... @skip_prepare def schema(self): return { 'fields': { 'id': 'integer', 'title': 'string', 'author': 'string', 'body': 'string', }, } Finally, it's just a matter of hooking up the URLs as well. You can do this manually or (once again) by extending a built-in method.:: # Add the correct import here. from django.conf.urls import url from restless.dj import DjangoResource from restless.resources import skip_prepare class PostResource(DjangoResource): def __init__(self, *args, **kwargs): super(PostResource, self).__init__(*args, **kwargs) self.http_methods.update({ 'schema': { 'GET': 'schema', } }) # The usual methods, then... # Note: We're using the ``skip_prepare`` decorator here so that Restless # doesn't run ``prepare`` on the schema data. # If your custom view returns a typical ``object/dict`` (like the # ``detail`` method), you can omit this. @skip_prepare def schema(self): return { 'fields': { 'id': 'integer', 'title': 'string', 'author': 'string', 'body': 'string', }, } # Finally, extend the URLs. @classmethod def urls(cls, name_prefix=None): urlpatterns = super(PostResource, cls).urls(name_prefix=name_prefix) return [ url(r'^schema/$', cls.as_view('schema'), name=cls.build_url_name('schema', name_prefix)), ] + urlpatterns .. note:: This step varies from framework to framework around hooking up the URLs/routes. The code is specific to the :py:class:`restless.dj.DjangoResource`, but the approach is the same regardless. You should now be able to hit something like http://127.0.0.1/api/posts/schema/ in your browser & get a JSON schema view! Customizing Data Output ======================= There are four approaches to customizing your data ouput. #. The built-in ``Preparer/FieldsPreparer`` (simple) #. The included ``SubPreparer/CollectionSubPreparer`` (slightly more complex) #. Overriding :py:meth:`restless.resources.Resource.prepare` (happy medium) #. Per-method data (flexible but most work) Fields ------ Using ``FieldsPreparer`` is documented elsewhere (see the :ref:`tutorial`), but the basic gist is that you create a ``FieldsPreparer`` instance & assign it on your resource class. It takes a ``fields`` parameter, which should be a dictionary of fields to expose. Example:: class MyResource(Resource): preparer = FieldsPreparer(fields={ # Expose the same name. "id": "id", # Rename a field. "author": "username", # Access deeper data. "type_id": "metadata.type.pk", }) This dictionary is a mapping, with keys representing the final name & the values acting as a lookup path. If the lookup path **has no** periods (i.e. ``name``) in it, it's considered to be an attribute/key on the item being processed. If that item looks like a ``dict``, key access is attempted. If it looks like an ``object``, attribute access is used. In either case, the found value is returned. If the lookup path **has** periods (i.e. ``entry.title``), it is split on the periods (like a Python import path) and recursively uses the previous value to look up the next value until a final value is found. Subpreparers & Collections -------------------------- Sometimes, your data isn't completely flat but is instead nested. This frequently occurs in conjunction with related data, such as a foreign key'd object or many-to-many scenario. In this case, you can lever "subpreparers". Restless ships with two of these, the ``SubPreparer`` & the ``CollectionSubPreparer``. The ``SubPreparer`` is useful for a single nested relation. You define a regular ``Preparer/FieldsPreparer`` (perhaps in a shareable location), then use the ``SubPreparer`` to pull it in & incorporate the nested data. For example:: # We commonly expose author information in our API as nested data. # This definition can happen in its own module or wherever needed. author_preparer = FieldsPreparer(fields={ 'id': 'pk', 'username': 'username', 'name': 'get_full_name', }) # ... # Then, in the main preparer, pull them in using `SubPreparer`. preparer = FieldsPreparer(fields={ 'author': SubPreparer('user', author_preparer), # Other fields can come before/follow as normal. 'content': 'post', 'created': 'created_at', }) This results in output like:: { "content": "Isn't my blog cool? I think so...", "created": "2017-05-22T10:34:48", "author": { "id": 5, "username": "joe", "name": "Joe Bob" } } The ``CollectionSubPreparer`` operates on the same principle (define a set of fields to be nested), but works with collections of things. These collections should be ordered & behave similar to iterables like ``list``s & ``tuples``. As an example:: # Set up a preparer that handles the data for each thing in the broader # collection. # Again, this can be in its own module or just wherever it's needed. comment_preparer = FieldsPreparer(fields={ 'comment': 'comment_text', 'created': 'created', }) # Use it with the ``CollectionSubPreparer`` to create a list # of prepared sub items. preparer = FieldsPreparer(fields={ # A normal blog post field. 'post': 'post_text', # All the comments on the post. 'comments': CollectionSubPreparer('comments.all', comment_preparer), }) Which would produce output like:: { "post": "Another day, another blog post.", "comments": [ { "comment": "I hear you. Boring day here too.", "created": "2017-05-23T16:43:22" }, { "comment": "SPAM SPAM SPAM", "created": "2017-05-24T21:21:21" } ] } Overriding ``prepare`` ---------------------- For every item (``object`` or ``dict``) that gets serialized as output, it runs through a ``prepare`` method on your ``Resource`` subclass. The default behavior checks to see if you have ``fields`` defined on your class & either just returns all the data (if there's no ``fields``) or uses the ``fields`` to extract plain data. However, you can use/abuse this method for your own nefarious purposes. For example, if you wanted to serve an API of users but sanitize the data, you could do something like:: from django.contrib.auth.models import User from restless.dj import DjangoResource from restless.preparers import FieldsPreparer class UserResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'username': 'username', # We're including email here, but we'll sanitize it later. 'email': 'email', 'date_joined': 'date_joined', }) def list(self): return User.objects.all() def detail(self, pk): return User.objects.get(pk=pk) def prepare(self, data): # ``data`` is the object/dict to be exposed. # We'll call ``super`` to prep the data, then we'll mask the email. prepped = super(UserResource, self).prepare(data) email = prepped['email'] at_offset = email.index('@') prepped['email'] = email[:at_offset + 1] + "..." return prepped This example is somewhat contrived, but you can perform any kind of transformation you want here, as long as you return a plain, serializable ``dict``. Per-Method Data --------------- Because Restless can serve plain old Python objects (anything JSON serializable + ``datetime`` + ``decimal``), the ultimate form of control is simply to load your data however you want, then return a simple/serializable form. For example, Django's ``models.Model`` classes are not normally JSON-serializable. We also may want to expose related data in a nested form. Here's an example of doing something like that.:: from restless.dj import DjangoResource from posts.models import Post class PostResource(DjangoResource): def detail(self, pk): # We do our rich lookup here. post = Post.objects.get(pk=pk).select_related('user') # Then we can simplify it & include related information. return { 'title': post.title, 'author': { 'id': post.user.id, 'username': post.user.username, 'date_joined': post.user.date_joined, # We exclude things like ``password`` & ``email`` here # intentionally. }, 'body': post.content, # ... } While this is more verbose, it gives you all the control. If you have resources for your nested data, you can also re-use them to make the construction easier. For example:: from django.contrib.auth.models import User from restless.dj import DjangoResource from restless.preparers import FieldsPreparer from posts.models import Post class UserResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'username': 'username', 'date_joined': 'date_joined', }) def detail(self, pk): return User.objects.get(pk=pk) class PostResource(DjangoResource): def detail(self, pk): # We do our rich lookup here. post = Post.objects.get(pk=pk).select_related('user') # Instantiate the ``UserResource`` ur = UserResource() # Then populate the data. return { 'title': post.title, # We leverage the ``prepare`` method from above to build the # nested data we want. 'author': ur.prepare(post.user), 'body': post.content, # ... } Data Validation =============== Validation can be a contentious issue. No one wants to risk data corruption or security holes in their services. However, there's no real standard or consensus on doing data validation even within the **individual** framework communities themselves, let alone *between* frameworks. So unfortunately, Restless mostly ignores this issue, leaving you to do data validation the way you think is best. The good news is that the data you'll need to validate is already in a convenient-to-work-with dictionary called ``Resource.data`` (assigned immediately after deserialization takes place). The recommended approach is to simply add on to your data methods themselves. For example, since Django ``Form`` objects are at least *bundled* with the framework, we'll use those as an example...:: from django.forms import ModelForm from restless.dj import DjangoResource from restless.exceptions import BadRequest class UserForm(ModelForm): class Meta(object): model = User fields = ['username', 'first_name', 'last_name', 'email'] class UserResource(DjangoResource): preparer = FieldsPreparer(fields={ "id": "id", "username": "username", "first_name": "first_name", "last_name": "last_name", "email": "email", }) def create(self): # We can create a bound form from the get-go. form = UserForm(self.data) if not form.is_valid(): raise BadRequest('Something is wrong.') # Continue as normal, using the form data instead. user = User.objects.create( username=form.cleaned_data['username'], first_name=form.cleaned_data['first_name'], last_name=form.cleaned_data['last_name'], email=form.cleaned_data['email'], ) return user If you're going to use this validation in other places, you're welcome to DRY up your code into a validation method. An example of this might look like...:: from django.forms import ModelForm from restless.dj import DjangoResource from restless.exceptions import BadRequest class UserForm(ModelForm): class Meta(object): model = User fields = ['username', 'first_name', 'last_name', 'email'] class UserResource(DjangoResource): preparer = FieldsPreparer(fields={ "id": "id", "username": "username", "first_name": "first_name", "last_name": "last_name", "email": "email", }) def validate_user(self): form = UserForm(self.data) if not form.is_valid(): raise BadRequest('Something is wrong.') return form.cleaned_data def create(self): cleaned = self.validate_user() user = User.objects.create( username=cleaned['username'], first_name=cleaned['first_name'], last_name=cleaned['last_name'], email=cleaned['email'], ) return user def update(self, pk): cleaned = self.validate_user() user = User.objects.get(pk=pk) user.username = cleaned['username'] user.first_name = cleaned['first_name'] user.last_name = cleaned['last_name'] user.email = cleaned['email'] user.save() return user Alternative Serialization ========================= For some, Restless' JSON-only syntax might not be appealing. Fortunately, overriding this is not terribly difficult. For the purposes of demonstration, we'll implement YAML in place of JSON. The process would be similar (but much more verbose) for XML (& brings `a host of problems`_ as well). Start by creating a ``Serializer`` subclass for the YAML. We'll override a couple methods there. This code can live anywhere, as long as it is importable for your ``Resource``.:: import yaml from restless.serializers import Serializer class YAMLSerializer(Serializer): def deserialize(self, body): # Do **NOT** use ``yaml.load`` here, as it can contain things like # *functions* & other dangers! return yaml.safe_load(body) def serialize(self, data): return yaml.dump(data) Once that class has been created, it's just a matter of assigning an instance onto your ``Resource``.:: # Old. class MyResource(Resource): # This was present by default. serializer = JSONSerializer() # New. class MyResource(Resource): serializer = YAMLSerializer() You can even do things like handle multiple serialization formats, say if the user provides a ``?format=yaml`` GET param...:: from restless.serializers import Serializer from restless.utils import json, MoreTypesJSONEncoder from django.template import Context, Template class MultiSerializer(Serializer): def deserialize(self, body): # This is Django-specific, but all frameworks can handle GET # parameters... ct = request.GET.get('format', 'json') if ct == 'yaml': return yaml.safe_load(body) else: return json.load(body) def serialize(self, data): # Again, Django-specific. ct = request.GET.get('format', 'json') if ct == 'yaml': return yaml.dump(body) else: return json.dumps(body, cls=MoreTypesJSONEncoder) .. _`a host of problems`: https://pypi.python.org/pypi/defusedxml restless-2.1.1/docs/index.rst000066400000000000000000000027251311406265400161600ustar00rootroot00000000000000======== restless ======== A lightweight REST miniframework for Python. Works great with Django_, Flask_, Pyramid_ & Tornado_, but should be useful for many other Python web frameworks. Based on the lessons learned from Tastypie_ & other REST libraries. .. _Django: http://djangoproject.com/ .. _Flask: http://flask.pocoo.org/ .. _Pyramid: http://www.pylonsproject.org/ .. _Tastypie: http://tastypieapi.org/ .. _Tornado: http://www.tornadoweb.org/ .. _tox: https://tox.readthedocs.io/ Features ======== * Small, fast codebase * JSON output by default, but overridable * RESTful * Python 3.2+ (with shims to make broke-ass Python 2.6+ work) * Flexible Anti-Features ============= (Things that will never be added...) * Automatic ORM integration * Authorization (per-object or not) * Extensive filtering options * XML output (though you can implement your own) * Metaclasses * Mixins * HATEOAS Topics ====== .. toctree:: :maxdepth: 2 tutorial philosophy extending .. toctree:: :maxdepth: 1 cookbook contributing security API Reference ============= .. toctree:: :glob: reference/* Release Notes ============= .. toctree:: :maxdepth: 1 releasenotes/v2.0.3 releasenotes/v2.0.2 releasenotes/v2.0.1 releasenotes/v2.0.0 releasenotes/v1.4.0 releasenotes/v1.3.0 releasenotes/v1.2.0 releasenotes/v1.1.0 releasenotes/v1.0.0 Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` restless-2.1.1/docs/make.bat000066400000000000000000000150611311406265400157210ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\restless.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\restless.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end restless-2.1.1/docs/philosophy.rst000066400000000000000000000045211311406265400172430ustar00rootroot00000000000000.. _philosophy: ========================== Philosophy Behind Restless ========================== Quite simply, I care about creating flexible & RESTFul APIs. In building Tastypie, I tried to create something extremely complete & comprehensive. The result was writing a lot of hook methods (for easy extensibility) & a lot of (perceived) bloat, as I tried to accommodate for everything people might want/need in a flexible/overridable manner. But in reality, all I really ever personally want are the RESTful verbs, JSON serialization & the ability of override behavior. This one is written for me, but maybe it's useful to you. .. note: I wrote most of Tastypie & have worked with many other RESTful frameworks. Commentary here is not meant as a slam, simply a point of difference. Pithy Statements ================ Keep it **simple** Complexity is the devil. It makes it harder to maintain, hard to be portable & hard for users to work with. Python 3 is the default Why write for the past? We'll support it, but Python 2.X should be treated as a (well-supported) afterthought. BSD Licensed Any other license is a bug. Work with as many web frameworks as possible. Django is my main target, but I want portable code that I can use from other frameworks. Switching frameworks ought to be simply a change of inheritance. RESTful by default REST is native to the web & works well. We should make it easy to *be* RESTful. If I wanted RPC, I'd just write my own crazy methods that did whatever I wanted. JSON-only Because XML sucks, bplist is Apple-specific & YAML is a security rats nest. Everything (I care about) speaks JSON, so let's keep it simple. B.Y.O.D. (Bring Your Own Data) Don't integrate with a specific ORM. Don't mandate a specific access format. We expose 8-ish simple methods (that map cleanly to the REST verbs/endpoints). Data access/manipulation happens there & the user knows best, so they should implement it. No HATEOAS I loved HATEOAS dearly, but it is complex & making it work with many frameworks is a windmill I don't care to tilt at. Most people never use the deep links anyhow. No Authorization Authorization schemes vary so wildly & everyone wants something different. Give them the ability to write it without natively trying to support it. restless-2.1.1/docs/reference/000077500000000000000000000000001311406265400162475ustar00rootroot00000000000000restless-2.1.1/docs/reference/constants.rst000066400000000000000000000007271311406265400210230ustar00rootroot00000000000000.. ref-constants ========= Constants ========= restless.constants ------------------ A set of constants included with ``restless``. Mostly nice status code mappings for use in exceptions or the ``Resource.status_map``. **OK** = ``200`` **CREATED** = ``201`` **ACCEPTED** = ``202`` **NO_CONTENT** = ``204`` **UNAUTHORIZED** = ``401`` **NOT_FOUND** = ``404`` **METHOD_NOT_ALLOWED** = ``405`` **APPLICATION_ERROR** = ``500`` **METHOD_NOT_IMPLEMENTED** = ``501`` restless-2.1.1/docs/reference/data.rst000066400000000000000000000001701311406265400177100ustar00rootroot00000000000000.. ref-data ==== Data ==== restless.data ------------- .. automodule:: restless.data :members: :undoc-members: restless-2.1.1/docs/reference/exceptions.rst000066400000000000000000000002421311406265400211600ustar00rootroot00000000000000.. ref-exceptions ========== Exceptions ========== restless.exceptions ------------------- .. automodule:: restless.exceptions :members: :undoc-members: restless-2.1.1/docs/reference/preparers.rst000066400000000000000000000002311311406265400210000ustar00rootroot00000000000000.. ref-prepare ========= Preparers ========= restless.preparers ------------------ .. automodule:: restless.preparers :members: :undoc-members: restless-2.1.1/docs/reference/resources.rst000066400000000000000000000012351311406265400210140ustar00rootroot00000000000000.. ref-resources ========= Resources ========= restless.resources ------------------ .. automodule:: restless.resources :members: :undoc-members: restless.dj ----------- .. automodule:: restless.dj :members: :undoc-members: restless.fl ----------- .. automodule:: restless.fl :members: :undoc-members: restless.pyr ------------ .. automodule:: restless.pyr :members: :undoc-members: restless.it ----------- .. automodule:: restless.it :members: :undoc-members: restless.tnd ------------ .. autoclass:: restless.tnd.TornadoResource :members: as_detail, as_list, request, application, _request_handler_base_, r_handler restless-2.1.1/docs/reference/serializers.rst000066400000000000000000000002511311406265400213330ustar00rootroot00000000000000.. ref-serializers =========== Serializers =========== restless.serializers -------------------- .. automodule:: restless.serializers :members: :undoc-members: restless-2.1.1/docs/reference/utils.rst000066400000000000000000000001771311406265400201460ustar00rootroot00000000000000.. ref-utils ===== Utils ===== restless.utils -------------- .. automodule:: restless.utils :members: :undoc-members: restless-2.1.1/docs/releasenotes/000077500000000000000000000000001311406265400170025ustar00rootroot00000000000000restless-2.1.1/docs/releasenotes/v1.0.0.rst000066400000000000000000000003241311406265400203550ustar00rootroot00000000000000restless v1.0.0 =============== :date: 2014-01-12 Initial production-ready release! Whoo hoo! Includes: * Full GET/CREATE/UPDATE/DELETE * Django support * Flask support * Real, live docs * OMG Tests Like Wow restless-2.1.1/docs/releasenotes/v1.1.0.rst000066400000000000000000000013141311406265400203560ustar00rootroot00000000000000restless v1.1.0 =============== :date: 2014-01-14 This release adds Pyramid support, easier-to-override serialization, more documentation & fixed Flask tests/``is_debug``. Features -------- * Added support for Pyramid (``restless.pyr.PyramidResource``). Thanks to binarydud for the patch! (SHA: 27e343e) * Added the ``Resource.raw_deserialize`` & ``Resource.raw_serialize`` methods to make changing the serialization format more DRY/easier. (SHA: 9d68aa5) * Added more documentation on how to extend Restless. (SHA: 0be1346 & SHA: 730dde1) Bugfixes -------- * Fixed the Flask tests to no longer be skipped. (SHA: 89d2bc7) * Fixed ``FlaskResource.is_debug`` to now do the correct lookup. (SHA: 89d2bc7) restless-2.1.1/docs/releasenotes/v1.2.0.rst000066400000000000000000000011741311406265400203630ustar00rootroot00000000000000restless v1.2.0 =============== :date: 2014-01-15 **BACKWARD-INCOMPATIBLE:** This release alters the Pyramid ``add_views`` method signature slightly, to be more idiomatic. It changed from ``endpoint_prefix`` to become ``routename_prefix``. Given that the Pyramid support was first released yesterday & this is an optional argument, the hope is the impact of this change is low. This should be (barring any security fixes) the only backward-incompatible change before v2.0.0. Bugfixes -------- * Altered the ``PyramidResource.add_views`` method signature, renaming the ``endpoint_prefix`` to ``routename_prefix``. (SHA: 5a7edc8) restless-2.1.1/docs/releasenotes/v1.3.0.rst000066400000000000000000000003241311406265400203600ustar00rootroot00000000000000restless v1.3.0 =============== :date: 2014-01-29 This release adds support for Itty! This only works under Python 2.X for now, due to itty itself. Features -------- * Added support for Itty. (SHA: 5cc4acd) restless-2.1.1/docs/releasenotes/v1.4.0.rst000066400000000000000000000011061311406265400203600ustar00rootroot00000000000000restless v1.4.0 =============== :date: 2014-02-20 This release improves the way errors are handled (serialized tracebacks in debug), making them more consistent. It also improves Django's support for ``ObjectDoesNotExist/Http404`` & switched to using ``py.test`` for testing. Features -------- * Better not-found behavior in Django. (SHA: 7cd2cfc) * Improved ``Http404`` behavior in Django. (SHA: 44b2e5f) * Switched to ``py.test``. (SHA: 30534a7) * Better error handling support. (SHA: ae5a9cb) Bugfixes -------- * Skips Itty's tests if it is not available. (SHA: b4e859b) restless-2.1.1/docs/releasenotes/v2.0.0.rst000066400000000000000000000052411311406265400203610ustar00rootroot00000000000000restless v2.0.0 =============== :date: 2014-05-23 This release improves the way data preparation & serialization are handled. It introduces these as separate, composable objects (``Preparer`` & ``Serializer``) that are assigned onto a ``Resource``. Porting from 1.X.X to 2.0.0 --------------------------- Porting is relatively straightforward in both the preparation & serialization cases. Preparation ~~~~~~~~~~~ If you were supplying ``fields`` on your ``Resource``, such as:: # posts/api.py from restless.dj import DjangoResource from posts.models import Post class PostResource(DjangoResource): fields = { 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', } Porting is simply 1.adding an import & 2. changing the assignment.:: # posts/api.py from restless.dj import DjangoResource # 1. ADDED IMPORT from restless.preparers import FieldsPreparer from posts.models import Post class PostResource(DjangoResource): # 2. CHANGED ASSIGNMENT preparer = FieldsPreparer{ 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', } Serialization ~~~~~~~~~~~~~ Serialization is even easier. If you performed no overridding, there's nothing to update. You simply get the new ``JSONSerializer`` object automatically. If you were overriding either ``raw_deserialize`` or ``raw_serialize``, you should create a new ``Serializer`` subclass & move the methods over to it, changing their signatures as you go. Then assign an instance of your new ``Serializer`` subclass onto your ``Resource``(s). Unported YAML serialization:: import yaml from restless import Resource class MyResource(Resource): def raw_deserialize(self, body): return yaml.safe_load(body) def raw_serialize(self, data): return yaml.dump(data) Ported serialization:: import yaml from restless import Resource from restless.serializers import Serializer class YAMLSerializer(Serializer): def deserialize(self, body): return yaml.safe_load(body) def serialize(self, data): return yaml.dump(data) class MyResource(Resource): serializer = YAMLSerializer() Features -------- * Added syntax highlighting to docs. (SHA: d398fdb) * Added a ``BAD_REQUEST`` constant & associated ``BadRequest`` error. (SHA: 93d73d6, SHA: 8d49b51 & SHA: a719c88) * Moved to composition for data preparation & serialization. (SHA: 38aabb9) restless-2.1.1/docs/releasenotes/v2.0.1.rst000066400000000000000000000012561311406265400203640ustar00rootroot00000000000000restless v2.0.1 =============== :date: 2014-08-20 This release has many bugfixes & introduces support for Tornado. Features -------- * Tornado support. (SHA: 2f8abcb) * Enabled testing for Python 3.4. (SHA: 67cd126) * Added a ``endpoint`` variable onto ``Resource``. (SHA: da162dd) * Added coveralls for coverage checking. (SHA: ec42c8b) Bugfixes -------- * Updated the tutorial around creating data. (SHA: 542914f) * Removed an erroneous underscore in the Flask docs. (SHA: 691b388) * Fixed ``JSONSerializer`` determining if ``str`` or ``bytes``. (SHA: 5376ac2) * Corrected an example in the "Extending" docs. (SHA: b39bca5) * Fixed docs in the validation docs. (SHA: 691b388) restless-2.1.1/docs/releasenotes/v2.0.2.rst000066400000000000000000000025041311406265400203620ustar00rootroot00000000000000restless v2.0.2 =============== :date: 2016-11-14 This release makes some long-needed changes on error handling for ``Resource`` and its subclasses, plus support for both Django >= 1.9 and Tornado >= 4.0 and allowing alphanumeric PKs on all supported frameworks. Features -------- * Allowed PKs with dashes and alphanumeric digits. (SHA: e52333b) * Reworked test suite so that it uses ``tox`` for simultaneously testing on CPython and PyPy, both 2.x and 3.x (SHA: 2035e21, SHA: 9ca0e8c, SHA: 3915980 & SHA: a1d2d96) * Reworked ``Resource`` so that it throws a ``NotImplementedError`` instead of returning an ``HttpResponse`` from Django. (SHA: 27859c8) * Added several ``HttpError`` subclasses. (SHA: e2aff93) * Changed ``Resource`` so that it allows any serializable object on the response body. (SHA: 1e3522b & SHA: b70a492) Bugfixes -------- * Changed ``JSONSerializer`` to throw a ``BadRequest`` upon a serialization error. (SHA: 8471463) * Updated ``DjangoResource`` to use lists instead of the deprecated ``django.conf.urls.patterns`` object. (SHA: f166e4d & SHA: f94c500) * Fixed ``FieldsPreparer`` behavior when parsing objects with a custom ``__getattr__``. (SHA: 665ef31) * Applied Debian's fix to Tornado tests for version 4.0.0 onwards. (SHA: 372e00a) * Skips tests for all unavailable frameworks. (SHA: 8b81b17) restless-2.1.1/docs/releasenotes/v2.0.3.rst000066400000000000000000000005171311406265400203650ustar00rootroot00000000000000restless v2.0.3 =============== :date: 2016-11-21 This release adds a change which was in restless v2.0.2 but got lost in the backporting process - sorry, everybody! Features -------- * Changed all ``Resource`` subclasses so that a 204 No Content response sends ``text/plain`` on ``Content-Type``. (SHA: 116da9f & SHA: b10be61) restless-2.1.1/docs/releasenotes/v2.1.0.rst000066400000000000000000000006501311406265400203610ustar00rootroot00000000000000restless v2.1.0 =============== :date: 2017-06-01 Features -------- * Added ``SubPreparer`` and ``CollectionSubPreparer`` classes to make easier to nest responses * Hability of using callables in preparers (as soon as they don't have args) Changes ------- * Dropped Itty support :( * Proper HTTP status messages * Added support to Django 1.9 to 1.11 (dropped support to Django <= 1.7) * Proper wrapping for decorators restless-2.1.1/docs/releasenotes/v2.1.1.rst000066400000000000000000000002111311406265400203530ustar00rootroot00000000000000restless v2.1.1 =============== :date: 2017-06-01 Bug Fixes ------- * Fixed an issue caused by trying to import six on setup.py file restless-2.1.1/docs/security.rst000066400000000000000000000024171311406265400167160ustar00rootroot00000000000000.. _security: ======== Security ======== Restless takes security seriously. By default, it: * does not access your filesystem in any way. * only allows GET requests, demanding that the user think about who should be able to work with a given endpoint. * has ``is_debug`` as ``False`` by default. * wraps JSON lists in an object to prevent exploits. While no known vulnerabilities exist, all software has bugs & Restless is no exception. If you believe you have found a security-related issue, please **DO NOT SUBMIT AN ISSUE/PULL REQUEST**. This would be a public disclosure & would allow for 0-day exploits. Instead, please send an email to "daniel@toastdriven.com" & include the following information: * A description of the problem/suggestion. * How to recreate the bug. * If relevant, including the versions of your: * Python interpreter * Web framework * Restless * Optionally of the other dependencies involved Please bear in mind that I'm not a security expert/researcher, so a layman's description of the issue is very important. Upon reproduction of the exploit, steps will be taken to fix the issue, release a new version & make users aware of the need to upgrade. Proper credit for the discovery of the issue will be granted via the AUTHORS file & other mentions. restless-2.1.1/docs/tutorial.rst000066400000000000000000000531331311406265400167130ustar00rootroot00000000000000.. _tutorial: ================= Restless Tutorial ================= Restless is an alternative take on REST frameworks. While other frameworks attempt to be very complete, include special features or tie deeply to ORMs, Restless is a trip back to the basics. It is fast, lightweight, and works with a small (but growing) number of different web frameworks. If you're interested in more of the backstory & reasoning behind Restless, please have a gander at the :ref:`philosophy` documentation. You can find some complete example implementation code in `the repository`_. .. _`the repository`: https://github.com/toastdriven/restless/tree/master/examples Why Restless? ============= Restless tries to be RESTful by default, but flexible enough. The main ``Resource`` class has data methods (that you implement) for all the main RESTful actions. It also uses HTTP status codes as correctly as possible. Restless is BYOD (bring your own data) and hence, works with almost any ORM/data source. If you can import a module to work with the data & can represent it as JSON, Restless can work with it. Restless is small & easy to keep in your head. Common usages involve overridding just a few easily remembered method names. Total source code is a under a thousand lines of code. Restless supports Python 3 **first**, but has backward-compatibility to work with Python 2.6+ code. Because the future is here. Restless is JSON-only by default. Most everything can speak JSON, it's a *data* format (not a *document* format) & it's pleasant for both computers and humans to work with. Restless is well-tested. Installation ============ Installation is a relatively simple affair. For the most recent stable release, simply use pip_ to run:: $ pip install restless Alternately, you can download the latest development source from Github:: $ git clone https://github.com/toastdriven/restless.git $ cd restless $ python setup.py install .. _pip: http://pip-installer.org/ Getting Started =============== Restless currently supports Django_, Flask_, Pyramid_ & Tornado_. For the purposes of most of this tutorial, we'll assume you're using Django. The process for developing & interacting with the API via Flask is nearly identical (& we'll be covering the differences at the end of this document). There are only two steps to getting a Restless API up & functional. They are: #. Implement a ``restless.Resource`` subclass #. Hook up the resource to your URLs Before beginning, you should be familiar with the common understanding of the behavior of the various `REST methods`_. .. _Django: http://djangoproject.com/ .. _Flask: http://flask.pocoo.org/ .. _Pyramid: http://www.pylonsproject.org/ .. _`REST methods`: http://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_web_services About Resources =============== The main class in Restless is :py:class:`restless.resources.Resource`. It provides all the dispatching/authentication/deserialization/serialization/response stuff so you don't have to. Instead, you define/implement a handful of methods that strictly do data access/modification. Those methods are: * ``Resource.list`` - *GET /* * ``Resource.detail`` - *GET /identifier/* * ``Resource.create`` - *POST /* * ``Resource.update`` - *PUT /identifier/* * ``Resource.delete`` - *DELETE /identifier/* Restless also supports some less common combinations (due to their more complex & use-specific natures): * ``Resource.create_detail`` - *POST /identifier/* * ``Resource.update_list`` - *PUT /* * ``Resource.delete_list`` - *DELETE /* Restless includes modules for various web frameworks. To create a resource to work with Django, you'll need to subclass from :py:class:`restless.dj.DjangoResource`. To create a resource to work with Flask, you'll need to subclass from :py:class:`restless.fl.FlaskResource`. .. note: The module names ``restless.dj`` & ``restless.fl`` are used (in place of ``restless.django`` & ``restless.flask``) to prevent import confusion. ``DjangoResource`` is itself a subclass, inheriting from ``restless.resource.Resource`` & overrides a small number of methods to make things work smoothly. The Existing Setup ================== We'll assume we're creating an API for our super-awesome blog platform. We have a ``posts`` application, which has a model setup like so...:: # posts/models.py from django.contrib.auth.models import User from django.db import models class Post(models.Model): user = models.ForeignKey(User, related_name='posts') title = models.CharField(max_length=128) slug = models.SlugField(blank=True) content = models.TextField(default='', blank=True) posted_on = models.DateTimeField(auto_now_add=True) updated_on = models.DateTimeField(auto_now=True) class Meta(object): ordering = ['-posted_on', 'title'] def __str__(self): return self.title This is just enough to get the ORM going & use some real data. The rest of the app (views, URLs, admin, forms, etc.) really aren't important for the purposes of creating a basic Restless API, so we'll ignore them for now. Creating A Resource =================== We'll start with defining the resource subclass. Where you put this code isn't particularly important (as long as other things can import the class you define), but a great convention is ``/api.py``. So in case of our tutorial app, we'll place this code in a new ``posts/api.py`` file. We'll start with the most basic functional example.:: # posts/api.py from restless.dj import DjangoResource from restless.preparers import FieldsPreparer from posts.models import Post class PostResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', }) # GET /api/posts/ (but not hooked up yet) def list(self): return Post.objects.all() # GET /api/posts// (but not hooked up yet) def detail(self, pk): return Post.objects.get(id=pk) As we've already covered, we're inheriting from ``restless.dj.DjangoResource``. We're also importing our ``Post`` model, because serving data out of an API is kinda important. The name of the class isn't particularly important, but it should be descriptive (and can play a role in hooking up URLs later on). Fields ------ We define a ``fields`` attribute on the class. This dictionary provides a mapping between what the API will return & where the data is. This allows you to mask/rename fields, prevent some fields from being exposed or lookup information buried deep in the data model. The mapping is defined like...:: FieldsPreparer(fields={ 'the_fieldname_exposed_to_the_user': 'a_dotted_path_to_the_data', }) This dotted path is what allows use to drill in. For instance, the ``author`` field above has a path of ``user.username``. When serializing, this will cause Restless to look for an attribute (or a key on a dict) called ``user``. From there, it will look further into the resulting object/dict looking for a ``username`` attribute/key, returning it as the final value. Methods ------- We're also defining a ``list`` method & a ``detail`` method. Both can take optional postitional/keyword parameters if necessary. These methods serve the **data** to present for their given endpoints. You don't need to manually construct any responses/status codes/etc., just provide what data should be present. The ``list`` method serves the ``GET`` method on the collection. It should return a ``list`` (or similar iterable, like ``QuerySet``) of data. In this case, we're simply returning all of our ``Post`` model instances. The ``detail`` method also serves the ``GET`` method, but this time for single objects **ONLY**. Providing a ``pk`` in the URL allows this method to lookup the data that should be served. .. note: Restless has this pattern of pairs of methods for each of the RESTful HTTP verbs, list variant & detail variant. ``create/create_detail`` handle ``POST``. ``update_list/update`` handle ``PUT``. And ``delete_list/delete`` handle ``DELETE``. Hooking Up The URLs =================== URLs to Restless resources get hooked up much like any other class-based view. However, Restless's :py:class:`restless.dj.DjangoResource` comes with a special method called ``urls``, which makes hooking up URLs more convenient. You can hook the URLs for the resource up wherever you want. The recommended practice would be to create a URLconf just for the API portion of your site.:: # The ``settings.ROOT_URLCONF`` file # myproject/urls.py from django.conf.urls import url, include # Add this! from posts.api import PostResource urlpatterns = [ # The usual fare, then... # Add this! url(r'api/posts/', include(PostResource.urls())), ] Note that unlike some other CBVs (admin specifically), the ``urls`` here is a **METHOD**, not an attribute/property. Those parens are important! Manual URLconfs --------------- You can also manually hook up URLs by specifying something like:: urlpatterns = [ # ... # Identical to the above. url(r'api/posts/$', PostResource.as_list(), name='api_post_list'), url(r'api/posts/(?P\d+)/$', PostResource.as_detail(), name='api_post_detail'), ] Testing the API =============== We've done enough to get the API (provided there's data in the DB) going, so let's make some requests! Go to a terminal & run:: $ curl -X GET http://127.0.0.1:8000/api/posts/ You should get something like this back...:: { "objects": [ { "id": 1, "title": "First Post!", "author": "daniel", "body": "This is the very first post on my shiny-new blog platform...", "posted_on": "2014-01-12T15:23:46", }, { # More here... } ] } You can also go to the same URL in a browser (http://127.0.0.1:8000/api/posts/) & you should also get the JSON back. .. note: Consider using browser plugins like JSONView to nicely format the JSON. You can get nice formatting at the command line by either piping to ``curl -X GET http://127.0.0.1:8000/api/posts/ | python -m json.tool``. Alternatively, you can use a tool like httpie_ (``http http://127.0.0.1:8000/api/posts/``). .. _httpie: https://pypi.python.org/pypi/httpie You can then load up an individual object by changing the URL to include a ``pk``.:: $ curl -X GET http://127.0.0.1:8000/api/posts/1/ You should get back...:: { "id": 1, "title": "First Post!", "author": "daniel", "body": "This is the very first post on my shiny-new blog platform...", "posted_on": "2014-01-12T15:23:46", } Note that the ``objects`` wrapper is no longer present & we get back the JSON for just that single object. Hooray! Creating/Updating/Deleting Data =============================== A read-only API is nice & all, but sometimes you want to be able to create data as well. So we'll implement some more methods.:: # posts/api.py from restless.dj import DjangoResource from restless.preparers import FieldsPreparer from posts.models import Post class PostResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', }) # GET /api/posts/ def list(self): return Post.objects.all() # GET /api/posts// def detail(self, pk): return Post.objects.get(id=pk) # Add this! # POST /api/posts/ def create(self): return Post.objects.create( title=self.data['title'], user=User.objects.get(username=self.data['author']), content=self.data['body'] ) # Add this! # PUT /api/posts// def update(self, pk): try: post = Post.objects.get(id=pk) except Post.DoesNotExist: post = Post() post.title = self.data['title'] post.user = User.objects.get(username=self.data['author']) post.content = self.data['body'] post.save() return post # Add this! # DELETE /api/posts// def delete(self, pk): Post.objects.get(id=pk).delete() By adding the ``create/update/delete`` methods, we now have the ability to add new items, update existing ones & delete individual items. Most of this code is relatively straightforward ORM calls, but there are a few interesting new things going on here. Note that the ``create`` & ``update`` methods are both using a special ``self.data`` variable. This is created by Restless during deserialization & is the **JSON** data the user sends as part of the request. .. warning:: This data (within ``self.data``) is mostly unsanitized (beyond standard JSON decoding) & could contain anything (not just the ``fields`` you define). You know your data best & validation is **very** non-standard between frameworks, so this is a place where Restless punts. Some people like cleaning the data with ``Forms``, others prefer to hand-sanitize, some do model validation, etc. Do what works best for you. You can refer to the :ref:`extending` documentation for recommended approaches. Also note that ``delete`` is the first method with **no return value**. You can do the same thing on ``create/update`` if you like. When there's no meaningful data returned, Restless simply sends back a correct status code & an empty body. Finally, there's no need to hook up more URLconfs. Restless delegates based on a list & a detail endpoint. All further dispatching is HTTP verb-based & handled by Restless. Testing the API, Round 2 ======================== Now let's test out our new functionality! Go to a terminal & run:: $ curl -X POST -H "Content-Type: application/json" -d '{"title": "New library released!", "author": "daniel", "body": "I just released a new thing!"}' http://127.0.0.1:8000/api/posts/ You should get something like this back...:: { "error": "Unauthorized" } Wait, what?!! But we added our new methods & everything! The reason you get unauthorized is that by default, **only GET** requests are allowed by Restless. It's the only sane/safe default to have & it's very easy to fix. Error Handling ============== By default, Restless tries to serialize any exceptions that may be encountered. What gets serialized depends on two methods: ``Resource.is_debug()`` & ``Resource.bubble_exceptions()``. ``is_debug`` ------------ Regardless of the error type, the exception's message will get serialized into the response under the ``"error"`` key. For example, if an ``IOError`` is raised during processing, you'll get a response like:: HTTP/1.0 500 INTERNAL SERVER ERROR Content-Type: application/json # Other headers... { "error": "Whatever." } If ``Resource.is_debug()`` returns ``True`` (the default is ``False``), Restless will also include a traceback. For example:: HTTP/1.0 500 INTERNAL SERVER ERROR Content-Type: application/json # Other headers... { "error": "Whatever.", "traceback": "Traceback (most recent call last):\n # Typical traceback..." } Each framework-specific ``Resource`` subclass implements ``is_debug()`` in a way most appropriate for the framework. In the case of the ``DjangoResource``, it returns ``settings.DEBUG``, allowing your resources to stay consistent with the rest of your application. ``bubble_exceptions`` --------------------- If ``Resource.bubble_exceptions()`` returns ``True`` (the default is ``False``), any exception encountered will simply be re-raised & it's up to your setup to handle it. Typically, this behavior is undesirable except in development & with frameworks that can provide extra information/debugging on exceptions. Feel free to override it (``return True``) or implement application-specific logic if that meets your needs. Authentication ============== We're going to override one more method in our resource subclass, this time adding the ``is_authenticated`` method.:: # posts/api.py from restless.dj import DjangoResource from restless.preparers import FieldsPreparer from posts.models import Post class PostResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', } # Add this! def is_authenticated(self): # Open everything wide! # DANGEROUS, DO NOT DO IN PRODUCTION. return True # Alternatively, if the user is logged into the site... # return self.request.user.is_authenticated() # Alternatively, you could check an API key. (Need a model for this...) # from myapp.models import ApiKey # try: # key = ApiKey.objects.get(key=self.request.GET.get('api_key')) # return True # except ApiKey.DoesNotExist: # return False def list(self): return Post.objects.all() def detail(self, pk): return Post.objects.get(id=pk) def create(self): return Post.objects.create( title=self.data['title'], user=User.objects.get(username=self.data['author']), content=self.data['body'] ) def update(self, pk): try: post = Post.objects.get(id=pk) except Post.DoesNotExist: post = Post() post.title = self.data['title'] post.user = User.objects.get(username=self.data['author']) post.content = self.data['body'] post.save() return post def delete(self, pk): Post.objects.get(id=pk).delete() With that change in place, now our API should play nice... Testing the API, Round 3 ======================== Back to the terminal & again run:: $ curl -X POST -H "Content-Type: application/json" -d '{"title": "New library released!", "author": "daniel", "body": "I just released a new thing!"}' http://127.0.0.1:8000/api/posts/ You should get something like this back...:: { "body": "I just released a new thing!", "title": "New library released!", "id": 3, "posted_on": "2014-01-13T10:02:55.926857+00:00", "author": "daniel" } Hooray! Now we can check for it in the list view (``GET`` http://127.0.0.1:8000/api/posts/) or by a detail (``GET`` http://127.0.0.1:8000/api/posts/3/). We can also update it. Restless expects **complete** bodies (don't try to send partial updates, that's typically reserved for ``PATCH``).:: $ curl -X PUT -H "Content-Type: application/json" -d '{"title": "Another new library released!", "author": "daniel", "body": "I just released a new piece of software!"}' http://127.0.0.1:8000/api/posts/3/ And we can delete our new data if we decide we don't like it.:: $ curl -X DELETE http://127.0.0.1:8000/api/posts/3/ Conclusion ========== We've now got a basic, working RESTful API in a short amount of code! And the possibilities don't stop at the ORM. You could hook up: * Redis * the NoSQL flavor of the month * text/log files * wrap calls to other (nastier) APIs You may also want to check the :ref:`cookbook` for other interesting/useful possibilities & implementation patterns. Bonus: Flask Support ==================== Outside of the ORM, precious little of what we implemented above was Django-specific. If you used an ORM like `Peewee`_ or `SQLAlchemy`_, you'd have very similar-looking code. In actuality, there are just two changes to make the Restless-portion of the above work within Flask. #. Change the inheritance #. Change how the URL routes are hooked up. .. _Peewee: http://peewee.readthedocs.org/en/latest/ .. _SQLAlchemy: http://www.sqlalchemy.org/ Change The Inheritance ---------------------- Restless ships with a :py:class:`restless.fl.FlaskResource` class, akin to the ``DjangoResource``. So the first change is dead simple.:: # Was: from restless.dj import DjangoResource # Becomes: from restless.fl import FlaskResource # ... # Was: class PostResource(DjangoResource): # Becomes: class PostResource(FlaskResource): # ... Change How The URL Routes Are Hooked Up --------------------------------------- Again, similar to the ``DjangoResource``, the ``FlaskResource`` comes with a special method to make hooking up the routes easier. Wherever your ``PostResource`` is defined, import your Flask ``app``, then call the following:: PostResource.add_url_rules(app, rule_prefix='/api/posts/') This will hook up the same two endpoints (list & detail, just like Django above) within the Flask app, doing similar dispatches. You can also do this manually (but it's ugly/hurts).:: app.add_url_rule('/api/posts/', endpoint='api_posts_list', view_func=PostResource.as_list(), methods=['GET', 'POST', 'PUT', 'DELETE']) app.add_url_rule('/api/posts//', endpoint='api_posts_detail', view_func=PostResource.as_detail(), methods=['GET', 'POST', 'PUT', 'DELETE']) Done! ----- Believe it or not, if your ORM could be made to look similar, that's all the further changes needed to get the same API (with the same end-user interactions) working on Flask! Huzzah! restless-2.1.1/examples/000077500000000000000000000000001311406265400151775ustar00rootroot00000000000000restless-2.1.1/examples/django/000077500000000000000000000000001311406265400164415ustar00rootroot00000000000000restless-2.1.1/examples/django/posts/000077500000000000000000000000001311406265400176115ustar00rootroot00000000000000restless-2.1.1/examples/django/posts/__init__.py000066400000000000000000000000001311406265400217100ustar00rootroot00000000000000restless-2.1.1/examples/django/posts/api.py000066400000000000000000000021361311406265400207360ustar00rootroot00000000000000from django.contrib.auth.models import User from restless.dj import DjangoResource from restless.preparers import FieldsPreparer from posts.models import Post class PostResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'user.username', 'body': 'content', 'posted_on': 'posted_on', }) def list(self): return Post.objects.all() def detail(self, pk): return Post.objects.get(id=pk) def create(self): return Post.objects.create( title=self.data['title'], user=User.objects.get(username=self.data['author']), content=self.data['body'] ) def update(self, pk): try: post = Post.objects.get(id=pk) except Post.DoesNotExist: post = Post() post.title = self.data['title'] post.user = User.objects.get(username=self.data['author']) post.content = self.data['body'] post.save() return post def delete(self, pk): Post.objects.get(id=pk).delete() restless-2.1.1/examples/django/posts/models.py000066400000000000000000000010151311406265400214430ustar00rootroot00000000000000from django.contrib.auth.models import User from django.db import models class Post(models.Model): user = models.ForeignKey(User, related_name='posts') title = models.CharField(max_length=128) slug = models.SlugField(blank=True) content = models.TextField(default='', blank=True) posted_on = models.DateTimeField(auto_now_add=True) updated_on = models.DateTimeField(auto_now=True) class Meta(object): ordering = ['-posted_on', 'title'] def __str__(self): return self.title restless-2.1.1/examples/django/posts/urls.py000066400000000000000000000005461311406265400211550ustar00rootroot00000000000000from django.conf.urls import url, include from .api import PostResource urlpatterns = [ url(r'^posts/', include(PostResource.urls())), # Alternatively, if you don't like the defaults... # url(r'^posts/$', PostResource.as_list(), name='api_posts_list'), # url(r'^posts/(?P\d+)/$', PostResource.as_detail(), name='api_posts_detail'), ] restless-2.1.1/examples/flask/000077500000000000000000000000001311406265400162775ustar00rootroot00000000000000restless-2.1.1/examples/flask/app.py000066400000000000000000000031361311406265400174340ustar00rootroot00000000000000from flask import Flask import redis from restless.fl import FlaskResource import time app = Flask(__name__) class UserResource(FlaskResource): def __init__(self, *args, **kwargs): super(UserResource, self).__init__(*args, **kwargs) self.conn = redis.StrictRedis(host='localhost', port=6379, db=0) def is_authenticated(self): return True def make_user_key(self, username): return 'user_{0}'.format(username) def list(self): usernames = self.conn.lrange('users', 0, 100) users = [] for user in usernames: users.append(self.conn.hgetall(self.make_user_key(user))) return users def detail(self, username): return self.conn.hgetall(self.make_user_key(username)) def create(self): key = self.make_user_key(self.data['username']) self.conn.hmset( key, { 'username': self.data['username'], 'email': self.data['email'], 'added_on': int(time.time()), } ) self.conn.rpush('users', self.data['username']) return self.conn.hgetall(key) UserResource.add_url_rules(app, rule_prefix='/api/users/') # Alternatively, if you don't like the defaults... # app.add_url_rule('/api/users/', endpoint='api_users_list', view_func=UserResource.as_list(), methods=['GET', 'POST', 'PUT', 'DELETE']) # app.add_url_rule('/api/users//', endpoint='api_users_detail', view_func=UserResource.as_detail(), methods=['GET', 'POST', 'PUT', 'DELETE']) if __name__ == '__main__': app.debug = True app.run() restless-2.1.1/examples/pyramid/000077500000000000000000000000001311406265400166445ustar00rootroot00000000000000restless-2.1.1/examples/pyramid/app.py000066400000000000000000000026241311406265400200020ustar00rootroot00000000000000import time import redis from wsgiref.simple_server import make_server from pyramid.config import Configurator from restless.pyr import PyramidResource class UserResource(PyramidResource): def __init__(self, *args, **kwargs): super(UserResource, self).__init__(*args, **kwargs) self.conn = redis.StrictRedis(host='localhost', port=6379, db=0) def is_authenticated(self): return True def make_user_key(self, username): return 'user_{0}'.format(username) def list(self): usernames = self.conn.lrange('users', 0, 100) users = [] for user in usernames: users.append(self.conn.hgetall(self.make_user_key(user))) return users def detail(self, username): return self.conn.hgetall(self.make_user_key(username)) def create(self): key = self.make_user_key(self.data['username']) self.conn.hmset( key, { 'username': self.data['username'], 'email': self.data['email'], 'added_on': int(time.time()), } ) self.conn.rpush('users', self.data['username']) return self.conn.hgetall(key) if __name__ == '__main__': config = Configurator() config = UserResource.add_views(config, '/users/') app = config.make_wsgi_app() server = make_server('0.0.0.0', 8080, app) server.serve_forever() restless-2.1.1/examples/tornado/000077500000000000000000000000001311406265400166455ustar00rootroot00000000000000restless-2.1.1/examples/tornado/app.py000066400000000000000000000013421311406265400177770ustar00rootroot00000000000000import tornado.ioloop from tornado import web, gen from restless.tnd import TornadoResource class PetResource(TornadoResource): def prepare(self): self.fake_db = { { "id": 1, "name": "Mitti", "type": "dog" }, { "id": 2, "name": "Gary", "type": "cat" } } @gen.coroutine def list(self): raise gen.Return(self.fake_db) routes = [ (r'/pets', PetResource.as_list()), (r'/pets/([^/]+)', PetResource.as_detail()) ] app = web.Application(routes, debug=True) if __name__ == '__main__': app.listen(8001) tornado.ioloop.IOLoop.instance().start() restless-2.1.1/requirements.txt000066400000000000000000000000131311406265400166370ustar00rootroot00000000000000six>=1.4.0 restless-2.1.1/restless/000077500000000000000000000000001311406265400152255ustar00rootroot00000000000000restless-2.1.1/restless/__init__.py000066400000000000000000000002271311406265400173370ustar00rootroot00000000000000__author__ = 'Daniel Lindsley' __license__ = 'BSD' __version__ = (2, 1, 1) VERSION = '.'.join(map(str, __version__)) from .resources import Resource restless-2.1.1/restless/constants.py000066400000000000000000000010011311406265400176030ustar00rootroot00000000000000# HTTP Status Codes OK = 200 CREATED = 201 ACCEPTED = 202 NO_CONTENT = 204 BAD_REQUEST = 400 UNAUTHORIZED = 401 FORBIDDEN = 403 NOT_FOUND = 404 METHOD_NOT_ALLOWED = 405 NOT_ACCEPTABLE = 406 CONFLICT = 409 GONE = 410 PRECONDITION_FAILED = 412 UNSUPPORTED_MEDIA_TYPE = 415 EXPECTATION_FAILED = 417 I_AM_A_TEAPOT = 418 UNPROCESSABLE_ENTITY = 422 LOCKED = 423 FAILED_DEPENDENCY = 424 TOO_MANY_REQUESTS = 429 UNAVAILABLE_FOR_LEGAL_REASONS = 451 APPLICATION_ERROR = 500 METHOD_NOT_IMPLEMENTED = 501 UNAVAILABLE = 503 restless-2.1.1/restless/data.py000066400000000000000000000013621311406265400165120ustar00rootroot00000000000000class Data(object): def __init__(self, value, should_prepare=True, prepare_with=None): """ A container object that carries meta information about the data. ``value`` should be the data to be returned to the client. This may be post-processed. ``should_prepare`` determines whether additional post-processing should occur & should be boolean. This is useful when returning objects or with complex requirements. Default is ``True``. ``prepare_with`` is reserved for future use in specifying a custom callable. Default is ``None`` (no custom callable). """ self.value = value self.should_prepare = should_prepare self.prepare_with = prepare_with restless-2.1.1/restless/dj.py000066400000000000000000000061301311406265400161740ustar00rootroot00000000000000import six from django.conf import settings from django.conf.urls import url from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponse, Http404 from django.views.decorators.csrf import csrf_exempt from .constants import OK, NO_CONTENT from .exceptions import NotFound from .resources import Resource class DjangoResource(Resource): """ A Django-specific ``Resource`` subclass. Doesn't require any special configuration, but helps when working in a Django environment. """ # Because Django. @classmethod def as_list(self, *args, **kwargs): return csrf_exempt(super(DjangoResource, self).as_list(*args, **kwargs)) @classmethod def as_detail(self, *args, **kwargs): return csrf_exempt(super(DjangoResource, self).as_detail(*args, **kwargs)) def is_debug(self): return settings.DEBUG def build_response(self, data, status=OK): if status == NO_CONTENT: # Avoid crashing the client when it tries to parse nonexisting JSON. content_type = 'text/plain' else: content_type = 'application/json' resp = HttpResponse(data, content_type=content_type, status=status) return resp def build_error(self, err): # A bit nicer behavior surrounding things that don't exist. if isinstance(err, (ObjectDoesNotExist, Http404)): err = NotFound(msg=six.text_type(err)) return super(DjangoResource, self).build_error(err) @classmethod def build_url_name(cls, name, name_prefix=None): """ Given a ``name`` & an optional ``name_prefix``, this generates a name for a URL. :param name: The name for the URL (ex. 'detail') :type name: string :param name_prefix: (Optional) A prefix for the URL's name (for resolving). The default is ``None``, which will autocreate a prefix based on the class name. Ex: ``BlogPostResource`` -> ``api_blog_post_list`` :type name_prefix: string :returns: The final name :rtype: string """ if name_prefix is None: name_prefix = 'api_{0}'.format( cls.__name__.replace('Resource', '').lower() ) name_prefix = name_prefix.rstrip('_') return '_'.join([name_prefix, name]) @classmethod def urls(cls, name_prefix=None): """ A convenience method for hooking up the URLs. This automatically adds a list & a detail endpoint to your URLconf. :param name_prefix: (Optional) A prefix for the URL's name (for resolving). The default is ``None``, which will autocreate a prefix based on the class name. Ex: ``BlogPostResource`` -> ``api_blogpost_list`` :type name_prefix: string :returns: A list of ``url`` objects for ``include(...)`` """ return [ url(r'^$', cls.as_list(), name=cls.build_url_name('list', name_prefix)), url(r'^(?P[\w-]+)/$', cls.as_detail(), name=cls.build_url_name('detail', name_prefix)), ] restless-2.1.1/restless/exceptions.py000066400000000000000000000067041311406265400177670ustar00rootroot00000000000000from .constants import (APPLICATION_ERROR, UNAUTHORIZED, NOT_FOUND, BAD_REQUEST, FORBIDDEN, NOT_ACCEPTABLE, GONE, PRECONDITION_FAILED, CONFLICT, UNSUPPORTED_MEDIA_TYPE, EXPECTATION_FAILED, I_AM_A_TEAPOT, TOO_MANY_REQUESTS, UNPROCESSABLE_ENTITY, UNAVAILABLE_FOR_LEGAL_REASONS, FAILED_DEPENDENCY, LOCKED) from .constants import METHOD_NOT_ALLOWED, METHOD_NOT_IMPLEMENTED, UNAVAILABLE class RestlessError(Exception): """ A common base exception from which all other exceptions in ``restless`` inherit from. No special attributes or behaviors. """ pass class HttpError(RestlessError): """ The foundational HTTP-related error. All other HTTP errors in ``restless`` inherit from this one. Has a ``status`` attribute. If present, ``restless`` will use this as the ``status_code`` in the response. Has a ``msg`` attribute. Has a reasonable default message (override-able from the constructor). """ status = APPLICATION_ERROR msg = "Application Error" def __init__(self, msg=None): if not msg: msg = self.__class__.msg super(HttpError, self).__init__(msg) class BadRequest(HttpError): status = BAD_REQUEST msg = "Bad request." class Unauthorized(HttpError): status = UNAUTHORIZED msg = "Unauthorized." class Forbidden(HttpError): status = FORBIDDEN msg = "Permission denied." class NotFound(HttpError): status = NOT_FOUND msg = "Resource not found." class MethodNotAllowed(HttpError): status = METHOD_NOT_ALLOWED msg = "The specified HTTP method is not allowed." class NotAcceptable(HttpError): # TODO: make serializers handle it? status = NOT_ACCEPTABLE msg = "Unable to send content specified on the request's Accept header(s)." class Conflict(HttpError): status = CONFLICT msg = "There was a conflict when processing the request." class Gone(HttpError): status = GONE msg = "Resource removed permanently." class PreconditionFailed(HttpError): status = PRECONDITION_FAILED msg = "Unable to satisfy one or more request preconditions." class UnsupportedMediaType(HttpError): status = UNSUPPORTED_MEDIA_TYPE msg = "Type of media provided on request is not supported." class ExpectationFailed(HttpError): status = EXPECTATION_FAILED msg = "Unable to satisfy requirements of Expect header." class IAmATeapot(HttpError): status = I_AM_A_TEAPOT msg = "This is a teapot; do not attempt to brew coffee with it." class UnprocessableEntity(HttpError): status = UNPROCESSABLE_ENTITY msg = "Request cannot be followed due to a semantic error." class Locked(HttpError): status = LOCKED msg = "Resource is locked." class FailedDependency(HttpError): status = FAILED_DEPENDENCY msg = "Request failed due to a previous failed request." class TooManyRequests(HttpError): status = TOO_MANY_REQUESTS msg = "There was a conflict when processing the request." class UnavailableForLegalReasons(HttpError): status = UNAVAILABLE_FOR_LEGAL_REASONS msg = "Resource made unavailable by a legal decision." class MethodNotImplemented(HttpError): status = METHOD_NOT_IMPLEMENTED msg = "The specified HTTP method is not implemented." class Unavailable(HttpError): status = UNAVAILABLE msg = "There was a conflict when processing the request." restless-2.1.1/restless/fl.py000066400000000000000000000075501311406265400162070ustar00rootroot00000000000000from flask import make_response from flask import request from .constants import OK, NO_CONTENT from .resources import Resource class FlaskResource(Resource): """ A Flask-specific ``Resource`` subclass. Doesn't require any special configuration, but helps when working in a Flask environment. """ @classmethod def as_list(cls, *init_args, **init_kwargs): # Overridden here, because Flask uses a global ``request`` object # rather than passing it to each view. def _wrapper(*args, **kwargs): # Make a new instance so that no state potentially leaks between # instances. inst = cls(*init_args, **init_kwargs) inst.request = request return inst.handle('list', *args, **kwargs) return _wrapper @classmethod def as_detail(cls, *init_args, **init_kwargs): # Overridden here, because Flask uses a global ``request`` object # rather than passing it to each view. def _wrapper(*args, **kwargs): # Make a new instance so that no state potentially leaks between # instances. inst = cls(*init_args, **init_kwargs) inst.request = request return inst.handle('detail', *args, **kwargs) return _wrapper def request_body(self): return self.request.data def is_debug(self): from flask import current_app return current_app.debug def build_response(self, data, status=OK): if status == NO_CONTENT: # Avoid crashing the client when it tries to parse nonexisting JSON. content_type = 'text/plain' else: content_type = 'application/json' return make_response(data, status, { 'Content-Type': content_type, }) @classmethod def build_endpoint_name(cls, name, endpoint_prefix=None): """ Given a ``name`` & an optional ``endpoint_prefix``, this generates a name for a URL. :param name: The name for the URL (ex. 'detail') :type name: string :param endpoint_prefix: (Optional) A prefix for the URL's name (for resolving). The default is ``None``, which will autocreate a prefix based on the class name. Ex: ``BlogPostResource`` -> ``api_blogpost_list`` :type endpoint_prefix: string :returns: The final name :rtype: string """ if endpoint_prefix is None: endpoint_prefix = 'api_{0}'.format( cls.__name__.replace('Resource', '').lower() ) endpoint_prefix = endpoint_prefix.rstrip('_') return '_'.join([endpoint_prefix, name]) @classmethod def add_url_rules(cls, app, rule_prefix, endpoint_prefix=None): """ A convenience method for hooking up the URLs. This automatically adds a list & a detail endpoint to your routes. :param app: The ``Flask`` object for your app. :type app: ``flask.Flask`` :param rule_prefix: The start of the URL to handle. :type rule_prefix: string :param endpoint_prefix: (Optional) A prefix for the URL's name (for endpoints). The default is ``None``, which will autocreate a prefix based on the class name. Ex: ``BlogPostResource`` -> ``api_blog_post_list`` :type endpoint_prefix: string :returns: Nothing """ methods = ['GET', 'POST', 'PUT', 'DELETE'] app.add_url_rule( rule_prefix, endpoint=cls.build_endpoint_name('list', endpoint_prefix), view_func=cls.as_list(), methods=methods ) app.add_url_rule( rule_prefix + '/', endpoint=cls.build_endpoint_name('detail', endpoint_prefix), view_func=cls.as_detail(), methods=methods ) restless-2.1.1/restless/preparers.py000066400000000000000000000152401311406265400176040ustar00rootroot00000000000000class Preparer(object): """ A plain preparation object which just passes through data. It also is relevant as the protocol subclasses should implement to work with Restless. """ def __init__(self): super(Preparer, self).__init__() def prepare(self, data): """ Handles actually transforming the data. By default, this does nothing & simply returns the data passed to it. """ return data class FieldsPreparer(Preparer): """ A more complex preparation object, this will return a given set of fields. This takes a ``fields`` parameter, which should be a dictionary of keys (fieldnames to expose to the user) & values (a dotted lookup path to the desired attribute/key on the object). Example:: preparer = FieldsPreparer(fields={ # ``user`` is the key the client will see. # ``author.pk`` is the dotted path lookup ``FieldsPreparer`` # will traverse on the data to return a value. 'user': 'author.pk', }) """ def __init__(self, fields): super(FieldsPreparer, self).__init__() self.fields = fields def prepare(self, data): """ Handles transforming the provided data into the fielded data that should be exposed to the end user. Uses the ``lookup_data`` method to traverse dotted paths. Returns a dictionary of data as the response. """ result = {} if not self.fields: # No fields specified. Serialize everything. return data for fieldname, lookup in self.fields.items(): if isinstance(lookup, SubPreparer): result[fieldname] = lookup.prepare(data) else: result[fieldname] = self.lookup_data(lookup, data) return result def lookup_data(self, lookup, data): """ Given a lookup string, attempts to descend through nested data looking for the value. Can work with either dictionary-alikes or objects (or any combination of those). Lookups should be a string. If it is a dotted path, it will be split on ``.`` & it will traverse through to find the final value. If not, it will simply attempt to find either a key or attribute of that name & return it. Example:: >>> data = { ... 'type': 'message', ... 'greeting': { ... 'en': 'hello', ... 'fr': 'bonjour', ... 'es': 'hola', ... }, ... 'person': Person( ... name='daniel' ... ) ... } >>> lookup_data('type', data) 'message' >>> lookup_data('greeting.en', data) 'hello' >>> lookup_data('person.name', data) 'daniel' """ value = data parts = lookup.split('.') if not parts or not parts[0]: return value part = parts[0] remaining_lookup = '.'.join(parts[1:]) if callable(getattr(data, 'keys', None)) and hasattr(data, '__getitem__'): # Dictionary enough for us. value = data[part] elif data is not None: # Assume it's an object. value = getattr(data, part) # Call if it's callable except if it's a Django DB manager instance # We check if is a manager by checking the db_manager (duck typing) if callable(value) and not hasattr(value, 'db_manager'): value = value() if not remaining_lookup: return value # There's more to lookup, so dive in recursively. return self.lookup_data(remaining_lookup, value) class SubPreparer(FieldsPreparer): """ A preparation class designed to be used within other preparers. This is primary to enable deeply-nested structures, allowing you to compose/share definitions as well. Typical usage consists of creating a configured instance of a FieldsPreparer, then use a `SubPreparer` to pull it in. Example:: # First, define the nested fields you'd like to expose. author_preparer = FieldsPreparer(fields={ 'id': 'pk', 'username': 'username', 'name': 'get_full_name', }) # Then, in the main preparer, pull them in using `SubPreparer`. preparer = FieldsPreparer(fields={ 'author': SubPreparer('user', author_preparer), # Other fields can come before/follow as normal. 'content': 'post', 'created': 'created_at', }) """ def __init__(self, lookup, preparer): self.lookup = lookup self.preparer = preparer def get_inner_data(self, data): """ Used internally so that the correct data is extracted out of the broader dataset, allowing the preparer being called to deal with just the expected subset. """ return self.lookup_data(self.lookup, data) def prepare(self, data): """ Handles passing the data to the configured preparer. Uses the ``get_inner_data`` method to provide the correct subset of the data. Returns a dictionary of data as the response. """ return self.preparer.prepare(self.get_inner_data(data)) class CollectionSubPreparer(SubPreparer): """ A preparation class designed to handle collections of data. This is useful in the case where you have a 1-to-many or many-to-many relationship of data to expose as part of the parent data. Example:: # First, set up a preparer that handles the data for each thing in # the broader collection. comment_preparer = FieldsPreparer(fields={ 'comment': 'comment_text', 'created': 'created', }) # Then use it with the ``CollectionSubPreparer`` to create a list # of prepared sub items. preparer = FieldsPreparer(fields={ # A normal blog post field. 'post': 'post_text', # All the comments on the post. 'comments': CollectionSubPreparer('comments.all', comment_preparer), }) """ def prepare(self, data): """ Handles passing each item in the collection data to the configured subpreparer. Uses a loop and the ``get_inner_data`` method to provide the correct item of the data. Returns a list of data as the response. """ result = [] for item in self.get_inner_data(data): result.append(self.preparer.prepare(item)) return result restless-2.1.1/restless/pyr.py000066400000000000000000000067621311406265400164240ustar00rootroot00000000000000from pyramid.response import Response from .constants import OK, NO_CONTENT from .resources import Resource class PyramidResource(Resource): """ A Pyramid-specific ``Resource`` subclass. Doesn't require any special configuration, but helps when working in a Pyramid environment. """ @classmethod def as_list(cls, *args, **kwargs): return super(PyramidResource, cls).as_list(*args, **kwargs) @classmethod def as_detail(cls, *init_args, **init_kwargs): def _wrapper(request): # Make a new instance so that no state potentially leaks between # instances. inst = cls(*init_args, **init_kwargs) inst.request = request name = request.matchdict['name'] return inst.handle('detail', name) return _wrapper def build_response(self, data, status=OK): if status == NO_CONTENT: # Avoid crashing the client when it tries to parse nonexisting JSON. content_type = 'text/plain' else: content_type = 'application/json' resp = Response(data, status_code=status, content_type=content_type) return resp @classmethod def build_routename(cls, name, routename_prefix=None): """ Given a ``name`` & an optional ``routename_prefix``, this generates a name for a URL. :param name: The name for the URL (ex. 'detail') :type name: string :param routename_prefix: (Optional) A prefix for the URL's name (for resolving). The default is ``None``, which will autocreate a prefix based on the class name. Ex: ``BlogPostResource`` -> ``api_blogpost_list`` :type routename_prefix: string :returns: The final name :rtype: string """ if routename_prefix is None: routename_prefix = 'api_{0}'.format( cls.__name__.replace('Resource', '').lower() ) routename_prefix = routename_prefix.rstrip('_') return '_'.join([routename_prefix, name]) @classmethod def add_views(cls, config, rule_prefix, routename_prefix=None): """ A convenience method for registering the routes and views in pyramid. This automatically adds a list and detail endpoint to your routes. :param config: The pyramid ``Configurator`` object for your app. :type config: ``pyramid.config.Configurator`` :param rule_prefix: The start of the URL to handle. :type rule_prefix: string :param routename_prefix: (Optional) A prefix for the route's name. The default is ``None``, which will autocreate a prefix based on the class name. Ex: ``PostResource`` -> ``api_post_list`` :type routename_prefix: string :returns: ``pyramid.config.Configurator`` """ methods = ('GET', 'POST', 'PUT', 'DELETE') config.add_route( cls.build_routename('list', routename_prefix), rule_prefix ) config.add_view( cls.as_list(), route_name=cls.build_routename('list', routename_prefix), request_method=methods ) config.add_route( cls.build_routename('detail', routename_prefix), rule_prefix + '{name}/' ) config.add_view( cls.as_detail(), route_name=cls.build_routename('detail', routename_prefix), request_method=methods ) return config restless-2.1.1/restless/resources.py000066400000000000000000000445441311406265400176240ustar00rootroot00000000000000from functools import wraps import sys from .constants import OK, CREATED, ACCEPTED, NO_CONTENT from .data import Data from .exceptions import MethodNotImplemented, Unauthorized from .preparers import Preparer from .serializers import JSONSerializer from .utils import format_traceback def skip_prepare(func): """ A convenience decorator for indicating the raw data should not be prepared. """ @wraps(func) def _wrapper(self, *args, **kwargs): value = func(self, *args, **kwargs) return Data(value, should_prepare=False) return _wrapper class Resource(object): """ Defines a RESTful resource. Users are expected to subclass this object & implement a handful of methods: * ``list`` * ``detail`` * ``create`` (requires authentication) * ``update`` (requires authentication) * ``delete`` (requires authentication) Additionally, the user may choose to implement: * ``create_detail`` (requires authentication) * ``update_list`` (requires authentication) * ``delete_list`` (requires authentication) Users may also wish to define a ``fields`` attribute on the class. By providing a dictionary of output names mapped to a dotted lookup path, you can control the serialized output. Users may also choose to override the ``status_map`` and/or ``http_methods`` on the class. These respectively control the HTTP status codes returned by the views and the way views are looked up (based on HTTP method & endpoint). """ status_map = { 'list': OK, 'detail': OK, 'create': CREATED, 'update': ACCEPTED, 'delete': NO_CONTENT, 'update_list': ACCEPTED, 'create_detail': CREATED, 'delete_list': NO_CONTENT, } http_methods = { 'list': { 'GET': 'list', 'POST': 'create', 'PUT': 'update_list', 'DELETE': 'delete_list', }, 'detail': { 'GET': 'detail', 'POST': 'create_detail', 'PUT': 'update', 'DELETE': 'delete', } } preparer = Preparer() serializer = JSONSerializer() def __init__(self, *args, **kwargs): self.init_args = args self.init_kwargs = kwargs self.request = None self.data = None self.endpoint = None self.status = 200 @classmethod def as_list(cls, *init_args, **init_kwargs): """ Used for hooking up the actual list-style endpoints, this returns a wrapper function that creates a new instance of the resource class & calls the correct view method for it. :param init_args: (Optional) Positional params to be persisted along for instantiating the class itself. :param init_kwargs: (Optional) Keyword params to be persisted along for instantiating the class itself. :returns: View function """ return cls.as_view('list', *init_args, **init_kwargs) @classmethod def as_detail(cls, *init_args, **init_kwargs): """ Used for hooking up the actual detail-style endpoints, this returns a wrapper function that creates a new instance of the resource class & calls the correct view method for it. :param init_args: (Optional) Positional params to be persisted along for instantiating the class itself. :param init_kwargs: (Optional) Keyword params to be persisted along for instantiating the class itself. :returns: View function """ return cls.as_view('detail', *init_args, **init_kwargs) @classmethod def as_view(cls, view_type, *init_args, **init_kwargs): """ Used for hooking up the all endpoints (including custom ones), this returns a wrapper function that creates a new instance of the resource class & calls the correct view method for it. :param view_type: Should be one of ``list``, ``detail`` or ``custom``. :type view_type: string :param init_args: (Optional) Positional params to be persisted along for instantiating the class itself. :param init_kwargs: (Optional) Keyword params to be persisted along for instantiating the class itself. :returns: View function """ @wraps(cls) def _wrapper(request, *args, **kwargs): # Make a new instance so that no state potentially leaks between # instances. inst = cls(*init_args, **init_kwargs) inst.request = request return inst.handle(view_type, *args, **kwargs) return _wrapper def request_method(self): """ Returns the HTTP method for the current request. If you're integrating with a new web framework, you might need to override this method within your subclass. :returns: The HTTP method in uppercase :rtype: string """ # By default, Django-esque. return self.request.method.upper() def request_body(self): """ Returns the body of the current request. Useful for deserializing the content the user sent (typically JSON). If you're integrating with a new web framework, you might need to override this method within your subclass. :returns: The body of the request :rtype: string """ # By default, Django-esque. return self.request.body def build_response(self, data, status=200): """ Given some data, generates an HTTP response. If you're integrating with a new web framework, you **MUST** override this method within your subclass. :param data: The body of the response to send :type data: string :param status: (Optional) The status code to respond with. Default is ``200`` :type status: integer :returns: A response object """ raise NotImplementedError() def build_error(self, err): """ When an exception is encountered, this generates a JSON error message for display to the user. :param err: The exception seen. The message is exposed to the user, so beware of sensitive data leaking. :type err: Exception :returns: A response object """ data = { 'error': err.args[0], } if self.is_debug(): # Add the traceback. data['traceback'] = format_traceback(sys.exc_info()) body = self.serializer.serialize(data) status = getattr(err, 'status', 500) return self.build_response(body, status=status) def is_debug(self): """ Controls whether or not the resource is in a debug environment. If so, tracebacks will be added to the serialized response. The default implementation simply returns ``False``, so if you're integrating with a new web framework, you'll need to override this method within your subclass. :returns: If the resource is in a debug environment :rtype: boolean """ return False def bubble_exceptions(self): """ Controls whether or not exceptions will be re-raised when encountered. The default implementation returns ``False``, which means errors should return a serialized response. If you'd like exceptions to be re-raised, override this method & return ``True``. :returns: Whether exceptions should be re-raised or not :rtype: boolean """ return False def handle(self, endpoint, *args, **kwargs): """ A convenient dispatching method, this centralized some of the common flow of the views. This wraps/calls the methods the user defines (``list/detail/create`` etc.), allowing the user to ignore the authentication/deserialization/serialization/response & just focus on their data/interactions. :param endpoint: The style of URI call (typically either ``list`` or ``detail``). :type endpoint: string :param args: (Optional) Any positional URI parameter data is passed along here. Somewhat framework/URL-specific. :param kwargs: (Optional) Any keyword/named URI parameter data is passed along here. Somewhat framework/URL-specific. :returns: A response object """ self.endpoint = endpoint method = self.request_method() try: # Use ``.get()`` so we can also dodge potentially incorrect # ``endpoint`` errors as well. if not method in self.http_methods.get(endpoint, {}): raise MethodNotImplemented( "Unsupported method '{0}' for {1} endpoint.".format( method, endpoint ) ) if not self.is_authenticated(): raise Unauthorized() self.data = self.deserialize(method, endpoint, self.request_body()) view_method = getattr(self, self.http_methods[endpoint][method]) data = view_method(*args, **kwargs) serialized = self.serialize(method, endpoint, data) except Exception as err: return self.handle_error(err) status = self.status_map.get(self.http_methods[endpoint][method], OK) return self.build_response(serialized, status=status) def handle_error(self, err): """ When an exception is encountered, this generates a serialized error message to return the user. :param err: The exception seen. The message is exposed to the user, so beware of sensitive data leaking. :type err: Exception :returns: A response object """ if self.bubble_exceptions(): raise err return self.build_error(err) def deserialize(self, method, endpoint, body): """ A convenience method for deserializing the body of a request. If called on a list-style endpoint, this calls ``deserialize_list``. Otherwise, it will call ``deserialize_detail``. :param method: The HTTP method of the current request :type method: string :param endpoint: The endpoint style (``list`` or ``detail``) :type endpoint: string :param body: The body of the current request :type body: string :returns: The deserialized data :rtype: ``list`` or ``dict`` """ if endpoint == 'list': return self.deserialize_list(body) return self.deserialize_detail(body) def deserialize_list(self, body): """ Given a string of text, deserializes a (presumed) list out of the body. :param body: The body of the current request :type body: string :returns: The deserialized body or an empty ``list`` """ if body: return self.serializer.deserialize(body) return [] def deserialize_detail(self, body): """ Given a string of text, deserializes a (presumed) object out of the body. :param body: The body of the current request :type body: string :returns: The deserialized body or an empty ``dict`` """ if body: return self.serializer.deserialize(body) return {} def serialize(self, method, endpoint, data): """ A convenience method for serializing data for a response. If called on a list-style endpoint, this calls ``serialize_list``. Otherwise, it will call ``serialize_detail``. :param method: The HTTP method of the current request :type method: string :param endpoint: The endpoint style (``list`` or ``detail``) :type endpoint: string :param data: The body for the response :type data: string :returns: A serialized version of the data :rtype: string """ if endpoint == 'list': # Create is a special-case, because you POST it to the collection, # not to a detail. if method == 'POST': return self.serialize_detail(data) return self.serialize_list(data) return self.serialize_detail(data) def serialize_list(self, data): """ Given a collection of data (``objects`` or ``dicts``), serializes them. :param data: The collection of items to serialize :type data: list or iterable :returns: The serialized body :rtype: string """ if data is None: return '' # Check for a ``Data``-like object. We should assume ``True`` (all # data gets prepared) unless it's explicitly marked as not. if not getattr(data, 'should_prepare', True): prepped_data = data.value else: prepped_data = [self.prepare(item) for item in data] final_data = self.wrap_list_response(prepped_data) return self.serializer.serialize(final_data) def serialize_detail(self, data): """ Given a single item (``object`` or ``dict``), serializes it. :param data: The item to serialize :type data: object or dict :returns: The serialized body :rtype: string """ if data is None: return '' # Check for a ``Data``-like object. We should assume ``True`` (all # data gets prepared) unless it's explicitly marked as not. if not getattr(data, 'should_prepare', True): prepped_data = data.value else: prepped_data = self.prepare(data) return self.serializer.serialize(prepped_data) def prepare(self, data): """ Given an item (``object`` or ``dict``), this will potentially go through & reshape the output based on ``self.prepare_with`` object. :param data: An item to prepare for serialization :type data: object or dict :returns: A potentially reshaped dict :rtype: dict """ return self.preparer.prepare(data) def wrap_list_response(self, data): """ Takes a list of data & wraps it in a dictionary (within the ``objects`` key). For security in JSON responses, it's better to wrap the list results in an ``object`` (due to the way the ``Array`` constructor can be attacked in Javascript). See http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ & similar for details. Overridable to allow for modifying the key names, adding data (or just insecurely return a plain old list if that's your thing). :param data: A list of data about to be serialized :type data: list :returns: A wrapping dict :rtype: dict """ return { "objects": data } def is_authenticated(self): """ A simple hook method for controlling whether a request is authenticated to continue. By default, we only allow the safe ``GET`` methods. All others are denied. :returns: Whether the request is authenticated or not. :rtype: boolean """ if self.request_method() == 'GET': return True return False # Common methods the user should implement. def list(self, *args, **kwargs): """ Returns the data for a GET on a list-style endpoint. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: A collection of data :rtype: list or iterable """ raise MethodNotImplemented() def detail(self, *args, **kwargs): """ Returns the data for a GET on a detail-style endpoint. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: An item :rtype: object or dict """ raise MethodNotImplemented() def create(self, *args, **kwargs): """ Allows for creating data via a POST on a list-style endpoint. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: May return the created item or ``None`` """ raise MethodNotImplemented() def update(self, *args, **kwargs): """ Updates existing data for a PUT on a detail-style endpoint. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: May return the updated item or ``None`` """ raise MethodNotImplemented() def delete(self, *args, **kwargs): """ Deletes data for a DELETE on a detail-style endpoint. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: ``None`` """ raise MethodNotImplemented() # Uncommon methods the user should implement. # These have intentionally uglier method names, which reflects just how # much harder they are to get right. def update_list(self, *args, **kwargs): """ Updates the entire collection for a PUT on a list-style endpoint. Uncommonly implemented due to the complexity & (varying) busines-logic involved. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: A collection of data :rtype: list or iterable """ raise MethodNotImplemented() def create_detail(self, *args, **kwargs): """ Creates a subcollection of data for a POST on a detail-style endpoint. Uncommonly implemented due to the rarity of having nested collections. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: A collection of data :rtype: list or iterable """ raise MethodNotImplemented() def delete_list(self, *args, **kwargs): """ Deletes *ALL* data in the collection for a DELETE on a list-style endpoint. Uncommonly implemented due to potential of trashing large datasets. Implement with care. **MUST BE OVERRIDDEN BY THE USER** - By default, this returns ``MethodNotImplemented``. :returns: ``None`` """ raise MethodNotImplemented() restless-2.1.1/restless/serializers.py000066400000000000000000000045261311406265400201420ustar00rootroot00000000000000from .exceptions import BadRequest from .utils import json, MoreTypesJSONEncoder class Serializer(object): """ A base serialization class. Defines the protocol expected of a serializer, but only raises ``NotImplementedError``. Either subclass this or provide an object with the same ``deserialize/serialize`` methods on it. """ def deserialize(self, body): """ Handles deserializing data coming from the user. Should return a plain Python data type (such as a dict or list) containing the data. :param body: The body of the current request :type body: string :returns: The deserialized data :rtype: ``list`` or ``dict`` """ raise NotImplementedError("Subclasses must implement this method.") def serialize(self, data): """ Handles serializing data being sent to the user. Should return a plain Python string containing the serialized data in the appropriate format. :param data: The body for the response :type data: ``list`` or ``dict`` :returns: A serialized version of the data :rtype: string """ raise NotImplementedError("Subclasses must implement this method.") class JSONSerializer(Serializer): def deserialize(self, body): """ The low-level deserialization. Underpins ``deserialize``, ``deserialize_list`` & ``deserialize_detail``. Has no built-in smarts, simply loads the JSON. :param body: The body of the current request :type body: string :returns: The deserialized data :rtype: ``list`` or ``dict`` """ try: if isinstance(body, bytes): return json.loads(body.decode('utf-8')) return json.loads(body) except ValueError: raise BadRequest('Request body is not valid JSON') def serialize(self, data): """ The low-level serialization. Underpins ``serialize``, ``serialize_list`` & ``serialize_detail``. Has no built-in smarts, simply dumps the JSON. :param data: The body for the response :type data: string :returns: A serialized version of the data :rtype: string """ return json.dumps(data, cls=MoreTypesJSONEncoder) restless-2.1.1/restless/tnd.py000066400000000000000000000124741311406265400163740ustar00rootroot00000000000000from tornado import web, gen from .constants import OK, NO_CONTENT from .resources import Resource from .exceptions import MethodNotImplemented, Unauthorized import weakref import inspect try: from tornado.concurrent import is_future except ImportError: is_future = None if is_future is None: """ Please refer to tornado.concurrent module(newer than 4.0) for implementation of this function. """ try: from concurrent import futures except ImportError: futures = None from tornado.concurrent import Future if futures is None: FUTURES = Future else: FUTURES = (Future, futures.Future) is_future = lambda x: isinstance(x, FUTURES) @gen.coroutine def _method(self, *args, **kwargs): """ the body of those http-methods used in tornado.web.RequestHandler """ yield self.resource_handler.handle(self.__resource_view_type__, *args, **kwargs) class _BridgeMixin(object): """ This mixin would pass tornado parameters to restless, and helps to init a resource instance """ def __init__(self, *args, **kwargs): super(_BridgeMixin, self).__init__(*args, **kwargs) # create a resource instance based on the registered class # and init-parameters self.resource_handler = self.__class__.__resource_cls__( *self.__resource_args__, **self.__resource_kwargs__ ) self.resource_handler.request = self.request self.resource_handler.application = self.application self.resource_handler.ref_rh = weakref.proxy(self) # avoid circular reference between class TornadoResource(Resource): """ A Tornado-specific ``Resource`` subclass. """ _request_handler_base_ = web.RequestHandler """ To override ``tornado.web.RequestHandler`` we used, please assign your RequestHandler via this attribute. """ def __init__(self, *args, **kwargs): super(TornadoResource, self).__init__(*args, **kwargs) self.request = None """ a reference to ``tornado.httpclient.HTTPRequest`` """ self.application = None """ a reference to ``tornado.web.Application`` """ self.ref_rh = None @property def r_handler(self): """ access to ``tornado.web.RequestHandler`` """ return self.ref_rh @classmethod def as_view(cls, view_type, *init_args, **init_kwargs): """ Return a subclass of tornado.web.RequestHandler and apply required setting. """ global _method new_cls = type( cls.__name__ + '_' + _BridgeMixin.__name__ + '_restless', (_BridgeMixin, cls._request_handler_base_,), dict( __resource_cls__=cls, __resource_args__=init_args, __resource_kwargs__=init_kwargs, __resource_view_type__=view_type) ) """ Add required http-methods to the newly created class We need to scan through MRO to find what functions users declared, and then add corresponding http-methods used by Tornado. """ bases = inspect.getmro(cls) bases = bases[0:bases.index(Resource)-1] for k, v in cls.http_methods[view_type].items(): if any(v in base_cls.__dict__ for base_cls in bases): setattr(new_cls, k.lower(), _method) return new_cls def request_method(self): return self.request.method def request_body(self): return self.request.body def build_response(self, data, status=OK): if status == NO_CONTENT: # Avoid crashing the client when it tries to parse nonexisting JSON. content_type = 'text/plain' else: content_type = 'application/json' self.ref_rh.set_header("Content-Type", "{}; charset=UTF-8" .format(content_type)) self.ref_rh.set_status(status) self.ref_rh.finish(data) def is_debug(self): return self.application.settings.get('debug', False) @gen.coroutine def handle(self, endpoint, *args, **kwargs): """ almost identical to Resource.handle, except the way we handle the return value of view_method. """ method = self.request_method() try: if not method in self.http_methods.get(endpoint, {}): raise MethodNotImplemented( "Unsupported method '{0}' for {1} endpoint.".format( method, endpoint ) ) if not self.is_authenticated(): raise Unauthorized() self.data = self.deserialize(method, endpoint, self.request_body()) view_method = getattr(self, self.http_methods[endpoint][method]) data = view_method(*args, **kwargs) if is_future(data): # need to check if the view_method is a generator or not data = yield data serialized = self.serialize(method, endpoint, data) except Exception as err: raise gen.Return(self.handle_error(err)) status = self.status_map.get(self.http_methods[endpoint][method], OK) raise gen.Return(self.build_response(serialized, status=status)) restless-2.1.1/restless/utils.py000066400000000000000000000021641311406265400167420ustar00rootroot00000000000000 import datetime import decimal import json import traceback import uuid class MoreTypesJSONEncoder(json.JSONEncoder): """ A JSON encoder that allows for more common Python data types. In addition to the defaults handled by ``json``, this also supports: * ``datetime.datetime`` * ``datetime.date`` * ``datetime.time`` * ``decimal.Decimal`` * ``uuid.UUID`` """ def default(self, data): if isinstance(data, (datetime.datetime, datetime.date, datetime.time)): return data.isoformat() elif isinstance(data, decimal.Decimal) or isinstance(data, uuid.UUID): return str(data) else: return super(MoreTypesJSONEncoder, self).default(data) def format_traceback(exc_info): stack = traceback.format_stack() stack = stack[:-2] stack.extend(traceback.format_tb(exc_info[2])) stack.extend(traceback.format_exception_only(exc_info[0], exc_info[1])) stack_str = "Traceback (most recent call last):\n" stack_str += "".join(stack) # Remove the last \n stack_str = stack_str[:-1] return stack_str restless-2.1.1/rtfd-requirements.txt000066400000000000000000000000071311406265400175770ustar00rootroot00000000000000Django restless-2.1.1/setup.cfg000066400000000000000000000000261311406265400152000ustar00rootroot00000000000000[wheel] universal = 1 restless-2.1.1/setup.py000066400000000000000000000021301311406265400150670ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup import restless setup( name='restless', version=restless.VERSION, description='A lightweight REST miniframework for Python.', author='Daniel Lindsley', author_email='daniel@toastdriven.com', url='http://github.com/toastdriven/restless/', long_description=open('README.rst', 'r').read(), packages=[ 'restless', ], requires=[ 'six(>=1.4.0)', ], install_requires=[ 'six>=1.4.0', ], tests_require=[ 'mock', 'tox', ], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Framework :: Flask', 'Framework :: Pyramid', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Topic :: Utilities' ], ) restless-2.1.1/tests/000077500000000000000000000000001311406265400145235ustar00rootroot00000000000000restless-2.1.1/tests/__init__.py000066400000000000000000000002451311406265400166350ustar00rootroot00000000000000import unittest if not hasattr(unittest.TestCase, 'addCleanup'): raise Exception("You're likely running Python 2.6. Please test on a newer version of Python.") restless-2.1.1/tests/fakes.py000066400000000000000000000010261311406265400161650ustar00rootroot00000000000000import six class FakeHttpRequest(object): def __init__(self, method='GET', body=''): self.method = method.upper() self.body = body if six.PY3: self.body = body.encode('utf-8') class FakeHttpResponse(object): def __init__(self, body, content_type='text/html'): self.body = body self.content_type = content_type self.status_code = 200 class FakeModel(object): def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) restless-2.1.1/tests/test_dj.py000066400000000000000000000314761311406265400165440ustar00rootroot00000000000000import unittest try: from http.client import responses except ImportError: from httplib import responses try: from django.conf import settings except ImportError: settings = None DjangoResource = object else: from django.http import Http404 from django.core.exceptions import ObjectDoesNotExist # Ugh. Settings for Django. settings.configure(DEBUG=True) from restless.dj import DjangoResource from restless.exceptions import Unauthorized from restless.preparers import FieldsPreparer from restless.resources import skip_prepare from restless.utils import json from .fakes import FakeHttpRequest, FakeModel class DjTestResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'username', 'body': 'content' }) fake_db = [] def __init__(self, *args, **kwargs): super(DjTestResource, self).__init__(*args, **kwargs) self.http_methods.update({ 'schema': { 'GET': 'schema', } }) def fake_init(self): # Just for testing. self.__class__.fake_db = [ FakeModel( id='dead-beef', title='First post', username='daniel', content='Hello world!'), FakeModel( id='de-faced', title='Another', username='daniel', content='Stuff here.'), FakeModel( id='bad-f00d', title='Last', username='daniel', content="G'bye!"), ] def is_authenticated(self): if self.request_method() == 'DELETE' and self.endpoint == 'list': return False return True def list(self): return self.fake_db def detail(self, pk): for item in self.fake_db: if item.id == pk: return item # If it wasn't found in our fake DB, raise a Django-esque exception. raise ObjectDoesNotExist("Model with pk {0} not found.".format(pk)) def create(self): self.fake_db.append(FakeModel( **self.data )) def update(self, pk): for item in self.fake_db: if item.id == pk: for k, v in self.data: setattr(item, k, v) return def create_detail(self): raise ValueError({ 'code': 'random-crazy', 'message': 'This is a random & crazy exception.', }) def delete(self, pk): for i, item in enumerate(self.fake_db): if item.id == pk: del self.fake_db[i] return @skip_prepare def schema(self): # A WILD SCHEMA VIEW APPEARS! return { 'fields': { 'id': { 'type': 'integer', 'required': True, 'help_text': 'The unique id for the post', }, 'title': { 'type': 'string', 'required': True, 'help_text': "The post's title", }, 'author': { 'type': 'string', 'required': True, 'help_text': 'The username of the author of the post', }, 'body': { 'type': 'string', 'required': False, 'default': '', 'help_text': 'The content of the post', } }, 'format': 'application/json', 'allowed_list_http_methods': ['GET', 'POST'], 'allowed_detail_http_methods': ['GET', 'PUT', 'DELETE'], } class DjTestResourceHttp404Handling(DjTestResource): def detail(self, pk): for item in self.fake_db: if item.id == pk: return item # If it wasn't found in our fake DB, raise a Django-esque exception. raise Http404("Model with pk {0} not found.".format(pk)) @unittest.skipIf(not settings, "Django is not available") class DjangoResourceTestCase(unittest.TestCase): def setUp(self): super(DjangoResourceTestCase, self).setUp() self.res = DjTestResource() # Just for the fake data. self.res.fake_init() def test_as_list(self): list_endpoint = DjTestResource.as_list() req = FakeHttpRequest('GET') resp = list_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'objects': [ { 'author': 'daniel', 'body': 'Hello world!', 'id': 'dead-beef', 'title': 'First post' }, { 'author': 'daniel', 'body': 'Stuff here.', 'id': 'de-faced', 'title': 'Another' }, { 'author': 'daniel', 'body': "G'bye!", 'id': 'bad-f00d', 'title': 'Last' } ] }) def test_as_detail(self): detail_endpoint = DjTestResource.as_detail() req = FakeHttpRequest('GET') resp = detail_endpoint(req, 'de-faced') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'author': 'daniel', 'body': 'Stuff here.', 'id': 'de-faced', 'title': 'Another' }) def test_as_view(self): # This would be hooked up via the URLconf... schema_endpoint = DjTestResource.as_view('schema') req = FakeHttpRequest('GET') resp = schema_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) schema = json.loads(resp.content.decode('utf-8')) self.assertEqual( sorted(list(schema['fields'].keys())), [ 'author', 'body', 'id', 'title', ] ) self.assertEqual(schema['fields']['id']['type'], 'integer') self.assertEqual(schema['format'], 'application/json') def test_handle_not_implemented(self): self.res.request = FakeHttpRequest('TRACE') resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 501) self.assertEqual(resp.reason_phrase.title(), responses[501]) resp_json = json.loads(resp.content.decode('utf-8')) self.assertEqual( resp_json['error'], "Unsupported method 'TRACE' for list endpoint.") self.assertTrue('traceback' in resp_json) def test_handle_not_authenticated(self): # Special-cased above for testing. self.res.request = FakeHttpRequest('DELETE') # First with DEBUG on resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 401) self.assertEqual(resp.reason_phrase.title(), responses[401]) resp_json = json.loads(resp.content.decode('utf-8')) self.assertEqual(resp_json['error'], 'Unauthorized.') self.assertTrue('traceback' in resp_json) # Now with DEBUG off. settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 401) resp_json = json.loads(resp.content.decode('utf-8')) self.assertEqual(resp_json, { 'error': 'Unauthorized.', }) self.assertFalse('traceback' in resp_json) # Last, with bubble_exceptions. class Bubbly(DjTestResource): def bubble_exceptions(self): return True with self.assertRaises(Unauthorized): bubb = Bubbly() bubb.request = FakeHttpRequest('DELETE') bubb.handle('list') def test_handle_build_err(self): # Special-cased above for testing. self.res.request = FakeHttpRequest('POST') settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = self.res.handle('detail') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 500) self.assertEqual(resp.reason_phrase.title(), responses[500]) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'error': { 'code': 'random-crazy', 'message': 'This is a random & crazy exception.', } }) def test_object_does_not_exist(self): # Make sure we get a proper Not Found exception rather than a # generic 500, when code raises a ObjectDoesNotExist exception. self.res.request = FakeHttpRequest('GET') settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = self.res.handle('detail', 1001) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 404) self.assertEqual(resp.reason_phrase.title(), responses[404]) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'error': 'Model with pk 1001 not found.' }) def test_http404_exception_handling(self): # Make sure we get a proper Not Found exception rather than a # generic 500, when code raises a Http404 exception. res = DjTestResourceHttp404Handling() res.request = FakeHttpRequest('GET') settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = res.handle('detail', 1001) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 404) self.assertEqual(resp.reason_phrase.title(), responses[404]) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'error': 'Model with pk 1001 not found.' }) def test_build_url_name(self): self.assertEqual( DjTestResource.build_url_name('list'), 'api_djtest_list' ) self.assertEqual( DjTestResource.build_url_name('detail'), 'api_djtest_detail' ) self.assertEqual( DjTestResource.build_url_name('schema'), 'api_djtest_schema' ) self.assertEqual( DjTestResource.build_url_name('list', name_prefix='v2_'), 'v2_list' ) self.assertEqual( DjTestResource.build_url_name('detail', name_prefix='v2_'), 'v2_detail' ) self.assertEqual( DjTestResource.build_url_name('schema', name_prefix='v2_'), 'v2_schema' ) def test_urls(self): patterns = DjTestResource.urls() self.assertEqual(len(patterns), 2) self.assertEqual(patterns[0].name, 'api_djtest_list') self.assertEqual(patterns[1].name, 'api_djtest_detail') patterns = DjTestResource.urls(name_prefix='v2_tests') self.assertEqual(len(patterns), 2) self.assertEqual(patterns[0].name, 'v2_tests_list') self.assertEqual(patterns[1].name, 'v2_tests_detail') def test_create(self): self.res.request = FakeHttpRequest( 'POST', body='{"id": 6, "title": "Moved hosts", "author": "daniel"}') self.assertEqual(len(self.res.fake_db), 3) resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 201) self.assertEqual(resp.content.decode('utf-8'), '') # Check the internal state. self.assertEqual(len(self.res.fake_db), 4) self.assertEqual(self.res.data, { 'author': 'daniel', 'id': 6, 'title': 'Moved hosts' }) def test_delete(self): self.res.request = FakeHttpRequest('DELETE') self.assertEqual(len(self.res.fake_db), 3) resp = self.res.handle('detail', pk='de-faced') self.assertEqual(resp['Content-Type'], 'text/plain') self.assertEqual(resp.status_code, 204) self.assertEqual(resp.content.decode('utf-8'), '') # Check the internal state. self.res.request = FakeHttpRequest('GET') self.assertEqual(len(self.res.fake_db), 2) resp = self.res.handle('detail', pk='de-faced') self.assertEqual(resp.status_code, 404) restless-2.1.1/tests/test_fl.py000066400000000000000000000077421311406265400165470ustar00rootroot00000000000000import unittest try: # Ugh. Globals for Flask. import flask from restless.fl import FlaskResource except ImportError: flask = None FlaskResource = object from restless.utils import json from .fakes import FakeHttpRequest class FlTestResource(FlaskResource): fake_db = [] def fake_init(self): # Just for testing. self.__class__.fake_db = [ {"id": 'dead-beef', "title": 'First post'}, {"id": 'de-faced', "title": 'Another'}, {"id": 'bad-f00d', "title": 'Last'}, ] def list(self): return self.fake_db def detail(self, pk): for item in self.fake_db: if item['id'] == pk: return item def create(self): self.fake_db.append(self.data) @unittest.skipIf(not flask, 'Flask is not available') class FlaskResourceTestCase(unittest.TestCase): def setUp(self): super(FlaskResourceTestCase, self).setUp() self.res = FlTestResource() self.app = flask.Flask('test_restless') self.app.config['DEBUG'] = True # Just for the fake data. self.res.fake_init() def test_as_list(self): list_endpoint = FlTestResource.as_list() flask.request = FakeHttpRequest('GET') with self.app.test_request_context('/whatever/', method='GET'): resp = list_endpoint() self.assertEqual(resp.headers['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.data.decode('utf-8')), { 'objects': [ { 'id': 'dead-beef', 'title': 'First post' }, { 'id': 'de-faced', 'title': 'Another' }, { 'id': 'bad-f00d', 'title': 'Last' } ] }) def test_as_detail(self): detail_endpoint = FlTestResource.as_detail() flask.request = FakeHttpRequest('GET') with self.app.test_request_context('/whatever/', method='GET'): resp = detail_endpoint('de-faced') self.assertEqual(resp.headers['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.data.decode('utf-8')), { 'id': 'de-faced', 'title': 'Another' }) def test_is_debug(self): with self.app.test_request_context('/whatever/', method='GET'): self.assertTrue(self.res.is_debug()) with self.app.test_request_context('/whatever/', method='GET'): self.app.debug = False # This should do the correct lookup. self.assertFalse(self.res.is_debug()) def test_build_response(self): with self.app.test_request_context('/whatever/', method='GET'): resp = self.res.build_response('Hello, world!', status=302) self.assertEqual(resp.status_code, 302) self.assertEqual(resp.headers['Content-Type'], 'application/json') self.assertEqual(resp.data.decode('utf-8'), 'Hello, world!') def test_add_url_rules(self): with self.app.test_request_context('/whatever/', method='GET'): FlTestResource.add_url_rules(self.app, '/api/') rules = sorted([rule.endpoint for rule in self.app.url_map.iter_rules()]) self.assertEqual(len(rules), 3) self.assertEqual(rules[0], 'api_fltest_detail') self.assertEqual(rules[1], 'api_fltest_list') FlTestResource.add_url_rules(self.app, '/api/', endpoint_prefix='v2_tests') rules = sorted([rule.endpoint for rule in self.app.url_map.iter_rules()]) self.assertEqual(len(rules), 5) self.assertEqual(rules[3], 'v2_tests_detail') self.assertEqual(rules[4], 'v2_tests_list') restless-2.1.1/tests/test_preparers.py000066400000000000000000000112661311406265400201450ustar00rootroot00000000000000import unittest from restless.preparers import (CollectionSubPreparer, SubPreparer, FieldsPreparer) class InstaObj(object): def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) def dont(self): return {'panic': 'vogon'} class LookupDataTestCase(unittest.TestCase): def setUp(self): super(LookupDataTestCase, self).setUp() self.preparer = FieldsPreparer(fields=None) self.obj_data = InstaObj( say='what', count=453, moof={ 'buried': { 'id': 7, 'data': InstaObj(yes='no') } }, parent=None ) self.dict_data = { 'hello': 'world', 'abc': 123, 'more': { 'things': 'here', 'nested': InstaObj( awesome=True, depth=3 ), }, 'parent': None, 'who': [ {'name': 'Ford'}, {'name': 'Arthur'}, {'name': 'Beeblebrox'}, ], 'dont': lambda: { 'panic': 'vogon', }, } def test_dict_simple(self): self.assertEqual(self.preparer.lookup_data('hello', self.dict_data), 'world') self.assertEqual(self.preparer.lookup_data('abc', self.dict_data), 123) def test_obj_simple(self): self.assertEqual(self.preparer.lookup_data('say', self.obj_data), 'what') self.assertEqual(self.preparer.lookup_data('count', self.obj_data), 453) def test_dict_nested(self): self.assertEqual(self.preparer.lookup_data('more.things', self.dict_data), 'here') self.assertEqual(self.preparer.lookup_data('more.nested.depth', self.dict_data), 3) def test_obj_nested(self): self.assertEqual(self.preparer.lookup_data('moof.buried.id', self.obj_data), 7) self.assertEqual(self.preparer.lookup_data('moof.buried.data.yes', self.obj_data), 'no') def test_dict_miss(self): with self.assertRaises(KeyError): self.preparer.lookup_data('another', self.dict_data) def test_obj_miss(self): with self.assertRaises(AttributeError): self.preparer.lookup_data('whee', self.obj_data) def test_dict_nullable_fk(self): self.assertEqual(self.preparer.lookup_data('parent.id', self.dict_data), None) def test_obj_nullable_fk(self): self.assertEqual(self.preparer.lookup_data('parent.id', self.obj_data), None) def test_empty_lookup(self): # We could possibly get here in the recursion. self.assertEqual(self.preparer.lookup_data('', 'Last value'), 'Last value') def test_complex_miss(self): with self.assertRaises(AttributeError): self.preparer.lookup_data('more.nested.nope', self.dict_data) def test_obj_callable(self): self.assertEqual( self.preparer.lookup_data('dont.panic', self.obj_data), 'vogon', ) def test_dict_callable(self): self.assertEqual( self.preparer.lookup_data('dont.panic', self.dict_data), 'vogon', ) def test_prepare_simple(self): preparer = FieldsPreparer(fields={ 'flying': 'say', }) preped = preparer.prepare(self.obj_data) self.assertEqual(preped, {'flying': 'what'}) def test_prepare_subpreparer(self): subpreparer = FieldsPreparer(fields={ 'id': 'id', 'data': 'data', }) preparer = FieldsPreparer(fields={ 'flying': 'say', 'wale': SubPreparer('moof.buried', subpreparer), }) preped = preparer.prepare(self.obj_data) def test_prepare_subsubpreparer(self): subsubpreparer = FieldsPreparer(fields={ 'really': 'yes', }) subpreparer = FieldsPreparer(fields={ 'data': SubPreparer('data', subsubpreparer), }) preparer = FieldsPreparer(fields={ 'wale': SubPreparer('moof.buried', subpreparer), }) preped = preparer.prepare(self.obj_data) self.assertEqual(preped, {'wale': {'data': {'really': 'no'}}}) def test_prepare_collection_subpreparer(self): subpreparer = FieldsPreparer(fields={ 'name': 'name', }) preparer = FieldsPreparer(fields={ 'who': CollectionSubPreparer('who', subpreparer), }) preped = preparer.prepare(self.dict_data) self.assertEqual(preped, {'who': [ {'name': 'Ford'}, {'name': 'Arthur'}, {'name': 'Beeblebrox'}, ]}) restless-2.1.1/tests/test_pyr.py000066400000000000000000000071701311406265400167530ustar00rootroot00000000000000import unittest try: from pyramid import testing from restless.pyr import PyramidResource except ImportError: testing = None PyramidResource = object from restless.utils import json from .fakes import FakeHttpRequest, FakeHttpResponse class PyrTestResource(PyramidResource): fake_db = [] def fake_init(self): # Just for testing. self.__class__.fake_db = [ {"id": "dead-beef", "title": 'First post'}, {"id": "de-faced", "title": 'Another'}, {"id": "bad-f00d", "title": 'Last'}, ] def list(self): return self.fake_db def detail(self, name): for item in self.fake_db: if item['id'] == name: return item def create(self): self.fake_db.append(self.data) def is_authenticated(self): if self.request_method() == 'DELETE': return False return True @unittest.skipIf(not testing, 'Pyramid is not available') class PyramidResourceTestCase(unittest.TestCase): def setUp(self): self.config = testing.setUp() self.res = PyrTestResource() self.res.fake_init() def test_as_list(self): list_endpoint = PyrTestResource.as_list() req = FakeHttpRequest('GET') resp = list_endpoint(req) self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.body.decode('utf-8')), { 'objects': [ { 'id': 'dead-beef', 'title': 'First post' }, { 'id': 'de-faced', 'title': 'Another' }, { 'id': 'bad-f00d', 'title': 'Last' } ] }) def test_as_detail(self): detail_endpoint = PyrTestResource.as_detail() req = testing.DummyRequest() req = FakeHttpRequest('GET') req.matchdict = {'name': 'de-faced'} resp = detail_endpoint(req) self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.body.decode('utf-8')), { 'id': 'de-faced', 'title': 'Another' }) def test_handle_not_authenticated(self): # Special-cased above for testing. self.res.request = FakeHttpRequest('DELETE') resp = self.res.handle('list') self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 401) self.assertEqual(resp.body.decode('utf-8'), '{"error": "Unauthorized."}') def test_add_views(self): config = PyrTestResource.add_views(self.config, '/users/') routes = config.get_routes_mapper().get_routes() self.assertEqual(len(routes), 2) self.assertEqual([r.name for r in routes], ['api_pyrtest_list', 'api_pyrtest_detail']) self.assertEqual([r.path for r in routes], ['/users/', '/users/{name}/']) def test_create(self): self.res.request = FakeHttpRequest('POST', body='{"id": 6, "title": "Moved hosts"}') self.assertEqual(len(self.res.fake_db), 3) resp = self.res.handle('list') self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 201) self.assertEqual(resp.body.decode('utf-8'), '') # Check the internal state. self.assertEqual(len(self.res.fake_db), 4) self.assertEqual(self.res.data, { 'id': 6, 'title': 'Moved hosts' }) restless-2.1.1/tests/test_resources.py000066400000000000000000000276541311406265400201640ustar00rootroot00000000000000import six import unittest from restless.exceptions import HttpError, NotFound, MethodNotImplemented from restless.preparers import Preparer, FieldsPreparer from restless.resources import Resource from restless.utils import json from .fakes import FakeHttpRequest, FakeHttpResponse class GenericResource(Resource): def build_response(self, data, status=200): resp = FakeHttpResponse(data, content_type='application/json') resp.status_code = status return resp # This should Fake some endpoint Authentication def is_authenticated(self): if self.endpoint == 'list': return False return super(GenericResource, self).is_authenticated() class ResourceTestCase(unittest.TestCase): resource_class = GenericResource def setUp(self): super(ResourceTestCase, self).setUp() self.res = self.resource_class() # Assign here, since we typically won't be entering through # ``as_list/as_detail`` methods like normal flow. self.res.request = FakeHttpRequest() def test_init(self): res = self.resource_class('abc', test=True) self.assertEqual(res.init_args, ('abc',)) self.assertEqual(res.init_kwargs, {'test': True}) self.assertEqual(res.request, None) self.assertEqual(res.data, None) self.assertEqual(res.endpoint, None) self.assertEqual(res.status, 200) def test_request_method(self): self.assertEqual(self.res.request_method(), 'GET') self.res.request = FakeHttpRequest('POST', '{"hello": "world"}') self.assertEqual(self.res.request_method(), 'POST') self.res.request = FakeHttpRequest('PUT', '{"hello": "world"}') self.assertEqual(self.res.request_method(), 'PUT') self.res.request = FakeHttpRequest('DELETE', '') self.assertEqual(self.res.request_method(), 'DELETE') def test_request_body(self): if six.PY3: self.assertEqual(self.res.request_body(), b'') else: self.assertEqual(self.res.request_body(), '') self.res.request = FakeHttpRequest('POST', '{"hello": "world"}') if six.PY3: self.assertEqual(self.res.request_body(), b'{"hello": "world"}') else: self.assertEqual(self.res.request_body(), '{"hello": "world"}') self.res.request = FakeHttpRequest('PUT', '{"hello": "world"}') if six.PY3: self.assertEqual(self.res.request_body(), b'{"hello": "world"}') else: self.assertEqual(self.res.request_body(), '{"hello": "world"}') self.res.request = FakeHttpRequest('DELETE', '{}') if six.PY3: self.assertEqual(self.res.request_body(), b'{}') else: self.assertEqual(self.res.request_body(), '{}') def test_build_response(self): resp = self.res.build_response('Hello, world!') self.assertEqual(resp.body, 'Hello, world!') self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 200) resp = self.res.build_response('{"hello": "world"}', status=302) self.assertEqual(resp.body, '{"hello": "world"}') self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 302) def test_build_error(self): err = HttpError("Whoopsie") resp = self.res.build_error(err) resp_body = json.loads(resp.body) self.assertEqual(resp_body, {'error': 'Whoopsie'}) self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 500) nf_err = NotFound() resp = self.res.build_error(nf_err) resp_body = json.loads(resp.body) # Default error message. self.assertEqual(resp_body, {'error': 'Resource not found.'}) self.assertEqual(resp.content_type, 'application/json') # Custom status code. self.assertEqual(resp.status_code, 404) # Non-restless exception. unknown_err = AttributeError("'something' not found on the object.") resp = self.res.build_error(unknown_err) resp_body = json.loads(resp.body) # Still gets the JSON treatment & an appropriate status code. self.assertEqual(resp_body, {'error': "'something' not found on the object."}) self.assertEqual(resp.content_type, 'application/json') self.assertEqual(resp.status_code, 500) def test_is_debug(self): self.assertFalse(self.res.is_debug()) def test_bubble_exceptions(self): self.assertFalse(self.res.bubble_exceptions()) def test_deserialize(self): list_body = '["one", "three", "two"]' self.assertEqual(self.res.deserialize('POST', 'list', list_body), [ "one", "three", "two", ]) # Should select list. self.assertEqual(self.res.deserialize('POST', 'list', ''), []) # Should select detail. self.assertEqual(self.res.deserialize('PUT', 'detail', ''), {}) def test_deserialize_list(self): body = '["one", "three", "two"]' self.assertEqual(self.res.deserialize_list(body), [ "one", "three", "two", ]) self.assertEqual(self.res.deserialize_list(''), []) def test_deserialize_detail(self): body = '{"title": "Hitchhiker\'s Guide To The Galaxy", "author": "Douglas Adams"}' self.assertEqual(self.res.deserialize_detail(body), { 'author': 'Douglas Adams', 'title': "Hitchhiker's Guide To The Galaxy", }) self.assertEqual(self.res.deserialize_detail(''), {}) def test_serialize(self): list_data = ['a', 'c', 'b'] detail_data = {'hello': 'world'} # Normal calls. self.assertEqual(self.res.serialize('GET', 'list', list_data), '{"objects": ["a", "c", "b"]}') self.assertEqual(self.res.serialize('GET', 'detail', detail_data), '{"hello": "world"}') # The create special-case. self.assertEqual(self.res.serialize('POST', 'list', detail_data), '{"hello": "world"}') # Make sure other methods aren't special-cased. self.assertEqual(self.res.serialize('PUT', 'list', list_data), '{"objects": ["a", "c", "b"]}') def test_serialize_list(self): data = [ { 'title': 'Cosmos', 'author': 'Carl Sagan', 'short_desc': 'A journey through the stars by an emminent astrophysist.', 'pub_date': '1980', }, { 'title': "Hitchhiker's Guide To The Galaxy", 'author': 'Douglas Adams', 'short_desc': "Don't forget your towel.", 'pub_date': '1979', } ] self.res.preparer = FieldsPreparer(fields={ 'title': 'title', 'author': 'author', 'synopsis': 'short_desc', }) res = self.res.serialize_list(data) self.assertEqual(json.loads(res), { 'objects': [ { 'author': 'Carl Sagan', 'synopsis': 'A journey through the stars by an emminent astrophysist.', 'title': 'Cosmos' }, { 'title': "Hitchhiker's Guide To The Galaxy", 'author': 'Douglas Adams', 'synopsis': "Don't forget your towel.", }, ], }) # Make sure we don't try to serialize a ``None``, which would fail. self.assertEqual(self.res.serialize_list(None), '') def test_serialize_detail(self): # This isn't very unit-y, but we're also testing that we're using the # right JSON encoder & that it can handle other data types. data = { 'title': 'Cosmos', 'author': 'Carl Sagan', 'short_desc': 'A journey through the stars by an emminent astrophysist.', } self.res.preparer = FieldsPreparer(fields={ 'title': 'title', 'author': 'author', 'synopsis': 'short_desc', }) res = self.res.serialize_detail(data) self.assertEqual(json.loads(res), { 'author': 'Carl Sagan', 'synopsis': 'A journey through the stars by an emminent astrophysist.', 'title': 'Cosmos' }) # Make sure we don't try to serialize a ``None``, which would fail. self.assertEqual(self.res.serialize_detail(None), '') def test_prepare(self): # Without fields. data = { 'title': 'Cosmos', 'author': 'Carl Sagan', 'short_desc': 'A journey through the stars by an emminent astrophysist.', 'pub_date': '1980' } # Should be unmodified. self.assertTrue(isinstance(self.res.preparer, Preparer)) self.assertEqual(self.res.prepare(data), data) self.res.preparer = FieldsPreparer(fields={ 'title': 'title', 'author': 'author', 'synopsis': 'short_desc', }) self.assertEqual(self.res.prepare(data), { 'author': 'Carl Sagan', 'synopsis': 'A journey through the stars by an emminent astrophysist.', 'title': 'Cosmos' }) def test_wrap_list_response(self): data = ['one', 'three', 'two'] self.assertEqual(self.res.wrap_list_response(data), { 'objects': [ 'one', 'three', 'two', ], }) def test_is_authenticated(self): # By default, only GETs are allowed. self.assertTrue(self.res.is_authenticated()) self.res.request = FakeHttpRequest('POST') self.assertFalse(self.res.is_authenticated()) self.res.request = FakeHttpRequest('PUT') self.assertFalse(self.res.is_authenticated()) self.res.request = FakeHttpRequest('DELETE') self.assertFalse(self.res.is_authenticated()) self.res.handle('list') self.assertFalse(self.res.is_authenticated()) def test_list(self): with self.assertRaises(MethodNotImplemented): self.res.list() def test_detail(self): with self.assertRaises(MethodNotImplemented): self.res.detail() def test_create(self): with self.assertRaises(MethodNotImplemented): self.res.create() def test_update(self): with self.assertRaises(MethodNotImplemented): self.res.update() def test_delete(self): with self.assertRaises(MethodNotImplemented): self.res.delete() def test_update_list(self): with self.assertRaises(MethodNotImplemented): self.res.update_list() def test_create_detail(self): with self.assertRaises(MethodNotImplemented): self.res.create_detail() def test_delete_list(self): with self.assertRaises(MethodNotImplemented): self.res.delete_list() def test_endpoint_list(self): self.res.handle('list') self.assertEqual(self.res.endpoint, 'list') def test_endpoint_detail(self): self.res.handle('detail') self.assertEqual(self.res.endpoint, 'detail') def test_endpoint_create(self): self.res.handle('create') self.assertEqual(self.res.endpoint, 'create') def test_endpoint_update(self): self.res.handle('update') self.assertEqual(self.res.endpoint, 'update') def test_endpoint_delete(self): self.res.handle('delete') self.assertEqual(self.res.endpoint, 'delete') def test_endpoint_update_list(self): self.res.handle('update_list') self.assertEqual(self.res.endpoint, 'update_list') def test_endpoint_create_detail(self): self.res.handle('create_detail') self.assertEqual(self.res.endpoint, 'create_detail') def test_endpoint_delete_list(self): self.res.handle('delete_list') self.assertEqual(self.res.endpoint, 'delete_list') restless-2.1.1/tests/test_serializers.py000066400000000000000000000024301311406265400204670ustar00rootroot00000000000000import datetime from decimal import Decimal import unittest import uuid from restless.exceptions import BadRequest from restless.serializers import JSONSerializer class JSONSerializerTestCase(unittest.TestCase): def setUp(self): super(JSONSerializerTestCase, self).setUp() self.serializer = JSONSerializer() self.dict_data = { 'hello': 'world', 'abc': 123, 'more': { 'things': 'here', # Some data the usual JSON encoder can't handle... 'nested': datetime.datetime(2014, 3, 30, 12, 55, 15), 'again': Decimal('18.9'), 'uuid': uuid.uuid4() }, } def test_serialize(self): body = self.serializer.serialize(self.dict_data) self.assertTrue('"hello": "world"' in body) self.assertTrue('"abc": 123' in body) self.assertTrue('"nested": "2014-03-30T12:55:15"' in body) self.assertTrue('"again": "18.9"' in body) def test_deserialize(self): self.assertEqual(self.serializer.deserialize('{"more": "things"}'), { 'more': 'things', }) def test_deserialize_invalid(self): with self.assertRaises(BadRequest): self.serializer.deserialize('not valid!') restless-2.1.1/tests/test_tnd.py000066400000000000000000000243101311406265400167210ustar00rootroot00000000000000import unittest import socket import six from restless.utils import json from restless.constants import UNAUTHORIZED def _newer_or_equal_(v): for i in six.moves.xrange(min(len(v), len(version_info))): expected, tnd = v[i], version_info[i] if tnd > expected: return True elif tnd == expected: continue else: return False return True def _equal_(v): for i in six.moves.xrange(min(len(v), len(version_info))): if v[i] != version_info[i]: return False return True try: from restless.tnd import TornadoResource, _BridgeMixin from tornado import testing, web, httpserver, gen, version_info from tornado.iostream import IOStream if _newer_or_equal_((4, 0, 0, 0)): from tornado.http1connection import HTTP1Connection except ImportError: class testing: AsyncHTTPTestCase = object class web: @staticmethod def Application(*args, **kw): return False class gen: @staticmethod def coroutine(fn): return fn class TornadoResource: @staticmethod def as_list(): pass @staticmethod def as_detail(): pass class TndBaseTestResource(TornadoResource): """ base test resource, containing a fake-db """ fake_db = [] def __init__(self): # Just for testing. self.__class__.fake_db = [ {"id": "dead-beef", "title": 'First post'}, {"id": "de-faced", "title": 'Another'}, {"id": "bad-f00d", "title": 'Last'}, ] class TndBasicTestResource(TndBaseTestResource): """ containing several basic view_method """ def list(self): return self.fake_db def detail(self, pk): for item in self.fake_db: if item['id'] == pk: return item return None def create(self): self.fake_db.append(self.data) class TndAsyncTestResource(TndBaseTestResource): """ asynchronous basic view_method """ @gen.coroutine def list(self): raise gen.Return(self.fake_db) @gen.coroutine def detail(self, pk): for item in self.fake_db: if item['id'] == pk: raise gen.Return(item) raise gen.Return(None) @gen.coroutine def create(self): self.fake_db.append(self.data) app = web.Application([ (r'/fake', TndBasicTestResource.as_list()), (r'/fake/([^/]+)', TndBasicTestResource.as_detail()), (r'/fake_async', TndAsyncTestResource.as_list()), (r'/fake_async/([^/]+)', TndAsyncTestResource.as_detail()) ], debug=True) @unittest.skipIf(not app, 'Tornado is not available') class BaseHTTPTestCase(testing.AsyncHTTPTestCase): """ base of test case """ def get_app(self): return app @unittest.skipIf(not app, 'Tornado is not available') class TndResourceTestCase(BaseHTTPTestCase): """ """ def test_as_list(self): resp = self.fetch( '/fake', method='GET', follow_redirects=False ) self.assertEqual(resp.headers['Content-Type'], 'application/json; charset=UTF-8') self.assertEqual(resp.code, 200) self.assertEqual(json.loads(resp.body.decode('utf-8')), { 'objects': [ { 'id': 'dead-beef', 'title': 'First post' }, { 'id': 'de-faced', 'title': 'Another' }, { 'id': 'bad-f00d', 'title': 'Last' } ] }) def test_as_detail(self): resp = self.fetch( '/fake/de-faced', method='GET', follow_redirects=False ) self.assertEqual(resp.headers['Content-Type'], 'application/json; charset=UTF-8') self.assertEqual(resp.code, 200) self.assertEqual(json.loads(resp.body.decode('utf-8')), { 'id': 'de-faced', 'title': 'Another' }) def test_not_authenticated(self): resp = self.fetch( '/fake', method='POST', body='{"id": 7, "title": "Moved hosts"}', follow_redirects=False ) self.assertEqual(resp.code, UNAUTHORIZED) @unittest.skipIf(not app, 'Tornado is not available') class BaseTestCase(unittest.TestCase): """ test case that export the wrapped tornado.web.RequestHandler. """ def init_request_handler(self, rh_cls, view_type): global app if view_type == 'list': rq = rh_cls.as_list() elif view_type == 'detail': rq = rh_cls.as_detail() # compose a fake incoming request fake_connection = None # after tornado 4.1, it's not allowed to build a RequestHandler without a connection. if _newer_or_equal_((4, 0, 0, 0)): ios = IOStream(socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)) context = None # there is a bug in these 2 version that would fail when # context is None if _equal_((4, 0, 1)) or _equal_((4, 0, 2)): context = httpserver._HTTPRequestContext(ios, None, None) fake_connection = HTTP1Connection(ios, False, context=context) fake_request = httpserver.HTTPRequest('GET', '/fake', body='test123', connection=fake_connection) self.new_handler = rq(app, fake_request) @unittest.skipIf(not app, 'Tornado is not available') class InternalTestCase(BaseTestCase): """ test-cases that check internal structure of the wrapped tornado.web.RequestHandler """ def setUp(self): self.init_request_handler(TndBasicTestResource, 'list') def test_is_debug(self): ori_debug = app.settings['debug'] app.settings['debug'] = False self.assertEqual(self.new_handler.resource_handler.is_debug(), False) app.settings['debug'] = True self.assertEqual(self.new_handler.resource_handler.is_debug(), True) app.settings['debug'] = ori_debug def test_body(self): self.assertEqual(self.new_handler.resource_handler.request_body(), 'test123') def test_method(self): self.assertEqual(self.new_handler.resource_handler.request_method(), 'GET') def test_class(self): """ test the generated tornado.web.RequestHandler """ self.assertEqual(self.new_handler.__class__.__name__, 'TndBasicTestResource__BridgeMixin_restless') self.assertTrue(_BridgeMixin in self.new_handler.__class__.__mro__) self.assertTrue(web.RequestHandler in self.new_handler.__class__.__mro__) def test_var(self): """ make sure variable from tornado is correctly passed. """ self.assertTrue(hasattr(self.new_handler.resource_handler, 'request')) self.assertTrue(hasattr(self.new_handler.resource_handler, 'application')) @unittest.skipIf(not app, 'Tornado is not available') class TndDeleteTestResource(TndBasicTestResource): """ testing inherited resource """ def delete(self, pk): self.fake_db = filter(lambda x: x['id'] != pk, self.fake_db) def delete_list(self): self.fake_db = {} @unittest.skipIf(not app, 'Tornado is not available') class FuncTrimTestCase(BaseTestCase): """ test-cases that make sure we removed unnecessary handler functions of the wrapped tornado.web.RequestHandler """ def test_empty_resource(self): self.init_request_handler(TndBaseTestResource, 'list') self.assertNotIn('post', self.new_handler.__class__.__dict__) self.assertNotIn('get', self.new_handler.__class__.__dict__) self.assertNotIn('delete', self.new_handler.__class__.__dict__) self.assertNotIn('put', self.new_handler.__class__.__dict__) def test_basic_resource_list(self): self.init_request_handler(TndBasicTestResource, 'list') self.assertIn('post', self.new_handler.__class__.__dict__) self.assertIn('get', self.new_handler.__class__.__dict__) self.assertNotIn('delete', self.new_handler.__class__.__dict__) self.assertNotIn('put', self.new_handler.__class__.__dict__) def test_basic_resource_detail(self): self.init_request_handler(TndBasicTestResource, 'detail') self.assertNotIn('post', self.new_handler.__class__.__dict__) self.assertIn('get', self.new_handler.__class__.__dict__) self.assertNotIn('delete', self.new_handler.__class__.__dict__) self.assertNotIn('put', self.new_handler.__class__.__dict__) def test_inheritance_resource_detail(self): self.init_request_handler(TndDeleteTestResource, 'detail') self.assertNotIn('post', self.new_handler.__class__.__dict__) self.assertIn('get', self.new_handler.__class__.__dict__) self.assertIn('delete', self.new_handler.__class__.__dict__) self.assertNotIn('put', self.new_handler.__class__.__dict__) @unittest.skipIf(not app, 'Tornado is not available') class TndAsyncResourceTestCase(BaseHTTPTestCase): """ test asynchronous view_method """ def test_as_list(self): resp = self.fetch( '/fake_async', method='GET', follow_redirects=False ) self.assertEqual(resp.headers['Content-Type'], 'application/json; charset=UTF-8') self.assertEqual(resp.code, 200) self.assertEqual(json.loads(resp.body.decode('utf-8')), { 'objects': [ { 'id': 'dead-beef', 'title': 'First post' }, { 'id': 'de-faced', 'title': 'Another' }, { 'id': 'bad-f00d', 'title': 'Last' } ] }) def test_as_detail(self): resp = self.fetch( '/fake_async/de-faced', method='GET', follow_redirects=False ) self.assertEqual(resp.headers['Content-Type'], 'application/json; charset=UTF-8') self.assertEqual(resp.code, 200) self.assertEqual(json.loads(resp.body.decode('utf-8')), { 'id': 'de-faced', 'title': 'Another' }) restless-2.1.1/tests/test_utils.py000066400000000000000000000012051311406265400172720ustar00rootroot00000000000000import sys import unittest from restless.utils import format_traceback class FormatTracebackTestCase(unittest.TestCase): def test_format_traceback(self): try: raise ValueError("Because we need an exception.") except: exc_info = sys.exc_info() result = format_traceback(exc_info) self.assertTrue(result.startswith('Traceback (most recent call last):\n')) self.assertFalse(result.endswith('\n')) lines = result.split('\n') self.assertTrue(len(lines) > 3) self.assertEqual(lines[-1], 'ValueError: Because we need an exception.') restless-2.1.1/tox.ini000066400000000000000000000013071311406265400146750ustar00rootroot00000000000000[tox] envlist = py{27,33,34,35,py2}-{dj18} py{27,34,35,py2}-{dj19,dj110,dj111} py{36}-{dj111} [testenv] basepython = py27: python2.7 py33: python3.3 py34: python3.4 py35: python3.5 py36: python3.6 pypy2: pypy deps = six pytest pytest-cov WebOb>=1.3.1,<1.7 Pyramid<1.8 tornado py{27,33,34,35}: Flask>=0.10 dj18: Django>=1.8,<1.9 dj19: Django>=1.9,<1.10 dj110: Django>=1.10,<1.11 dj111: Django>=1.11,<1.12 commands = py.test --cov=restless [travis] python = 2.7: py27 3.3: py33 3.4: py34 3.5: py35 3.6: py36 pypy: pypy2 [travis:env] DJANGO = 1.8: dj18 1.9: dj19 1.10: dj110 1.11: dj111