├── .gitignore ├── .travis.yml ├── CONTRIBUTORS ├── ChangeLog ├── LICENSE ├── README.md ├── doc ├── build │ └── .placeholder └── source │ ├── _static │ └── .placeholder │ ├── _templates │ └── .placeholder │ ├── api.rst │ ├── changelog.rst │ ├── conf.py │ ├── contributors.rst │ ├── customization.rst │ ├── debugging.rst │ ├── index.rst │ ├── installation.rst │ ├── license.rst │ ├── quickstart.rst │ ├── tastypie.rst │ └── usage.rst ├── drest ├── __init__.py ├── api.py ├── exc.py ├── interface.py ├── meta.py ├── request.py ├── resource.py ├── response.py ├── serialization.py └── testing.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── api_tests.py ├── exc_tests.py ├── interface_tests.py ├── meta_tests.py ├── request_tests.py ├── resource_tests.py ├── response_tests.py └── serialization_tests.py └── utils ├── bump-version.sh ├── drest.mockapi ├── mockapi │ ├── __init__.py │ ├── api.py │ ├── fixtures │ │ └── initial_data.json │ ├── manage.py │ ├── projects │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── settings.py │ └── urls.py ├── requirements.txt └── setup.py ├── make-release.sh ├── run-mockapi.sh ├── run-tests.sh ├── travis-bootstrap.sh └── travis.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | doc/build/* 29 | coverage_report/ 30 | test.py 31 | .idea/ 32 | utils/drest.mockapi/src/ 33 | mockapi.err 34 | mockapi.out 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | script: ./utils/travis.sh 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | - "3.2" 7 | - "3.3" 8 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | * BJ Dierkes (Creator/Primary Developer) 5 | * Andrew Alcock 6 | * Zenobius Jiricek 7 | * Oliver Drake 8 | * Rodrigo Reis 9 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2 | ChangeLog 3 | ============================================================================== 4 | 5 | All bugs/feature details can be found at: 6 | 7 | https://github.com/derks/drest/issues/XXXXX 8 | 9 | 10 | Where XXXXX is the 'Issue #' referenced below. Additionally, this change log 11 | is available online at: 12 | 13 | http://drest.readthedocs.org/en/latest/changelog.html 14 | 15 | .. raw:: html 16 | 17 |

18 | 19 | 20 | 0.9.13 - development (will be released as 0.9.14 or 1.0.0) 21 | ------------------------------------------------------------------------------ 22 | 23 | Bugs: 24 | 25 | * None 26 | 27 | Features: 28 | 29 | * Display params after they are seralized/encoded in debug output. 30 | 31 | 32 | 0.9.12 - Nov 12, 2013 33 | ------------------------------------------------------------------------------ 34 | 35 | Bugs: 36 | 37 | * :issue:`26` - Fixed incorrect reference to serialiser in 38 | TastyPieResourceHandler 39 | 40 | Features: 41 | 42 | * :issue:`23` - Ability to set request timeout. Default is to not timeout 43 | * :issue:`24` - Allow suppression of body in GET requests. Default is 44 | to not send the `body` in GET requests. 45 | * :issue:`25` - Added support for `patch_list` in TastyPieResourceHandler 46 | 47 | 48 | 0.9.10 - Jul 18, 2012 49 | ------------------------------------------------------------------------------ 50 | 51 | Bugs: 52 | 53 | * :issue:`20` - Should catch ServerNotFoundError 54 | 55 | Feature Enhancements: 56 | 57 | * :issue:`17` - Use relative imports to make drest more portable 58 | * :issue:`19` - Added PATCH support for default REST resource. 59 | 60 | 61 | 0.9.8 - Jul 02, 2012 62 | ------------------------------------------------------------------------------ 63 | 64 | Bug Fixes: 65 | 66 | * :issue:`12` - Params are not added correctly to GET request 67 | 68 | Feature Enhancements: 69 | 70 | * :issue:`15` - dRestResponse object should include response headers 71 | 72 | 73 | 0.9.6 - Mar 23, 2012 74 | ------------------------------------------------------------------------------ 75 | 76 | Bug Fixes: 77 | 78 | - :issue:`9` - GET params incorrectly stored as permanent 79 | _extra_url_params. 80 | 81 | Feature Enhancements: 82 | 83 | - :issue:`4` - Better support for nested resource names. 84 | - :issue:`5`, :issue:`8` - Request object is now exposed publicly. 85 | - :issue:`6` - Add capability to suppress final forward-slash 86 | - :issue:`7` - Cache http object for improved performance. 87 | 88 | Incompatible Changes: 89 | 90 | - api._request is now api.request. api.request (old function) is now 91 | api.make_request() 92 | 93 | - Lots of code refactoring.. numerous minor changes may break 94 | compatibility if using backend functions, but not likely if accessing 95 | the high level api functions. 96 | 97 | - Every request now returns a drest.response.ResponseHandler object 98 | rather than a (response, data) tuple. 99 | 100 | 101 | 0.9.4 - Feb 16, 2012 102 | ------------------------------------------------------------------------------ 103 | 104 | Bug Fixes: 105 | 106 | - :issue:`3` - TypeError: object.__init__() takes no parameters 107 | 108 | Feature Enhancements: 109 | 110 | - Improved test suite, now powered by Django TastyPie! 111 | - Added support for Basic HTTP Auth. 112 | 113 | Incompatible Changes: 114 | 115 | - drest.api.API.auth() now implements Basic HTTP Auth by default rather 116 | than just appending the user/password/etc to the URL. 117 | 118 | 119 | .. raw:: html 120 | 121 |

122 | 123 | 0.9.2 - Feb 01, 2012 124 | ------------------------------------------------------------------------------ 125 | 126 | - Initial Beta release. Future versions will detail bugs/features/etc. 127 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | Copyright (c) 2011-2013, Data Folk Labs, LLC 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of BJ Dierkes nor the names of its contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dRest HTTP/REST Client Library for Python 2 | ========================================= 3 | 4 | dRest is a configurable HTTP/REST client library for Python. It's goal is to 5 | make the creation of API clients dead simple, without lacking features. 6 | 7 | [![Continuous Integration Status](https://secure.travis-ci.org/datafolklabs/drest.png)](http://travis-ci.org/datafolklabs/drest/) 8 | 9 | Features include: 10 | 11 | * Light-weight API Client Library, implementing REST by default 12 | * Native support for the Django TastyPie API Framework 13 | * Only one external dependency on httplib2 14 | * Key pieces of the library are customizable by defined handlers 15 | * Interface definitions ensure handlers are properly implemented 16 | * Tested against Python versions 2.6, 2.7, 3.2, 3.3 17 | * 100% test coverage via Nose 18 | 19 | More Information 20 | ---------------- 21 | 22 | * RTFD: http://drest.rtfd.org/ 23 | * CODE: http://github.com/datafolklabs/drest/ 24 | * PYPI: http://pypi.python.org/pypi/drest/ 25 | * T-CI: http://travis-ci.org/datafolklabs/drest/ 26 | 27 | Usage 28 | ----- 29 | 30 | ```python 31 | import drest 32 | 33 | # Create a generic client api object 34 | api = drest.API('http://localhost:8000/api/v1/') 35 | 36 | # Make calls openly via any HTTP Method, and any path 37 | # GET http://localhost:8000/api/v1/users/1/ 38 | response = api.make_request('GET', '/users/1/') 39 | 40 | # Or attach a resource 41 | api.add_resource('users') 42 | 43 | # Get available resources 44 | api.resources 45 | >>> ['users', 'projects', 'etc'] 46 | 47 | # Get all objects of a resource 48 | # GET http://localhost:8000/api/v1/users/ 49 | response = api.users.get() 50 | 51 | # Get a single resource with primary key '1' 52 | # GET http://localhost:8000/api/v1/users/1/ 53 | response = api.users.get(1) 54 | 55 | # Create a resource data dictionary 56 | user_data = dict( 57 | username='john.doe', 58 | password='oober-secure-password', 59 | first_name='John', 60 | last_name='Doe', 61 | ) 62 | 63 | # POST http://localhost:8000/api/v1/users/ 64 | response = api.users.post(user_data) 65 | 66 | # Update a resource with primary key '1' 67 | response = api.users.get(1) 68 | updated_data = response.data.copy() 69 | updated_data['first_name'] = 'John' 70 | updated_data['last_name'] = 'Doe' 71 | 72 | # PUT http://localhost:8000/api/v1/users/1/ 73 | response = api.users.put(1, updated_data) 74 | 75 | # Patch a resource with primary key '1' 76 | # PATCH http://localhost:8000/api/v1/users/1/ 77 | response = api.users.patch(1, dict(first_name='Johnny')) 78 | 79 | # Delete a resource with primary key '1' 80 | # DELETE http://localhost:8000/api/v1/users/1/ 81 | response = api.users.delete(1) 82 | 83 | # Get the status of the request 84 | response.status 85 | 86 | # Or the data returned by the request 87 | response.data 88 | 89 | # Or the headers returned by the request 90 | response.headers 91 | ``` 92 | 93 | License 94 | ------- 95 | 96 | The dRest library is Open Source and is distributed under the BSD License 97 | (three clause). Please see the LICENSE file included with this software. 98 | -------------------------------------------------------------------------------- /doc/build/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datafolklabs/drest/12b053bced20102c8a9986e5621b78688720dd6e/doc/build/.placeholder -------------------------------------------------------------------------------- /doc/source/_static/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datafolklabs/drest/12b053bced20102c8a9986e5621b78688720dd6e/doc/source/_static/.placeholder -------------------------------------------------------------------------------- /doc/source/_templates/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datafolklabs/drest/12b053bced20102c8a9986e5621b78688720dd6e/doc/source/_templates/.placeholder -------------------------------------------------------------------------------- /doc/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. _drest.api: 5 | 6 | :mod:`drest.api` 7 | ---------------- 8 | 9 | .. automodule:: drest.api 10 | :members: 11 | 12 | .. _drest.exc: 13 | 14 | :mod:`drest.exc` 15 | ---------------- 16 | 17 | .. automodule:: drest.exc 18 | :members: 19 | 20 | .. _drest.interface: 21 | 22 | :mod:`drest.interface` 23 | ---------------------- 24 | 25 | .. automodule:: drest.interface 26 | :members: 27 | 28 | .. _drest.meta: 29 | 30 | :mod:`drest.meta` 31 | ----------------- 32 | 33 | .. automodule:: drest.meta 34 | :members: 35 | 36 | .. _drest.request: 37 | 38 | :mod:`drest.request` 39 | -------------------- 40 | 41 | .. automodule:: drest.request 42 | :members: 43 | 44 | :mod:`drest.response` 45 | --------------------- 46 | 47 | .. automodule:: drest.response 48 | :members: 49 | 50 | .. _drest.response: 51 | 52 | :mod:`drest.resource` 53 | --------------------- 54 | 55 | .. automodule:: drest.resource 56 | :members: 57 | 58 | .. _drest.serialization: 59 | 60 | :mod:`drest.serialization` 61 | -------------------------- 62 | 63 | .. automodule:: drest.serialization 64 | :members: 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /doc/source/changelog.rst: -------------------------------------------------------------------------------- 1 | ../../ChangeLog -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # dRest documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Dec 16 19:22:53 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | VERSION = '0.9' 17 | RELEASE = '0.9.13' 18 | 19 | sys.path.insert(0, os.path.abspath('../../src/drest/')) 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ----------------------------------------------------- 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be extensions 32 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.extlinks', 36 | ] 37 | 38 | extlinks = {'issue' : ('https://github.com/derks/drest/issues/%s', 'Issue #')} 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | #source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = 'dRest' 54 | copyright = '2012, BJ Dierkes' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = VERSION 62 | 63 | # The full version, including alpha/beta/rc tags. 64 | release = RELEASE 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | #language = None 69 | 70 | # There are two options for replacing |today|: either, you set today to some 71 | # non-false value, then it is used: 72 | #today = '' 73 | # Else, today_fmt is used as the format for a strftime call. 74 | #today_fmt = '%B %d, %Y' 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | exclude_patterns = [] 79 | 80 | # The reST default role (used for this markup: `text`) to use for all documents. 81 | #default_role = None 82 | 83 | # If true, '()' will be appended to :func: etc. cross-reference text. 84 | #add_function_parentheses = True 85 | 86 | # If true, the current module name will be prepended to all description 87 | # unit titles (such as .. function::). 88 | #add_module_names = True 89 | 90 | # If true, sectionauthor and moduleauthor directives will be shown in the 91 | # output. They are ignored by default. 92 | #show_authors = False 93 | 94 | # The name of the Pygments (syntax highlighting) style to use. 95 | pygments_style = 'sphinx' 96 | 97 | # A list of ignored prefixes for module index sorting. 98 | #modindex_common_prefix = [] 99 | 100 | 101 | # -- Options for HTML output --------------------------------------------------- 102 | 103 | # The theme to use for HTML and HTML Help pages. See the documentation for 104 | # a list of builtin themes. 105 | html_theme = 'nature' 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | #html_theme_options = {} 111 | 112 | # Add any paths that contain custom themes here, relative to this directory. 113 | #html_theme_path = [] 114 | 115 | # The name for this set of Sphinx documents. If None, it defaults to 116 | # " v documentation". 117 | #html_title = None 118 | 119 | # A shorter title for the navigation bar. Default is the same as html_title. 120 | #html_short_title = None 121 | 122 | # The name of an image file (relative to this directory) to place at the top 123 | # of the sidebar. 124 | #html_logo = None 125 | 126 | # The name of an image file (within the static path) to use as favicon of the 127 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 128 | # pixels large. 129 | #html_favicon = None 130 | 131 | # Add any paths that contain custom static files (such as style sheets) here, 132 | # relative to this directory. They are copied after the builtin static files, 133 | # so a file named "default.css" will overwrite the builtin "default.css". 134 | html_static_path = ['_static'] 135 | 136 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 137 | # using the given strftime format. 138 | #html_last_updated_fmt = '%b %d, %Y' 139 | 140 | # If true, SmartyPants will be used to convert quotes and dashes to 141 | # typographically correct entities. 142 | #html_use_smartypants = True 143 | 144 | # Custom sidebar templates, maps document names to template names. 145 | #html_sidebars = {} 146 | 147 | # Additional templates that should be rendered to pages, maps page names to 148 | # template names. 149 | #html_additional_pages = {} 150 | 151 | # If false, no module index is generated. 152 | #html_domain_indices = True 153 | 154 | # If false, no index is generated. 155 | #html_use_index = True 156 | 157 | # If true, the index is split into individual pages for each letter. 158 | #html_split_index = False 159 | 160 | # If true, links to the reST sources are added to the pages. 161 | #html_show_sourcelink = True 162 | 163 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 164 | #html_show_sphinx = True 165 | 166 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 167 | #html_show_copyright = True 168 | 169 | # If true, an OpenSearch description file will be output, and all pages will 170 | # contain a tag referring to it. The value of this option must be the 171 | # base URL from which the finished HTML is served. 172 | #html_use_opensearch = '' 173 | 174 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 175 | #html_file_suffix = None 176 | 177 | # Output file base name for HTML help builder. 178 | htmlhelp_basename = 'dRestdoc' 179 | 180 | 181 | # -- Options for LaTeX output -------------------------------------------------- 182 | 183 | latex_elements = { 184 | # The paper size ('letterpaper' or 'a4paper'). 185 | #'papersize': 'letterpaper', 186 | 187 | # The font size ('10pt', '11pt' or '12pt'). 188 | #'pointsize': '10pt', 189 | 190 | # Additional stuff for the LaTeX preamble. 191 | #'preamble': '', 192 | } 193 | 194 | # Grouping the document tree into LaTeX files. List of tuples 195 | # (source start file, target name, title, author, documentclass [howto/manual]). 196 | latex_documents = [ 197 | ('index', 'dRest.tex', 'dRest Documentation', 198 | 'BJ Dierkes', 'manual'), 199 | ] 200 | 201 | # The name of an image file (relative to this directory) to place at the top of 202 | # the title page. 203 | #latex_logo = None 204 | 205 | # For "manual" documents, if this is true, then toplevel headings are parts, 206 | # not chapters. 207 | #latex_use_parts = False 208 | 209 | # If true, show page references after internal links. 210 | #latex_show_pagerefs = False 211 | 212 | # If true, show URL addresses after external links. 213 | #latex_show_urls = False 214 | 215 | # Documents to append as an appendix to all manuals. 216 | #latex_appendices = [] 217 | 218 | # If false, no module index is generated. 219 | #latex_domain_indices = True 220 | 221 | 222 | # -- Options for manual page output -------------------------------------------- 223 | 224 | # One entry per manual page. List of tuples 225 | # (source start file, name, description, authors, manual section). 226 | man_pages = [ 227 | ('index', 'drest', 'dRest Documentation', 228 | ['BJ Dierkes'], 1) 229 | ] 230 | 231 | # If true, show URL addresses after external links. 232 | #man_show_urls = False 233 | 234 | 235 | # -- Options for Texinfo output ------------------------------------------------ 236 | 237 | # Grouping the document tree into Texinfo files. List of tuples 238 | # (source start file, target name, title, author, 239 | # dir menu entry, description, category) 240 | texinfo_documents = [ 241 | ('index', 'dRest', 'dRest Documentation', 242 | 'BJ Dierkes', 'dRest', 'One line description of project.', 243 | 'Miscellaneous'), 244 | ] 245 | 246 | # Documents to append as an appendix to all manuals. 247 | #texinfo_appendices = [] 248 | 249 | # If false, no module index is generated. 250 | #texinfo_domain_indices = True 251 | 252 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 253 | #texinfo_show_urls = 'footnote' 254 | -------------------------------------------------------------------------------- /doc/source/contributors.rst: -------------------------------------------------------------------------------- 1 | ../../CONTRIBUTORS -------------------------------------------------------------------------------- /doc/source/customization.rst: -------------------------------------------------------------------------------- 1 | Customizing dRest 2 | ================= 3 | 4 | Every piece of dRest is completely customizable by way of 'handlers'. 5 | 6 | API Reference: 7 | 8 | * :mod:`drest.api` 9 | * :mod:`drest.resource` 10 | * :mod:`drest.request` 11 | * :mod:`drest.response` 12 | * :mod:`drest.serialization` 13 | 14 | Example 15 | ------- 16 | 17 | The following is just a quick glance at what it would look to chain together 18 | a custom Serialization Handler, Request Handler, Resource Handler, and 19 | finally a custom API client object. This is not meant to be comprehensive 20 | by any means. In the real world, you will need to read the source code 21 | documentation listed above and determine a) what you need to customize, and 22 | b) what functionality you need to maintain. 23 | 24 | .. code-block:: python 25 | 26 | import drest 27 | 28 | class MySerializationHandler(drest.serialization.SerializationHandler): 29 | def serialize(self, data_dict): 30 | # do something to serialize data dictionary 31 | pass 32 | 33 | def deserialize(self, serialized_data): 34 | # do something to deserialize data 35 | pass 36 | 37 | class MyResponseHandler(drest.response.ResponseHandler) 38 | def __init__(self, status, data, **kw): 39 | super(MyResponseHandler, self).__init__(status, data, **kw) 40 | # do something to customize the response handler 41 | 42 | class MyRequestHandler(drest.request.RequestHandler): 43 | class Meta: 44 | serialization_handler = MySerializationHandler 45 | response_handler = MyResponseHandler 46 | 47 | def handle_response(self, response): 48 | # do something to wrape every response 49 | pass 50 | 51 | class MyResourceHandler(drest.resource.ResourceHandler): 52 | class Meta: 53 | request_handler = MyRequestHandler 54 | 55 | def some_custom_function(self, params=None): 56 | if params is None: 57 | params = {} 58 | # do some kind of custom api call 59 | return self.request('GET', '/users/some_custom_function', params) 60 | 61 | class MyAPI(drest.api.API): 62 | class Meta: 63 | baseurl = 'http://example.com/api/v1/' 64 | resource_handler = MyResourceHandler 65 | request_handler = MyRequestHandler 66 | 67 | def auth(self, *args, **kw): 68 | # do something to customize authentication 69 | pass 70 | 71 | api = MyAPI() 72 | 73 | # Add resources 74 | api.add_resource('users') 75 | api.add_resource('projects') 76 | 77 | # GET http://example.com/api/v1/users/ 78 | api.users.get() 79 | 80 | # GET http://example.com/api/v1/users/133/ 81 | api.users.get(133) 82 | 83 | # PUT http://example.com/api/v1/users/133/ 84 | api.users.put(133, data_dict) 85 | 86 | # POST http://example.com/api/v1/users/ 87 | api.users.post(data_dict) 88 | 89 | # DELETE http://example.com/api/v1/users/133/ 90 | api.users.delete(133) 91 | 92 | # GET http://example.com/api/v1/users/some_custom_function/ 93 | api.users.some_custom_function() 94 | 95 | Note that the id '133' above is the fictitious id of a user resource. 96 | -------------------------------------------------------------------------------- /doc/source/debugging.rst: -------------------------------------------------------------------------------- 1 | Debugging Requests 2 | ================== 3 | 4 | Often times in development, making calls to an API can be obscure and 5 | difficult to work with especially when receiving 500 Internal Server Errors 6 | with no idea what happens. In browser development, most frameworks like 7 | Django or the like provide some sort of debugging interface allowing 8 | developers to analyze tracebacks, and what not. Not so much when developing 9 | command line apps or similar. 10 | 11 | Enabling Debug Output 12 | --------------------- 13 | 14 | In order to enable DEBUG output for every request, simply set the 'DREST_DEBUG' 15 | environment variable to 1: 16 | 17 | .. code-block:: text 18 | 19 | $ set DREST_DEBUG=1 20 | 21 | $ python test.py 22 | DREST_DEBUG: method=POST url=http://localhost:8000/api/v0/systems/ params={} headers={'Content-Type': 'application/json', 'Authorization': 'ApiKey john.doe:XXXXXXXXXXXX'} 23 | 24 | In the above, test.py just made a simple api.system.post() call which 25 | triggered DREST_DEBUG output. In the output you have access to a number of 26 | things: 27 | 28 | method 29 | This is the method used to make the request. 30 | 31 | url 32 | The full url path of the request 33 | 34 | params 35 | Any parameters passed with the request 36 | 37 | headers 38 | Any headers passed with the request 39 | 40 | Once done debugging, just disable DREST_DEBUG: 41 | 42 | .. code-block:: text 43 | 44 | $ unset DREST_DEBUG 45 | 46 | Viewing Upstream Tracebacks 47 | --------------------------- 48 | 49 | If the error is happening server side, like a 500 Internal Server Error, you 50 | will likely receive a traceback in the return content (at least during 51 | development). This of course depends on the API you are developing against, 52 | however the following is common practice in development: 53 | 54 | .. code-block:: python 55 | 56 | try: 57 | response = api.my_resource.get() 58 | except drest.exc.dRestRequestError as e: 59 | print e.response.status 60 | print e.response.data 61 | print e.response.headers 62 | 63 | The above gives you the response object, as well as the content (data)... this 64 | is useful because the exception is triggered in drest code and not your own 65 | (therefore bringing the response object back down the stack where you can 66 | use it). 67 | 68 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. dRest documentation master file, created by 2 | sphinx-quickstart on Fri Dec 16 19:22:53 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | dRest Documentation 7 | =================== 8 | 9 | dRest is a configurable HTTP/REST client library for Python. It's goal is to 10 | make the creation of API clients dead simple, without lacking features. 11 | 12 | .. image:: https://secure.travis-ci.org/datafolklabs/drest.png 13 | :target: http://travis-ci.org/#!/datafolklabs/drest 14 | 15 | Key Features: 16 | 17 | * Light-weight API Client Library, implementing REST by default 18 | * Native support for the Django TastyPie API Framework 19 | * Only one external dependency on httplib2 20 | * Key pieces of the library are customizable by defined handlers 21 | * Interface definitions ensure handlers are properly implemented 22 | * Tested against all major versions of Python versions 2.6 through 3.2 23 | * 100% test coverage 24 | 25 | Additional Links: 26 | 27 | * RTFD: `http://drest.rtfd.org/ `_ 28 | * CODE: `http://github.com/datafolklabs/drest/ `_ 29 | * PYPI: `http://pypi.python.org/pypi/drest/ `_ 30 | * T-CI: `http://travis-ci.org/#!/datafolklabs/drest `_ 31 | * HELP: drest@librelist.org 32 | 33 | Contents: 34 | 35 | .. toctree:: 36 | :maxdepth: 1 37 | 38 | license 39 | changelog 40 | contributors 41 | api 42 | 43 | .. toctree:: 44 | :maxdepth: 2 45 | 46 | usage 47 | 48 | 49 | 50 | 51 | Indices and tables 52 | ================== 53 | 54 | * :ref:`genindex` 55 | * :ref:`modindex` 56 | * :ref:`search` 57 | 58 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | The following outlines installation of dRest. It is recommended to work out 5 | of a `VirtualENV `_ 6 | for development, which is reference throughout this documentation. VirtualENV 7 | is easily installed on most platforms either with 'easy_install' or 'pip' or 8 | via your OS distributions packaging system (yum, apt, brew, etc). 9 | 10 | Creating a Virtual Environment 11 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | 13 | .. code-block:: text 14 | 15 | $ virtualenv --no-site-packages ~/env/drest/ 16 | 17 | $ source ~/env/drest/bin/activate 18 | 19 | 20 | When installing drest, ensure that your development environment is active 21 | by sourcing the activate script (as seen above). 22 | 23 | 24 | Installing Stable Versions From PyPi 25 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 26 | 27 | .. code-block:: text 28 | 29 | (drest) $ pip install drest 30 | 31 | 32 | Installing Development Version From Git 33 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 34 | 35 | .. code-block:: text 36 | 37 | (drest) $ pip install -e git+git://github.com/derks/drest.git#egg=drest 38 | 39 | 40 | Running Unit Tests in Development 41 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 42 | 43 | To run tests, do the following from the 'root' directory of the drest source: 44 | 45 | .. code-block:: text 46 | 47 | (drest) $ ./utils/run-tests.sh 48 | 49 | 50 | For Python 3 testing, you will need to run 'drest.mockapi' manually via a 51 | seperate virtualenv setup for Python 2.6+ (in a separate terminal), and then 52 | run the test suite with the option '--without-mockapi': 53 | 54 | Terminal 1: 55 | 56 | .. code-block:: text 57 | 58 | $ virtualenv-2.7 ~/env/drest-py27/ 59 | 60 | $ source ~/env/drest-py27/bin/activate 61 | 62 | (drest-py27) $ ./utils/run-mockapi.sh 63 | 64 | 65 | Terminal 2: 66 | 67 | .. code-block:: text 68 | 69 | $ virtualenv-3.2 ~/env/drest-py32/ 70 | 71 | $ source ~/env/drest-py32/bin/activate 72 | 73 | (drest-py32) $ ./utils/run-tests.sh --without-mockapi 74 | -------------------------------------------------------------------------------- /doc/source/license.rst: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /doc/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart Guide 2 | ================ 3 | 4 | A REST Client Example 5 | --------------------- 6 | 7 | Note that the following is all fictitious data. What is received from and 8 | sent to an API is unique to every API. Do not copy and paste these examples. 9 | 10 | Connecting with an API 11 | ^^^^^^^^^^^^^^^^^^^^^^ 12 | 13 | .. code-block:: python 14 | 15 | import drest 16 | api = drest.API('http://localhost:8000/api/v1/') 17 | 18 | Authentication 19 | ^^^^^^^^^^^^^^ 20 | 21 | By default, drest.api.API.auth() implements HTTP Basic Authentication. This 22 | is generally overridden however by specific API's that subclass from api.API(). 23 | 24 | .. code-block:: python 25 | 26 | api.auth('john.doe', 'my_password') 27 | 28 | 29 | Note that authentication may not be necessary for your use case, or for 30 | read-only API's. 31 | 32 | Making Requests 33 | ^^^^^^^^^^^^^^^ 34 | 35 | Requests can be made openly by specifying the method 36 | (GET, PUT, POST, DELETE, ...), as well as the path (after the baseurl). 37 | 38 | .. code-block:: python 39 | 40 | # GET http://localhost:8000/api/v1/users/1/ 41 | response = api.make_request('GET', '/users/1/') 42 | 43 | Additionally, you can add a resource which makes access to the API more 44 | native and programatic. 45 | 46 | .. code-block:: python 47 | 48 | # Add a basic resource (assumes path='/users/') 49 | api.add_resource('users') 50 | 51 | # A list of available resources is available at: 52 | api.resources 53 | 54 | # GET http://localhost:8000/api/v1/users/ 55 | response = api.users.get() 56 | 57 | # GET http://localhost:8000/api/v1/users/1/ 58 | response = api.users.get(1) 59 | 60 | 61 | Creating a resource only requires a dictionary of 'parameters' passed to the 62 | resource: 63 | 64 | .. code-block:: python 65 | 66 | user_data = dict( 67 | username='john.doe', 68 | password='oober-secure-password', 69 | first_name='John', 70 | last_name='Doe', 71 | ) 72 | 73 | # POST http://localhost:8000/api/v1/users/ 74 | response = api.users.post(user_data) 75 | 76 | Updating a resource is as easy as requesting data for it, modifying it, and 77 | sending it back 78 | 79 | .. code-block:: python 80 | 81 | response = api.users.get(1) 82 | updated_data = response.data.copy() 83 | updated_data['first_name'] = 'John' 84 | updated_data['last_name'] = 'Doe' 85 | 86 | # PUT http://localhost:8000/api/v1/users/1/ 87 | response = api.users.put(1, updated_data) 88 | 89 | Or you can simply 'PATCH' a resource: 90 | 91 | .. code-block:: python 92 | 93 | # PATCH http://localhost:8000/api/v1/users/1/ 94 | response = api.users.patch(1, dict(first_name='Johnny')) 95 | 96 | Deleting a resource simply requires the primary key: 97 | 98 | .. code-block:: python 99 | 100 | # DELETE http://localhost:8000/api/v1/users/1/ 101 | response = api.users.delete(1) 102 | 103 | 104 | Working With Return Data 105 | ------------------------ 106 | 107 | Every call to an API by default returns a drest.response.ResponseHandler 108 | object. The two most useful members of this object are: 109 | 110 | * response.status (http status code) 111 | * response.data (the data returned by the api) 112 | * response.headers (the headers dictionary returned by the request) 113 | 114 | If a serialization handler is used, then response.data will be the 115 | unserialized form (Python dict). 116 | 117 | The Response Object 118 | ^^^^^^^^^^^^^^^^^^^ 119 | 120 | .. code-block:: python 121 | 122 | response = api.users.get() 123 | response.status # 200 124 | response.data # dict 125 | response.headers # dict 126 | 127 | 128 | Developers can base conditions on the status of the response (or other 129 | fields): 130 | 131 | .. code-block:: python 132 | 133 | response = api.users.get() 134 | if response.status != 200: 135 | print "Uhoh.... we didn't get a good response." 136 | 137 | 138 | The data returned from a request is the data returned by the API. This is 139 | generally JSON, YAML, XML, etc... however if a Serialization handler is 140 | enabled, this will be a python dictionary. See :mod:`drest.serialization`. 141 | 142 | response.data: 143 | 144 | .. code-block:: python 145 | 146 | { 147 | u'meta': 148 | { 149 | u'previous': None, 150 | u'total_count': 3, 151 | u'offset': 0, 152 | u'limit': 20, 153 | u'next': 154 | None 155 | }, 156 | u'objects': 157 | [ 158 | { 159 | u'username': u'john.doe', 160 | u'first_name': u'John', 161 | u'last_name': u'Doe', 162 | u'resource_pk': 2, 163 | u'last_login': u'2012-01-26T01:21:20', 164 | u'resource_uri': u'/api/v1/users/2/', 165 | u'id': u'2', 166 | u'date_joined': u'2008-09-04T14:25:29' 167 | } 168 | ] 169 | } 170 | 171 | The above is fictitious data returned from a TastyPie API. What is returned 172 | by an API is unique to that API therefore you should expect the 'data' to be 173 | different that the above. 174 | 175 | 176 | Connecting Over SSL 177 | ------------------- 178 | 179 | Though this is documented elsewhere, it is a pretty common question. Often 180 | times API services are SSL enabled (over https://) but do not possess a valid 181 | or active SSL certificate. Anytime an API service has an invalid, or usually 182 | self-signed certificate, you will receive an SSL error similar to: 183 | 184 | .. code-block:: text 185 | 186 | [Errno 1] _ssl.c:503: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed 187 | 188 | 189 | In order to work around such situations, simply pass the following to your 190 | api: 191 | 192 | .. code-block:: python 193 | 194 | api = drest.API('https://example.com/api/v1/', ignore_ssl_validation=True) 195 | 196 | 197 | -------------------------------------------------------------------------------- /doc/source/tastypie.rst: -------------------------------------------------------------------------------- 1 | .. _tastypie: 2 | 3 | Working With Django TastyPie 4 | ============================ 5 | 6 | dRest includes an API specifically built for the 7 | `Django TastyPie `_ API framework. 8 | It handles auto-detection of resources, and their schema, as well as 9 | other features to support the interface including getting resource data 10 | by 'resource_uri'. 11 | 12 | API Reference 13 | ^^^^^^^^^^^^^ 14 | 15 | * :mod:`drest.api.TastyPieAPI` 16 | * :mod:`drest.request.TastyPieRequestHandler` 17 | * :mod:`drest.resource.TastyPieResourceHandler` 18 | 19 | -------------------------------------------------------------------------------- /doc/source/usage.rst: -------------------------------------------------------------------------------- 1 | Usage Documentation 2 | =================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | installation 10 | quickstart 11 | tastypie 12 | customization 13 | debugging -------------------------------------------------------------------------------- /drest/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # only import api classes for convenience 3 | from .api import API, TastyPieAPI -------------------------------------------------------------------------------- /drest/api.py: -------------------------------------------------------------------------------- 1 | """dRest core API connection library.""" 2 | 3 | import re 4 | from . import interface, resource, request, serialization, meta, exc 5 | from . import response 6 | 7 | class API(meta.MetaMixin): 8 | """ 9 | The API class acts as a high level 'wrapper' around multiple lower level 10 | handlers. Most of the meta arguments are optionally passed to one or 11 | more handlers upon instantiation. All handler classes must be passed 12 | *un-instantiated*. 13 | 14 | Arguments: 15 | 16 | baseurl 17 | Translated to self.baseurl (for convenience). 18 | 19 | Optional Arguments and Meta: 20 | 21 | debug 22 | Boolean. Toggle debug console output. Default: False. 23 | 24 | baseurl 25 | The base url to the API endpoint. 26 | 27 | request_handler 28 | The Request Handler class that performs the actual HTTP (or other) 29 | requests. Default: drest.request.RequestHandler. 30 | 31 | resource_handler 32 | The Resource Handler class that is used when api.add_resource is 33 | called. Default: drest.resource.ResourceHandler. 34 | 35 | response_handler 36 | An un-instantiated Response Handler class used to return 37 | responses to the caller. Default: drest.response.ResponseHandler. 38 | 39 | serialization_handler 40 | An un-instantiated Serialization Handler class used to 41 | serialize/deserialize data. 42 | Default: drest.serialization.JsonSerializationHandler. 43 | 44 | ignore_ssl_validation 45 | Boolean. Whether or not to ignore ssl validation errors. 46 | Default: False 47 | 48 | serialize 49 | Boolean. Whether or not to serialize data before sending 50 | requests. Default: False. 51 | 52 | deserialize 53 | Boolean. Whether or not to deserialize data before returning 54 | the Response object. Default: True. 55 | 56 | trailing_slash 57 | Boolean. Whether or not to append a trailing slash to the 58 | request url. Default: True. 59 | 60 | extra_headers 61 | A dictionary of key value pairs that are added to the HTTP headers 62 | of *every* request. Passed to request_handler.add_header(). 63 | 64 | extra_params 65 | A dictionary of key value pairs that are added to the POST, or 66 | 'payload' data sent with *every* request. Passed to 67 | request_handler.add_param(). 68 | 69 | extra_url_params 70 | A dictionary of key value pairs that are added to the GET/URL 71 | parameters of *every* request. Passed to 72 | request_handler.add_extra_url_param(). 73 | 74 | timeout 75 | The amount of seconds where a request should timeout. Default: 30 76 | 77 | Usage 78 | 79 | .. code-block:: python 80 | 81 | import drest 82 | 83 | # Create a generic client api object 84 | api = drest.API('http://localhost:8000/api/v1/') 85 | 86 | # Or something more customized: 87 | api = drest.API( 88 | baseurl='http://localhost:8000/api/v1/', 89 | trailing_slash=False, 90 | ignore_ssl_validation=True, 91 | ) 92 | 93 | # Or even more so: 94 | class MyAPI(drest.API): 95 | class Meta: 96 | baseurl = 'http://localhost:8000/api/v1/' 97 | extra_headers = dict(MyKey='Some Value For Key') 98 | extra_params = dict(some_param='some_value') 99 | request_handler = MyCustomRequestHandler 100 | api = MyAPI() 101 | 102 | # By default, the API support HTTP Basic Auth with username/password. 103 | api.auth('john.doe', 'password') 104 | 105 | # Make calls openly 106 | response = api.make_request('GET', '/users/1/') 107 | 108 | # Or attach a resource 109 | api.add_resource('users') 110 | 111 | # Get available resources 112 | api.resources 113 | 114 | # Get all objects of a resource 115 | response = api.users.get() 116 | 117 | # Get a single resource with primary key '1' 118 | response = api.users.get(1) 119 | 120 | # Update a resource with primary key '1' 121 | response = api.users.get(1) 122 | updated_data = response.data.copy() 123 | updated_data['first_name'] = 'John' 124 | updated_data['last_name'] = 'Doe' 125 | 126 | response = api.users.put(data['id'], updated_data) 127 | 128 | # Create a resource 129 | user_data = dict( 130 | username='john.doe', 131 | password='oober-secure-password', 132 | first_name='John', 133 | last_name='Doe', 134 | ) 135 | response = api.users.post(user_data) 136 | 137 | # Delete a resource with primary key '1' 138 | response = api.users.delete(1) 139 | """ 140 | class Meta: 141 | baseurl = None 142 | request_handler = request.RequestHandler 143 | resource_handler = resource.RESTResourceHandler 144 | extra_headers = {} 145 | extra_params = {} 146 | extra_url_params = {} 147 | 148 | def __init__(self, baseurl=None, **kw): 149 | if baseurl: 150 | kw['baseurl'] = baseurl 151 | super(API, self).__init__(**kw) 152 | 153 | self.baseurl = self._meta.baseurl.strip('/') 154 | self._resources = [] 155 | 156 | self._setup_request_handler(**kw) 157 | 158 | def _setup_request_handler(self, **kw): 159 | request.validate(self._meta.request_handler) 160 | self.request = self._meta.request_handler(**kw) 161 | 162 | # just makes things easier to be able to wrap meta under the api 163 | # and pass it to the request handler. 164 | for meta in dir(self._meta): 165 | if meta.startswith('_'): 166 | continue 167 | if hasattr(self.request._meta, meta): 168 | setattr(self.request._meta, meta, getattr(self._meta, meta)) 169 | 170 | for key in self._meta.extra_headers: 171 | self.request.add_header(key, self._meta.extra_headers[key]) 172 | 173 | for key in self._meta.extra_params: 174 | self.request.add_param(key, self._meta.extra_params[key]) 175 | 176 | for key in self._meta.extra_url_params: 177 | self.request.add_url_param(key, self._meta.extra_url_params[key]) 178 | 179 | def auth(self, user, password, **kw): 180 | """ 181 | This authentication mechanism implements HTTP Basic Authentication. 182 | 183 | Required Arguments: 184 | 185 | user 186 | The API username. 187 | 188 | password 189 | The password of that user. 190 | 191 | """ 192 | self.request.set_auth_credentials(user, password) 193 | 194 | def make_request(self, method, path, params=None, headers=None): 195 | if params is None: 196 | params = {} 197 | if headers is None: 198 | headers = {} 199 | url = "%s/%s/" % (self.baseurl.strip('/'), path.strip('/')) 200 | return self.request.make_request(method, url, params, headers) 201 | 202 | @property 203 | def resources(self): 204 | return self._resources 205 | 206 | def add_resource(self, name, resource_handler=None, path=None): 207 | """ 208 | Add a resource handler to the api object. 209 | 210 | Required Arguments: 211 | 212 | name 213 | The name of the resource. This is generally the basic name 214 | of the resource on the API. For example '/api/v0/users/' 215 | would likely be called 'users' and will be accessible as 216 | 'api.users' from which additional calls can be made. For 217 | example 'api.users.get()'. 218 | 219 | Optional Arguments: 220 | 221 | resource_handler 222 | The resource handler class to use. Defaults to 223 | self._meta.resource_handler. 224 | 225 | path 226 | The path to the resource on the API (after the base url). 227 | Defaults to '//'. 228 | 229 | 230 | Nested Resources: 231 | 232 | It is possible to attach resources in a 'nested' fashion. For example 233 | passing a name of 'my.nested.users' would be accessible as 234 | api.my.nested.users.get(). 235 | 236 | Usage: 237 | 238 | .. code-block:: python 239 | 240 | api.add_resource('users') 241 | response = api.users.get() 242 | 243 | # Or for nested resources 244 | api.add_resource('my.nested.users', path='/users/') 245 | response = api.my.nested.users.get() 246 | 247 | """ 248 | safe_list = ['.', '_'] 249 | for char in name: 250 | if char in safe_list: 251 | continue 252 | if not char.isalnum(): 253 | raise exc.dRestResourceError( 254 | "resource name must be alpha-numeric." 255 | ) 256 | 257 | if not path: 258 | path = '%s' % name 259 | else: 260 | path = path.strip('/') 261 | 262 | if not resource_handler: 263 | resource_handler = self._meta.resource_handler 264 | 265 | resource.validate(resource_handler) 266 | handler = resource_handler(self, name, path) 267 | if hasattr(self, name): 268 | raise exc.dRestResourceError( 269 | "The object '%s' already exist on '%s'" % (name, self)) 270 | 271 | 272 | # break up if nested 273 | parts = name.split('.') 274 | if len(parts) == 1: 275 | setattr(self, name, handler) 276 | elif len(parts) > 1: 277 | first = parts.pop(0) 278 | last = parts.pop() 279 | 280 | # add the first object to self 281 | setattr(self, first, resource.NestedResource()) 282 | first_obj = getattr(self, first) 283 | current_obj = first_obj 284 | 285 | # everything in between 286 | for part in parts: 287 | setattr(current_obj, part, resource.NestedResource()) 288 | current_obj = getattr(current_obj, part) 289 | 290 | # add the actual resource to the chain of nested objects 291 | setattr(current_obj, last, handler) 292 | 293 | self._resources.append(name) 294 | 295 | class TastyPieAPI(API): 296 | """ 297 | This class implements an API client, specifically tailored for 298 | interfacing with `TastyPie `_. 299 | 300 | Optional / Meta Arguments: 301 | 302 | auth_mech 303 | The auth mechanism to use. One of ['basic', 'api_key']. 304 | Default: 'api_key'. 305 | 306 | auto_detect_resources 307 | Boolean. Whether or not to auto detect, and add resource objects 308 | to the api. Default: True. 309 | 310 | 311 | Authentication Mechanisms 312 | 313 | Currently the only supported authentication mechanism are: 314 | 315 | * ApiKeyAuthentication 316 | * BasicAuthentication 317 | 318 | Usage 319 | 320 | Please note that the following example use ficticious resource data. 321 | What is returned, and sent to the API is unique to the API itself. Please 322 | do not copy and paste any of the following directly without modifying the 323 | request parameters per your use case. 324 | 325 | Create the client object, and authenticate with a user/api_key pair by 326 | default: 327 | 328 | .. code-block:: python 329 | 330 | import drest 331 | api = drest.api.TastyPieAPI('http://localhost:8000/api/v0/') 332 | api.auth('john.doe', '34547a497326dde80bcaf8bcee43e3d1b5f24cc9') 333 | 334 | 335 | OR authenticate against HTTP Basic Auth: 336 | 337 | .. code-block:: python 338 | 339 | import drest 340 | api = drest.api.TastyPieAPI('http://localhost:8000/api/v0/', 341 | auth_mech='basic') 342 | api.auth('john.doe', 'my_password') 343 | 344 | 345 | As drest auto-detects TastyPie resources, you can view those at: 346 | 347 | .. code-block:: python 348 | 349 | api.resources 350 | 351 | And access their schema: 352 | 353 | .. code-block:: python 354 | 355 | api.users.schema 356 | 357 | As well as make the usual calls such as: 358 | 359 | .. code-block:: python 360 | 361 | api.users.get() 362 | api.users.get() 363 | api.users.put(, data_dict) 364 | api.users.post(data_dict) 365 | api.users.delete() 366 | 367 | What about filtering? (these depend on how the `API is configured `_): 368 | 369 | .. code-block:: python 370 | 371 | api.users.get(params=dict(username='admin')) 372 | api.users.get(params=dict(username__icontains='admin')) 373 | ... 374 | 375 | See :mod:`drest.api.API` for more standard usage examples. 376 | 377 | """ 378 | class Meta: 379 | request_handler = request.TastyPieRequestHandler 380 | resource_handler = resource.TastyPieResourceHandler 381 | auto_detect_resources = True 382 | auth_mech = 'api_key' 383 | 384 | auth_mechanizms = ['api_key', 'basic'] 385 | 386 | def __init__(self, *args, **kw): 387 | super(TastyPieAPI, self).__init__(*args, **kw) 388 | if self._meta.auto_detect_resources: 389 | self.find_resources() 390 | 391 | def auth(self, *args, **kw): 392 | """ 393 | Authenticate the request, determined by Meta.auth_mech. Arguments 394 | and Keyword arguments are just passed to the auth_mech function. 395 | 396 | """ 397 | if self._meta.auth_mech in self.auth_mechanizms: 398 | func = getattr(self, '_auth_via_%s' % self._meta.auth_mech) 399 | func(*args, **kw) 400 | else: 401 | raise exc.dRestAPIError("Unknown TastyPie auth mechanism.") 402 | 403 | def _auth_via_basic(self, user, password, **kw): 404 | """ 405 | This is just a wrapper around drest.api.API.auth(). 406 | 407 | """ 408 | return super(TastyPieAPI, self).auth(user, password) 409 | 410 | def _auth_via_api_key(self, user, api_key, **kw): 411 | """ 412 | This authentication mechanism adds an Authorization header for 413 | user/api_key per the 414 | `TastyPie Documentation `_. 415 | 416 | Required Arguments: 417 | 418 | user 419 | The API username. 420 | 421 | api_key 422 | The API Key of that user. 423 | 424 | """ 425 | key = 'Authorization' 426 | value = 'ApiKey %s:%s' % (user, api_key) 427 | self.request.add_header(key, value) 428 | 429 | def find_resources(self): 430 | """ 431 | Find available resources, and add them via add_resource(). 432 | 433 | """ 434 | response = self.make_request('GET', '/') 435 | for resource in list(response.data.keys()): 436 | if resource not in self._resources: 437 | self.add_resource(resource) 438 | 439 | -------------------------------------------------------------------------------- /drest/exc.py: -------------------------------------------------------------------------------- 1 | 2 | class dRestError(Exception): 3 | """Generic dRest Errors.""" 4 | def __init__(self, msg): 5 | self.msg = msg 6 | 7 | def __repr__(self): 8 | return "" % self.msg 9 | 10 | def __str__(self): 11 | return str(self.msg) 12 | 13 | class dRestInterfaceError(dRestError): 14 | """dRest Interface Errors.""" 15 | 16 | def __init__(self, msg): 17 | super(dRestInterfaceError, self).__init__(msg) 18 | 19 | def __repr__(self): 20 | return "dRestInterfaceError: %s" % self.msg 21 | 22 | class dRestRequestError(dRestError): 23 | """dRest Request Errors.""" 24 | def __init__(self, msg, response): 25 | super(dRestRequestError, self).__init__(msg) 26 | self.response = response 27 | 28 | def __repr__(self): 29 | return "dRestRequestError: %s" % self.msg 30 | 31 | class dRestResourceError(dRestError): 32 | """dRest Resource Errors.""" 33 | 34 | def __init__(self, msg): 35 | super(dRestResourceError, self).__init__(msg) 36 | 37 | def __repr__(self): 38 | return "dRestResourceError: %s" % self.msg 39 | 40 | class dRestAPIError(dRestError): 41 | """dRest API Errors.""" 42 | 43 | def __init__(self, msg): 44 | super(dRestAPIError, self).__init__(msg) 45 | 46 | def __repr__(self): 47 | return "dRestAPIError: %s" % self.msg 48 | -------------------------------------------------------------------------------- /drest/interface.py: -------------------------------------------------------------------------------- 1 | 2 | from . import exc 3 | 4 | class Interface(object): 5 | """ 6 | This is an abstract base class that all interface classes should 7 | subclass from. 8 | 9 | """ 10 | def __init__(self): 11 | """ 12 | An interface definition class. All Interfaces should subclass from 13 | here. Note that this is not an implementation and should never be 14 | used directly. 15 | """ 16 | raise exc.dRestInterfaceError("Interfaces can not be used directly.") 17 | 18 | class Attribute(object): 19 | """ 20 | Defines an Interface attribute. 21 | 22 | Usage: 23 | 24 | .. code-block:: python 25 | 26 | from drest import interface 27 | 28 | class MyInterface(interface.Interface): 29 | my_attribute = interface.Attribute("A description of my_attribute.") 30 | 31 | """ 32 | def __init__(self, description): 33 | """ 34 | An interface attribute definition. 35 | 36 | Required Arguments: 37 | 38 | description 39 | The description of the attribute. 40 | 41 | """ 42 | self.description = description 43 | 44 | def __repr__(self): 45 | return "Attribute: %s" % self.description 46 | 47 | def __str__(self): 48 | return str(self.__repr__()) 49 | 50 | def validate(interface, obj, members, metas=[]): 51 | """ 52 | A wrapper to validate interfaces. 53 | 54 | Required Arguments: 55 | 56 | interface 57 | The interface class to validate against 58 | 59 | obj 60 | The object to validate. 61 | 62 | members 63 | A list of object members that must exist. 64 | 65 | Optional Arguments: 66 | 67 | metas 68 | A list of meta parameters that must exist. 69 | 70 | """ 71 | invalid = [] 72 | 73 | for member in members: 74 | if not hasattr(obj, member): 75 | invalid.append(member) 76 | 77 | if hasattr(obj, '_meta'): 78 | for meta in metas: 79 | if not hasattr(obj._meta, meta): 80 | invalid.append('_meta.%s' % meta) 81 | 82 | if invalid: 83 | raise exc.dRestInterfaceError("Invalid or missing: %s in %s" % \ 84 | (invalid, obj)) -------------------------------------------------------------------------------- /drest/meta.py: -------------------------------------------------------------------------------- 1 | """dRest core meta functionality. Barrowed from http://slumber.in/.""" 2 | 3 | class Meta(object): 4 | """ 5 | Model that acts as a container class for a meta attributes for a larger 6 | class. It stuffs any kwarg it gets in it's init as an attribute of itself. 7 | 8 | """ 9 | 10 | def __init__(self, **kw): 11 | self._merge(kw) 12 | 13 | def _merge(self, dict_obj): 14 | for key, value in dict_obj.items(): 15 | setattr(self, key, value) 16 | 17 | class MetaMixin(object): 18 | """ 19 | Mixin that provides the Meta class support to add settings to instances 20 | of slumber objects. Meta settings cannot start with a _. 21 | 22 | """ 23 | 24 | def __init__(self, *args, **kw): 25 | # Get a List of all the Classes we in our MRO, find any attribute named 26 | # Meta on them, and then merge them together in order of MRO 27 | metas = reversed([x.Meta for x in self.__class__.mro() \ 28 | if hasattr(x, "Meta")]) 29 | final_meta = {} 30 | 31 | # Merge the Meta classes into one dict 32 | for meta in metas: 33 | final_meta.update(dict([x for x in list(meta.__dict__.items()) \ 34 | if not x[0].startswith("_")])) 35 | 36 | # Update the final Meta with any kw passed in 37 | for key in list(final_meta.keys()): 38 | if key in kw: 39 | final_meta[key] = kw.pop(key) 40 | 41 | self._meta = Meta(**final_meta) 42 | 43 | # FIX ME: object.__init__() doesn't take params without exception 44 | super(MetaMixin, self).__init__() -------------------------------------------------------------------------------- /drest/request.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | if sys.version_info[0] < 3: 6 | import httplib # pragma: no cover 7 | from urllib import urlencode # pragma: no cover 8 | from urllib2 import urlopen # pragma: no cover 9 | 10 | else: 11 | from http import client as httplib # pragma: no cover 12 | from urllib.parse import urlencode # pragma: no cover 13 | from urllib.request import urlopen # pragma: no cover 14 | 15 | import socket 16 | from httplib2 import Http, ServerNotFoundError 17 | 18 | from . import exc, interface, meta, serialization, response 19 | 20 | def validate(obj): 21 | """Validates a handler implementation against the IRequest interface.""" 22 | members = [ 23 | 'add_param', 24 | 'add_url_param', 25 | 'add_header', 26 | 'make_request', 27 | 'handle_response', 28 | ] 29 | metas = [ 30 | 'response_handler', 31 | 'serialization_handler', 32 | 'serialize', 33 | 'deserialize', 34 | ] 35 | interface.validate(IRequest, obj, members, metas) 36 | 37 | class IRequest(interface.Interface): 38 | """ 39 | This class defines the Request Handler Interface. Classes that 40 | implement this handler must provide the methods and attributes defined 41 | below. 42 | 43 | All implementations must provide sane 'default' functionality when 44 | instantiated with no arguments. Meaning, it can and should accept 45 | optional parameters that alter how it functions, but can not require 46 | any parameters. 47 | 48 | Implementations do *not* subclass from interfaces. 49 | 50 | """ 51 | 52 | def add_param(key, value): 53 | """ 54 | Add extra parameters to pass along with *every* request. 55 | These are passed with the request 'payload' (serialized if a 56 | serialization handler is enabled). With GET requests they are 57 | appended to the URL. 58 | 59 | Required Arguments: 60 | 61 | key 62 | The key of the parameter to add. 63 | 64 | value 65 | The value of the parameter to add. 66 | 67 | """ 68 | 69 | def add_url_param(key, value): 70 | """ 71 | Similar to 'add_params', however this function adds extra parameters 72 | to the url for *every* request. 73 | These are *not* passed with the request 'payload' (serialized if a 74 | serialization handler is enabled) except for GET requests. 75 | 76 | Required Arguments: 77 | 78 | key 79 | The key of the parameter to add. 80 | 81 | value 82 | The value of the parameter to add. 83 | 84 | """ 85 | 86 | 87 | def add_header(key, value): 88 | """ 89 | Add extra headers to pass along with *every* request. 90 | 91 | Required Arguments: 92 | 93 | key 94 | The key of the header to add. 95 | 96 | value 97 | The value of the header to add. 98 | 99 | """ 100 | 101 | def make_request(method, path, params=None, headers=None): 102 | """ 103 | Make a request with the upstream API. 104 | 105 | Required Arguments: 106 | 107 | method 108 | The HTTP method to request as. I.e. ['GET', 'POST', 'PUT', 109 | 'DELETE', '...']. 110 | 111 | path 112 | The of the request url *after* the baseurl. 113 | 114 | 115 | Optional Arguments: 116 | 117 | params 118 | Dictionary of parameters to pass with the request. These will be serialized 119 | if configured to serialize. 120 | 121 | headers 122 | Dictionary of headers to pass to the request. 123 | 124 | """ 125 | 126 | def handle_response(response_object): 127 | """ 128 | Called after the request is made. This is a convenient place for 129 | developers to handle what happens during every request per their 130 | application needs. 131 | 132 | Required Arguments: 133 | 134 | response_object 135 | The response object created by the request. 136 | 137 | """ 138 | 139 | class RequestHandler(meta.MetaMixin): 140 | """ 141 | Generic class that handles HTTP requests. Uses the Json Serialization 142 | handler by default, but only 'deserializes' response content. 143 | 144 | Optional Arguments / Meta: 145 | 146 | debug 147 | Boolean. Toggle debug console output. Default: False. 148 | 149 | ignore_ssl_validation 150 | Boolean. Whether or not to ignore ssl validation errors. 151 | Default: False 152 | 153 | response_handler 154 | An un-instantiated Response Handler class used to return 155 | responses to the caller. Default: drest.response.ResponseHandler. 156 | 157 | serialization_handler 158 | An un-instantiated Serialization Handler class used to 159 | serialize/deserialize data. 160 | Default: drest.serialization.JsonSerializationHandler. 161 | 162 | serialize 163 | Boolean. Whether or not to serialize data before sending 164 | requests. Default: False. 165 | 166 | deserialize 167 | Boolean. Whether or not to deserialize data before returning 168 | the Response object. Default: True. 169 | 170 | trailing_slash 171 | Boolean. Whether or not to append a trailing slash to the 172 | request url. Default: True. 173 | 174 | timeout 175 | The amount of seconds where a request should timeout. 176 | Default: None 177 | 178 | """ 179 | class Meta: 180 | debug = False 181 | ignore_ssl_validation = False 182 | response_handler = response.ResponseHandler 183 | serialization_handler = serialization.JsonSerializationHandler 184 | serialize = False 185 | deserialize = True 186 | trailing_slash = True 187 | allow_get_body = False 188 | timeout = None 189 | 190 | def __init__(self, **kw): 191 | super(RequestHandler, self).__init__(**kw) 192 | self._extra_params = {} 193 | self._extra_url_params = {} 194 | self._extra_headers = {} 195 | self._auth_credentials = () 196 | self._http = None 197 | 198 | if 'DREST_DEBUG' in os.environ and \ 199 | os.environ['DREST_DEBUG'] in [1, '1']: 200 | self._meta.debug = True 201 | 202 | response.validate(self._meta.response_handler) 203 | if self._meta.serialization_handler: 204 | serialization.validate(self._meta.serialization_handler) 205 | self._serialization = self._meta.serialization_handler(**kw) 206 | headers = self._serialization.get_headers() 207 | for key in headers: 208 | self.add_header(key, headers[key]) 209 | else: 210 | self._meta.serialize = False 211 | self._meta.deserialize = False 212 | 213 | def _serialize(self, data): 214 | if self._meta.serialize: 215 | return self._serialization.serialize(data) 216 | else: 217 | return data 218 | 219 | def _deserialize(self, data): 220 | if self._meta.deserialize: 221 | return self._serialization.deserialize(data) 222 | else: 223 | return data 224 | 225 | def set_auth_credentials(self, user, password): 226 | """ 227 | Set the authentication user and password that will be used for 228 | HTTP Basic and Digest Authentication. 229 | 230 | Required Arguments: 231 | 232 | user 233 | The authentication username. 234 | 235 | password 236 | That user's password. 237 | 238 | """ 239 | self._auth_credentials = (user, password) 240 | self._clear_http() 241 | 242 | def add_param(self, key, value): 243 | """ 244 | Adds a key/value to self._extra_params, which is sent with every 245 | request. 246 | 247 | Required Arguments: 248 | 249 | key 250 | The key of the parameter. 251 | 252 | value 253 | The value of 'key'. 254 | 255 | """ 256 | self._extra_params[key] = value 257 | 258 | def add_url_param(self, key, value): 259 | """ 260 | Adds a key/value to self._extra_url_params, which is sent with every 261 | request (in the URL). 262 | 263 | Required Arguments: 264 | 265 | key 266 | The key of the parameter. 267 | 268 | value 269 | The value of 'key'. 270 | 271 | """ 272 | self._extra_url_params[key] = value 273 | 274 | def add_header(self, key, value): 275 | """ 276 | Adds a key/value to self._extra_headers, which is sent with every 277 | request. 278 | 279 | Required Arguments: 280 | 281 | key 282 | The key of the parameter. 283 | 284 | value 285 | The value of 'key'. 286 | 287 | """ 288 | self._extra_headers[key] = value 289 | 290 | def _get_http(self): 291 | """ 292 | Returns either the existing (cached) httplib2.Http() object, or 293 | a new instance of one. 294 | 295 | """ 296 | if self._http == None: 297 | if self._meta.ignore_ssl_validation: 298 | self._http = Http(disable_ssl_certificate_validation=True, 299 | timeout=self._meta.timeout) 300 | else: 301 | self._http = Http(timeout=self._meta.timeout) 302 | 303 | if self._auth_credentials: 304 | self._http.add_credentials(self._auth_credentials[0], 305 | self._auth_credentials[1]) 306 | return self._http 307 | 308 | def _clear_http(self): 309 | self._http = None 310 | 311 | def _make_request(self, url, method, payload=None, headers=None): 312 | """ 313 | A wrapper around httplib2.Http.request. 314 | 315 | Required Arguments: 316 | 317 | url 318 | The url of the request. 319 | 320 | method 321 | The method of the request. I.e. 'GET', 'PUT', 'POST', 'DELETE'. 322 | 323 | Optional Arguments: 324 | 325 | payload 326 | The urlencoded parameters. 327 | 328 | headers 329 | Additional headers of the request. 330 | 331 | """ 332 | if payload is None: 333 | if self._meta.serialize: 334 | payload = self._serialize({}) 335 | else: 336 | payload = urlencode({}) 337 | if headers is None: 338 | headers = {} 339 | 340 | try: 341 | http = self._get_http() 342 | return http.request(url, method, payload, headers=headers) 343 | 344 | except socket.error as e: 345 | # Try again just in case there was an issue with the cached _http 346 | try: 347 | self._clear_http() 348 | return self._get_http().request(url, method, payload, 349 | headers=headers) 350 | except socket.error as e: 351 | raise exc.dRestAPIError(e) 352 | 353 | except ServerNotFoundError as e: 354 | raise exc.dRestAPIError(e.args[0]) 355 | 356 | def _get_complete_url(self, method, url, params): 357 | url = "%s%s" % (url.strip('/'), '/' if self._meta.trailing_slash else '') 358 | 359 | if method == 'GET': 360 | url_params = dict(self._extra_url_params, **params) 361 | else: 362 | url_params = self._extra_url_params 363 | 364 | if url_params: 365 | url = "%s?%s" % (url, urlencode(url_params)) 366 | 367 | return url 368 | 369 | def make_request(self, method, url, params=None, headers=None): 370 | """ 371 | Make a call to a resource based on path, and parameters. 372 | 373 | Required Arguments: 374 | 375 | method 376 | One of HEAD, GET, POST, PUT, PATCH, DELETE, etc. 377 | 378 | url 379 | The full url of the request (without any parameters). Any 380 | params (with GET method) and self.extra_url_params will be 381 | added to this url. 382 | 383 | Optional Arguments: 384 | 385 | params 386 | Dictionary of additional (one-time) keyword arguments for the 387 | request. 388 | 389 | headers 390 | Dictionary of additional (one-time) headers of the request. 391 | 392 | """ 393 | if params is None: 394 | params = {} 395 | if headers is None: 396 | headers = {} 397 | params = dict(self._extra_params, **params) 398 | headers = dict(self._extra_headers, **headers) 399 | url = self._get_complete_url(method, url, params) 400 | 401 | if self._meta.serialize: 402 | payload = self._serialize(params) 403 | else: 404 | payload = urlencode(params) 405 | 406 | if self._meta.debug: 407 | print('DREST_DEBUG: method=%s url=%s params=%s headers=%s' % \ 408 | (method, url, payload, headers)) 409 | 410 | 411 | if method is 'GET' and not self._meta.allow_get_body: 412 | payload = '' 413 | if self._meta.debug: 414 | print("DREST_DEBUG: supressing body for GET request") 415 | 416 | res_headers, data = self._make_request(url, method, payload, 417 | headers=headers) 418 | unserialized_data = data 419 | serialized_data = None 420 | if self._meta.deserialize: 421 | serialized_data = data 422 | data = self._deserialize(data) 423 | 424 | return_response = response.ResponseHandler( 425 | int(res_headers['status']), data, res_headers, 426 | ) 427 | 428 | return self.handle_response(return_response) 429 | 430 | def handle_response(self, response_object): 431 | """ 432 | A simple wrapper to handle the response. By default raises 433 | exc.dRestRequestError if the response code is within 400-499, or 500. 434 | Must return the original, or modified, response object. 435 | 436 | Required Arguments: 437 | 438 | response_object 439 | The response object created by the request. 440 | 441 | """ 442 | response = response_object 443 | if (400 <= response.status <=499) or (response.status == 500): 444 | msg = "Received HTTP Code %s - %s" % ( 445 | response.status, 446 | httplib.responses[int(response.status)]) 447 | raise exc.dRestRequestError(msg, response=response) 448 | return response 449 | 450 | class TastyPieRequestHandler(RequestHandler): 451 | """ 452 | This class implements the IRequest interface, specifically tailored for 453 | interfacing with `TastyPie `_. 454 | 455 | See :mod:`drest.request.RequestHandler` for Meta options and usage. 456 | 457 | """ 458 | class Meta: 459 | serialize = True 460 | deserialize = True 461 | serialization_handler = serialization.JsonSerializationHandler 462 | 463 | def __init__(self, **kw): 464 | super(TastyPieRequestHandler, self).__init__(**kw) 465 | -------------------------------------------------------------------------------- /drest/resource.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | from . import interface, exc, meta, request 4 | 5 | def validate(obj): 6 | """Validates a handler implementation against the IResource interface.""" 7 | members = [ 8 | 'filter', 9 | ] 10 | metas = [ 11 | 'baseurl', 12 | 'resource', 13 | 'path', 14 | 'request', 15 | ] 16 | interface.validate(IResource, obj, members) 17 | 18 | class IResource(interface.Interface): 19 | """ 20 | This class defines the Resource Handler Interface. Classes that 21 | implement this handler must provide the methods and attributes defined 22 | below. 23 | 24 | All implementations must provide sane 'default' functionality when 25 | instantiated with no arguments. Meaning, it can and should accept 26 | optional parameters that alter how it functions, but can not require 27 | any parameters. 28 | 29 | Implementations do *not* subclass from interfaces. 30 | 31 | """ 32 | 33 | class ResourceHandler(meta.MetaMixin): 34 | """ 35 | This class acts as a base class that other resource handler should 36 | subclass from. 37 | 38 | """ 39 | class Meta: 40 | pass 41 | 42 | def __init__(self, api_obj, name, path, **kw): 43 | super(ResourceHandler, self).__init__(**kw) 44 | self.api = api_obj 45 | self.path = path 46 | self.name = name 47 | 48 | def filter(self, params): 49 | """ 50 | Give the ability to alter params before sending the request. 51 | 52 | Required Arguments: 53 | 54 | params 55 | The list of params that will be passed to the endpoint. 56 | 57 | """ 58 | return params 59 | 60 | class RESTResourceHandler(ResourceHandler): 61 | """ 62 | This class implements the IResource interface, specifically for 63 | interacting with REST-like resources. It provides convenient functions 64 | that wrap around the typical GET, PUT, POST, DELETE actions. 65 | 66 | Optional Arguments / Meta: 67 | 68 | api_obj 69 | The api (parent) object that this resource is being attached to. 70 | 71 | name 72 | The name of the resource on the API. 73 | 74 | path 75 | The path to the resource (after api.baseurl). 76 | 77 | Usage: 78 | 79 | .. code-block:: python 80 | 81 | import drest 82 | 83 | class MyAPI(drest.api.API): 84 | class Meta: 85 | resource_handler = drest.resource.RESTResourceHandler 86 | ... 87 | 88 | """ 89 | def __init__(self, api_obj, name, path, **kw): 90 | super(RESTResourceHandler, self).__init__(api_obj, name, path, **kw) 91 | 92 | def get(self, resource_id=None, params=None): 93 | """ 94 | Get all records for a resource, or a single resource record. 95 | 96 | Optional Arguments: 97 | 98 | resource_id 99 | The resource id (may also be a label in some environments). 100 | 101 | params 102 | Additional request parameters to pass along. 103 | 104 | """ 105 | if params is None: 106 | params = {} # pragma: no cover 107 | if resource_id: 108 | path = '/%s/%s' % (self.path, resource_id) 109 | else: 110 | path = '/%s' % self.path 111 | 112 | try: 113 | response = self.api.make_request('GET', path, 114 | params=self.filter(params)) 115 | except exc.dRestRequestError as e: 116 | msg = "%s (resource: %s, id: %s)" % (e.msg, self.name, 117 | resource_id) 118 | raise exc.dRestRequestError(msg, e.response) 119 | 120 | return response 121 | 122 | def create(self, params=None): 123 | """A synonym for self.post().""" 124 | if params is None: 125 | params = {} # pragma: no cover 126 | 127 | return self.post(params) 128 | 129 | def post(self, params=None): 130 | """ 131 | Create a new resource. 132 | 133 | Required Arguments: 134 | 135 | params 136 | A dictionary of parameters (different for every resource). 137 | 138 | """ 139 | if params is None: 140 | params = {} # pragma: no cover 141 | 142 | params = self.filter(params) 143 | path = '/%s' % self.path 144 | 145 | try: 146 | response = self.api.make_request('POST', path, self.filter(params)) 147 | except exc.dRestRequestError as e: 148 | msg = "%s (resource: %s)" % (e.msg, self.name) 149 | raise exc.dRestRequestError(msg, e.response) 150 | 151 | return response 152 | 153 | def update(self, resource_id, params=None): 154 | """A synonym for self.put().""" 155 | if params is None: 156 | params = {} # pragma: no cover 157 | 158 | return self.put(resource_id, params) 159 | 160 | def put(self, resource_id, params=None): 161 | """ 162 | Update an existing resource. 163 | 164 | Required Arguments: 165 | 166 | resource_id 167 | The id of the resource to update. 168 | 169 | params 170 | A dictionary of parameters (different for every resource). 171 | 172 | """ 173 | if params is None: 174 | params = {} # pragma: no cover 175 | 176 | params = self.filter(params) 177 | path = '/%s/%s' % (self.path, resource_id) 178 | 179 | try: 180 | response = self.api.make_request('PUT', path, params) 181 | except exc.dRestRequestError as e: 182 | msg = "%s (resource: %s, id: %s)" % (e.msg, self.name, 183 | resource_id) 184 | raise exc.dRestRequestError(msg, e.response) 185 | 186 | return response 187 | 188 | def patch(self, resource_id, params=None): 189 | """ 190 | Update only specific items of an existing resource. 191 | 192 | Required Arguments: 193 | 194 | resource_id 195 | The id of the resource to update. 196 | 197 | params 198 | A dictionary of parameters (different for every resource). 199 | 200 | """ 201 | if params is None: 202 | params = {} # pragma: no cover 203 | 204 | params = self.filter(params) 205 | path = '/%s/%s' % (self.path, resource_id) 206 | 207 | try: 208 | response = self.api.make_request('PATCH', path, params) 209 | except exc.dRestRequestError as e: 210 | msg = "%s (resource: %s, id: %s)" % (e.msg, self.name, 211 | resource_id) 212 | raise exc.dRestRequestError(msg, e.response) 213 | 214 | return response 215 | 216 | def delete(self, resource_id, params=None): 217 | """ 218 | Delete resource record. 219 | 220 | Required Arguments: 221 | 222 | resource_id 223 | The resource id 224 | 225 | Optional Arguments: 226 | 227 | params 228 | Some resource might allow additional parameters. For example, 229 | the user resource has a 'rdikwid' (really delete I know what 230 | I'm doing) option which causes a user to *really* be deleted 231 | (normally deletion only sets the status to 'Deleted'). 232 | 233 | """ 234 | if params is None: 235 | params = {} # pragma: no cover 236 | path = '/%s/%s' % (self.path, resource_id) 237 | try: 238 | response = self.api.make_request('DELETE', path, params) 239 | except exc.dRestRequestError as e: 240 | msg = "%s (resource: %s, id: %s)" % (e.msg, self.name, 241 | resource_id) 242 | raise exc.dRestRequestError(msg, e.response) 243 | 244 | return response 245 | 246 | class TastyPieResourceHandler(RESTResourceHandler): 247 | """ 248 | This class implements the IResource interface, specifically tailored for 249 | interfacing with `TastyPie `_. 250 | 251 | """ 252 | class Meta: 253 | """ 254 | Handler meta-data (can be passed as keyword arguments to the parent 255 | class). 256 | 257 | """ 258 | request = request.TastyPieRequestHandler 259 | """The request handler used to make requests. 260 | Default: TastyPieRequestHandler.""" 261 | 262 | collection_name = 'objects' 263 | """The name of the collection. Default: objects""" 264 | 265 | def __init__(self, api_obj, name, path, **kw): 266 | super(TastyPieResourceHandler, self).__init__(api_obj, name, path, **kw) 267 | self._schema = None 268 | 269 | def get_by_uri(self, resource_uri, params=None): 270 | """ 271 | A wrapper around self.get() that accepts a TastyPie 'resource_uri' 272 | rather than a 'pk' (primary key). 273 | 274 | :param resource_uri: The resource URI to GET. 275 | :param params: Any additional keyword arguments are passed as extra 276 | request parameters. 277 | 278 | Usage: 279 | 280 | .. code-block:: python 281 | 282 | import drest 283 | api = drest.api.TastyPieAPI('http://localhost:8000/api/v0/') 284 | api.auth(user='john.doe', 285 | api_key='34547a497326dde80bcaf8bcee43e3d1b5f24cc9') 286 | response = api.users.get_by_uri('/api/v1/users/234/') 287 | 288 | """ 289 | if params is None: 290 | params = {} # pragma: no cover 291 | 292 | resource_uri = resource_uri.rstrip('/') 293 | pk = resource_uri.split('/')[-1] 294 | return self.get(pk, params) 295 | 296 | def patch_list(self, create_objects=[], delete_objects=[]): 297 | """ 298 | Tastypie resources have a patch_list method that allows you to create 299 | and delete bulk collections of objects. This uses HTTP PATCH. 300 | 301 | :param create_objects: List of objects to create in dict form. 302 | :param delete_objects: List of objects to delete in dict form. 303 | 304 | """ 305 | create_objects = [self.filter(o) for o in create_objects] 306 | delete_objects = [self.filter(o) for o in delete_objects] 307 | delete_collection_name = "deleted_%s" % self._meta.collection_name 308 | data = { 309 | self._meta.collection_name: create_objects, 310 | delete_collection_name: delete_objects, 311 | } 312 | return self.api.make_request('PATCH', self.path, data) 313 | 314 | @property 315 | def schema(self): 316 | """ 317 | Returns the resources schema. 318 | 319 | """ 320 | if not self._schema: 321 | response = self.api.make_request('GET', '%s/schema' % self.path) 322 | self._schema = response.data 323 | 324 | return self._schema 325 | 326 | class NestedResource(object): 327 | pass 328 | -------------------------------------------------------------------------------- /drest/response.py: -------------------------------------------------------------------------------- 1 | 2 | from . import exc, meta, interface 3 | 4 | def validate(obj): 5 | """Validates a handler implementation against the IResponse interface.""" 6 | members = [ 7 | 'status', 8 | 'data', 9 | 'headers', 10 | ] 11 | metas = [] 12 | interface.validate(IResponse, obj, members, metas) 13 | 14 | class IResponse(interface.Interface): 15 | """ 16 | This class defines the Response Handler Interface. Classes that 17 | implement this handler must provide the methods and attributes defined 18 | below. 19 | 20 | All implementations must provide sane 'default' functionality when 21 | instantiated with no arguments. Meaning, it can and should accept 22 | optional parameters that alter how it functions, but can not require 23 | any parameters. 24 | 25 | Implementations do *not* subclass from interfaces. 26 | 27 | """ 28 | status = interface.Attribute('The response status (i.e. HTTP code).') 29 | data = interface.Attribute('The data returned by the request.') 30 | headers = interface.Attribute('The headers returned by the request.') 31 | 32 | class ResponseHandler(meta.MetaMixin): 33 | class Meta: 34 | pass 35 | 36 | status = None 37 | data = None 38 | headers = None 39 | 40 | def __init__(self, status, data, headers): 41 | self.status = int(status) 42 | self.data = data 43 | self.headers = headers 44 | -------------------------------------------------------------------------------- /drest/serialization.py: -------------------------------------------------------------------------------- 1 | 2 | from . import interface, exc, meta 3 | 4 | def validate(obj): 5 | """Validates a handler implementation against the ISerialize interface.""" 6 | members = [ 7 | 'serialize', 8 | 'deserialize', 9 | 'get_headers', 10 | ] 11 | interface.validate(ISerialization, obj, members) 12 | 13 | class ISerialization(interface.Interface): 14 | """ 15 | This class defines the Serialization Handler Interface. Classes that 16 | implement this handler must provide the methods and attributes defined 17 | below. 18 | 19 | All implementations must provide sane 'default' functionality when 20 | instantiated with no arguments. Meaning, it can and should accept 21 | optional parameters that alter how it functions, but can not require 22 | any parameters. 23 | 24 | Implementations do *not* subclass from interfaces. 25 | 26 | """ 27 | 28 | def get_headers(): 29 | """ 30 | Return a dictionary of additional headers to include in requests. 31 | 32 | """ 33 | 34 | def deserialize(): 35 | """ 36 | Load a serialized string and return a dictionary of key/value pairs. 37 | 38 | Required Arguments: 39 | 40 | serialized_data 41 | A string of serialzed data. 42 | 43 | Returns: dict 44 | 45 | """ 46 | 47 | def serialize(): 48 | """ 49 | Dump a dictionary of values from a serialized string. 50 | 51 | Required Arguments: 52 | 53 | data_dict 54 | A data dictionary to serialize. 55 | 56 | Returns: string 57 | 58 | """ 59 | 60 | class SerializationHandler(meta.MetaMixin): 61 | """ 62 | Generic Serialization Handler. Should be used to subclass from. 63 | 64 | """ 65 | def __init__(self, **kw): 66 | super(SerializationHandler, self).__init__(**kw) 67 | 68 | def get_headers(self): 69 | return {} 70 | 71 | def deserialize(self, serialized_string): 72 | raise NotImplementedError 73 | 74 | def serialize(self, dict_obj): 75 | raise NotImplementedError 76 | 77 | 78 | class JsonSerializationHandler(SerializationHandler): 79 | """ 80 | This handler implements the ISerialization interface using the standard 81 | json library. 82 | 83 | """ 84 | def __init__(self, **kw): 85 | try: 86 | import json # pragma: no cover 87 | except ImportError as e: # pragma: no cover 88 | import simplejson as json # pragma: no cover 89 | 90 | self.backend = json 91 | super(JsonSerializationHandler, self).__init__(**kw) 92 | 93 | def deserialize(self, serialized_string): 94 | try: 95 | # Fix for Python3 96 | if type(serialized_string) == bytes: 97 | serialized_string = serialized_string.decode('utf-8') 98 | 99 | return self.backend.loads(serialized_string) 100 | except ValueError as e: 101 | return dict(error=e.args[0]) 102 | 103 | def serialize(self, dict_obj): 104 | return self.backend.dumps(dict_obj) 105 | 106 | def get_headers(self): 107 | headers = { 108 | 'Content-Type' : 'application/json', 109 | } 110 | return headers 111 | -------------------------------------------------------------------------------- /drest/testing.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | MOCKAPI = os.environ.get('DREST_MOCKAPI', 'http://localhost:8000/api/v0') 4 | 5 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | httplib2 2 | mock 3 | nose 4 | coverage 5 | sphinx 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httplib2 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=3 3 | debug=0 4 | detailed-errors=1 5 | with-coverage=1 6 | cover-package=drest 7 | cover-erase=1 8 | cover-html=1 9 | cover-tests=0 10 | cover-html-dir=coverage_report/ 11 | 12 | [build_sphinx] 13 | source-dir = doc/source 14 | build-dir = doc/build 15 | all_files = 1 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import setup, find_packages 3 | import sys, os 4 | 5 | VERSION = '0.9.13' 6 | 7 | setup(name='drest', 8 | version=VERSION, 9 | description="dRest API Client Library for Python", 10 | long_description="dRest API Client Library for Python", 11 | classifiers=[], 12 | keywords='rest api', 13 | author='BJ Dierkes', 14 | author_email='derks@datafolklabs.com', 15 | url='http://github.com/datafolklabs/drest/', 16 | license='BSD', 17 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 18 | include_package_data=True, 19 | zip_safe=False, 20 | test_suite='nose.collector', 21 | install_requires=[ 22 | ### Required to build documentation 23 | # "Sphinx >=1.0", 24 | # 25 | ### Required for testing 26 | # "nose", 27 | # "coverage", 28 | # 29 | ### Required to function 30 | 'httplib2', 31 | ], 32 | setup_requires=[ 33 | ], 34 | entry_points=""" 35 | """, 36 | namespace_packages=[ 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | os.environ['DREST_DEBUG'] = '1' -------------------------------------------------------------------------------- /tests/api_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.api.""" 2 | 3 | import os 4 | import unittest 5 | from nose.tools import ok_, eq_, raises 6 | 7 | import drest 8 | from drest.testing import MOCKAPI 9 | 10 | api = drest.api.API(MOCKAPI) 11 | 12 | class MyAPI(drest.api.TastyPieAPI): 13 | class Meta: 14 | baseurl = MOCKAPI 15 | extra_headers = dict(foo='bar') 16 | extra_params = dict(foo2='bar2') 17 | extra_url_params = dict(foo3='bar3') 18 | 19 | class APITestCase(unittest.TestCase): 20 | def test_auth(self): 21 | api.auth('john.doe', 'password') 22 | eq_(api.request._auth_credentials[0], 'john.doe') 23 | eq_(api.request._auth_credentials[1], 'password') 24 | 25 | def test_custom_auth(self): 26 | class MyAPI(drest.API): 27 | def auth(self, *args, **kw): 28 | for key in kw: 29 | self.request.add_url_param(key, kw[key]) 30 | myapi = MyAPI(MOCKAPI) 31 | myapi.auth(user='john.doe', password='password') 32 | eq_(myapi.request._extra_url_params['user'], 'john.doe') 33 | eq_(myapi.request._extra_url_params['password'], 'password') 34 | 35 | def test_wrapped_meta(self): 36 | class MyAPI2(drest.api.TastyPieAPI): 37 | class Meta: 38 | trailing_slash = False 39 | 40 | myapi = MyAPI2(MOCKAPI) 41 | eq_(myapi.request._meta.trailing_slash, False) 42 | 43 | def test_extra_headers(self): 44 | api = MyAPI() 45 | eq_('bar', api.request._extra_headers['foo']) 46 | 47 | def test_extra_params(self): 48 | api = MyAPI() 49 | eq_('bar2', api.request._extra_params['foo2']) 50 | 51 | def test_extra_url_params(self): 52 | api = MyAPI() 53 | eq_('bar3', api.request._extra_url_params['foo3']) 54 | 55 | def test_request(self): 56 | response = api.make_request('GET', '/') 57 | res = 'users' in response.data 58 | ok_(res) 59 | 60 | def test_add_resource(self): 61 | api.add_resource('users') 62 | response = api.users.get() 63 | 64 | api.add_resource('users2', path='/users/') 65 | response = api.users2.get() 66 | 67 | api.add_resource('users3', path='/users/', 68 | resource_handler=drest.resource.RESTResourceHandler) 69 | response = api.users3.get() 70 | 71 | @raises(drest.exc.dRestResourceError) 72 | def test_duplicate_resource(self): 73 | api.add_resource('users') 74 | 75 | @raises(drest.exc.dRestResourceError) 76 | def test_bad_resource_name(self): 77 | api.add_resource('some!bogus-name') 78 | 79 | def test_nested_resource_name(self): 80 | api.add_resource('nested.users.resource', path='/users/') 81 | eq_(api.nested.__class__, drest.resource.NestedResource) 82 | eq_(api.nested.users.__class__, drest.resource.NestedResource) 83 | eq_(api.nested.users.resource.__class__, drest.resource.RESTResourceHandler) 84 | 85 | def test_tastypieapi_via_apikey_auth(self): 86 | api = drest.api.TastyPieAPI(MOCKAPI) 87 | api.auth(user='john.doe', api_key='JOHN_DOE_API_KEY') 88 | 89 | # verify headers 90 | eq_(api.request._extra_headers, 91 | {'Content-Type': 'application/json', 92 | 'Authorization': 'ApiKey john.doe:JOHN_DOE_API_KEY'}) 93 | 94 | # verify resources 95 | res = 'users' in api.resources 96 | ok_(res) 97 | res = 'projects' in api.resources 98 | ok_(res) 99 | 100 | # and requests 101 | response = api.users_via_apikey_auth.get() 102 | eq_(response.data['objects'][0]['username'], 'admin') 103 | 104 | response = api.projects.get(params=dict(label__startswith='Test Project')) 105 | ok_(response.data['objects'][0]['label'].startswith('Test Project')) 106 | 107 | def test_tastypieapi_via_basic_auth(self): 108 | api = drest.api.TastyPieAPI(MOCKAPI, auth_mech='basic') 109 | api.auth(user='john.doe', password='password') 110 | 111 | eq_(api.request._auth_credentials[0], 'john.doe') 112 | eq_(api.request._auth_credentials[1], 'password') 113 | 114 | # verify resources 115 | res = 'users' in api.resources 116 | ok_(res) 117 | res = 'projects' in api.resources 118 | ok_(res) 119 | 120 | # and requests 121 | response = api.users_via_basic_auth.get() 122 | eq_(response.data['objects'][0]['username'], 'admin') 123 | 124 | @raises(drest.exc.dRestAPIError) 125 | def test_tastypieapi_via_unknown_auth(self): 126 | api = drest.api.TastyPieAPI(MOCKAPI, auth_mech='bogus') 127 | api.auth(user='john.doe', password='password') 128 | -------------------------------------------------------------------------------- /tests/exc_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.exc.""" 2 | 3 | import os 4 | import unittest 5 | from nose.tools import eq_, raises 6 | import drest 7 | from drest.testing import MOCKAPI 8 | 9 | api = drest.api.API(MOCKAPI) 10 | 11 | class ExceptionTestCase(unittest.TestCase): 12 | @raises(drest.exc.dRestError) 13 | def test_error(self): 14 | try: 15 | raise drest.exc.dRestError('Error Msg') 16 | except drest.exc.dRestError as e: 17 | e.__repr__() 18 | eq_(e.msg, 'Error Msg') 19 | eq_(e.__str__(), str(e.msg)) 20 | raise 21 | 22 | @raises(drest.exc.dRestInterfaceError) 23 | def test_interface_error(self): 24 | try: 25 | raise drest.exc.dRestInterfaceError('Error Msg') 26 | except drest.exc.dRestInterfaceError as e: 27 | e.__repr__() 28 | eq_(e.msg, 'Error Msg') 29 | eq_(e.__str__(), str(e.msg)) 30 | raise 31 | 32 | @raises(drest.exc.dRestRequestError) 33 | def test_request_error(self): 34 | try: 35 | api = drest.api.API(MOCKAPI) 36 | response = api.make_request('GET', '/') 37 | raise drest.exc.dRestRequestError('Error Msg', response) 38 | except drest.exc.dRestRequestError as e: 39 | e.__repr__() 40 | eq_(e.msg, 'Error Msg') 41 | eq_(e.__str__(), str(e.msg)) 42 | raise 43 | 44 | @raises(drest.exc.dRestResourceError) 45 | def test_resource_error(self): 46 | try: 47 | raise drest.exc.dRestResourceError('Error Msg') 48 | except drest.exc.dRestResourceError as e: 49 | e.__repr__() 50 | eq_(e.msg, 'Error Msg') 51 | eq_(e.__str__(), str(e.msg)) 52 | raise 53 | 54 | @raises(drest.exc.dRestAPIError) 55 | def test_api_error(self): 56 | try: 57 | raise drest.exc.dRestAPIError('Error Msg') 58 | except drest.exc.dRestAPIError as e: 59 | e.__repr__() 60 | eq_(e.msg, 'Error Msg') 61 | eq_(e.__str__(), str(e.msg)) 62 | raise -------------------------------------------------------------------------------- /tests/interface_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.interface.""" 2 | 3 | import os 4 | import unittest 5 | from nose.tools import eq_, raises 6 | import drest 7 | from drest.testing import MOCKAPI 8 | 9 | api = drest.api.API(MOCKAPI) 10 | 11 | class ITest(drest.interface.Interface): 12 | pass 13 | 14 | class TestHandler(drest.meta.MetaMixin): 15 | class Meta: 16 | test_meta = 'some value' 17 | 18 | def test_func(self): 19 | pass 20 | 21 | def __repr__(self): 22 | return 'TestHandler' 23 | 24 | class InterfaceTestCase(unittest.TestCase): 25 | @raises(drest.exc.dRestInterfaceError) 26 | def test_interface(self): 27 | _int = drest.interface.Interface() 28 | 29 | def test_attribute(self): 30 | attr = drest.interface.Attribute('Attr Description') 31 | eq_(str(attr), 'Attribute: Attr Description') 32 | 33 | def test_validate(self): 34 | drest.interface.validate(ITest, TestHandler(), ['test_func']) 35 | 36 | def test_validate_meta(self): 37 | drest.interface.validate(ITest, TestHandler(), ['test_func'], ['test_meta']) 38 | 39 | @raises(drest.exc.dRestInterfaceError) 40 | def test_validate_missing_member(self): 41 | try: 42 | drest.interface.validate(ITest, TestHandler(), ['missing_func']) 43 | except drest.exc.dRestInterfaceError as e: 44 | eq_(e.msg, "Invalid or missing: ['missing_func'] in TestHandler") 45 | raise 46 | 47 | @raises(drest.exc.dRestInterfaceError) 48 | def test_validate_missing_meta(self): 49 | try: 50 | drest.interface.validate(ITest, TestHandler(), [], ['missing_meta']) 51 | except drest.exc.dRestInterfaceError as e: 52 | eq_(e.msg, "Invalid or missing: ['_meta.missing_meta'] in TestHandler") 53 | raise -------------------------------------------------------------------------------- /tests/meta_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.meta.""" 2 | 3 | from nose.tools import eq_ 4 | import drest 5 | 6 | class Test(drest.meta.MetaMixin): 7 | class Meta: 8 | some_param = None 9 | 10 | def __init__(self, **kw): 11 | super(Test, self).__init__(**kw) 12 | 13 | def test_meta(): 14 | test = Test(some_param='some_value') 15 | eq_(test._meta.some_param, 'some_value') -------------------------------------------------------------------------------- /tests/request_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.request.""" 2 | 3 | import os 4 | import unittest 5 | import mock 6 | from random import random 7 | from nose.tools import eq_, ok_, raises 8 | 9 | try: 10 | import json 11 | except ImportError as e: 12 | import simplejson as json 13 | 14 | import drest 15 | from drest.testing import MOCKAPI 16 | 17 | class RequestTestCase(unittest.TestCase): 18 | def test_debug(self): 19 | os.environ['DREST_DEBUG'] = '1' 20 | req = drest.request.RequestHandler(debug=True) 21 | req.make_request('GET', '%s/' % MOCKAPI) 22 | eq_(req._meta.debug, True) 23 | os.environ['DREST_DEBUG'] = '0' 24 | 25 | def test_no_serialization(self): 26 | req = drest.request.RequestHandler(serialization_handler=None) 27 | response = req.make_request('GET', '%s/users/1/' % MOCKAPI) 28 | eq_(response.data, req._deserialize(response.data)) 29 | eq_(dict(foo='bar'), req._serialize(dict(foo='bar'))) 30 | eq_(json.loads(response.data.decode('utf-8'))['username'], 'admin') 31 | 32 | @raises(drest.exc.dRestAPIError) 33 | def test_socket_error(self): 34 | req = drest.request.RequestHandler() 35 | try: 36 | response = req.make_request('GET', 'http://bogusurl.localhost/') 37 | except drest.exc.dRestAPIError as e: 38 | res = e.__repr__().find('Unable to find the server') 39 | test_res = res >= 0 40 | ok_(test_res) 41 | raise 42 | 43 | @raises(drest.exc.dRestAPIError) 44 | def test_socket_timeout(self): 45 | req = drest.request.RequestHandler(timeout=1) 46 | try: 47 | response = req.make_request( 48 | 'GET', 49 | 'http://localhost:8000/fake_long_request/', 50 | params=dict(seconds=10), 51 | ) 52 | except drest.exc.dRestAPIError as e: 53 | res = e.__repr__().find('timed out') 54 | test_res = res >= 0 55 | ok_(test_res) 56 | raise 57 | 58 | @raises(drest.exc.dRestAPIError) 59 | def test_server_not_found_error(self): 60 | req = drest.request.RequestHandler() 61 | try: 62 | response = req.make_request('GET', 'http://bogus.example.com/api/') 63 | except drest.exc.dRestAPIError as e: 64 | res = e.__repr__().find('Unable to find the server') 65 | test_res = res >= 0 66 | ok_(test_res) 67 | raise 68 | 69 | def test_trailing_slash(self): 70 | req = drest.request.RequestHandler(trailing_slash=False) 71 | response = req.make_request('GET', '%s/users/1/' % MOCKAPI) 72 | 73 | def test_extra_params(self): 74 | params = {} 75 | params['label'] = "Project Label %s" % random() 76 | req = drest.request.TastyPieRequestHandler() 77 | req.add_param('label', params['label']) 78 | eq_(req._extra_params, params) 79 | response = req.make_request('POST', '%s/projects/' % MOCKAPI, params) 80 | 81 | def test_payload_and_headers_are_none(self): 82 | req = drest.request.TastyPieRequestHandler() 83 | response = req._make_request('%s/projects/' % MOCKAPI, 'GET', 84 | payload=None, headers=None) 85 | 86 | req = drest.request.TastyPieRequestHandler(serialize=False) 87 | response = req._make_request('%s/projects/' % MOCKAPI, 'GET', 88 | payload=None, headers=None) 89 | 90 | def test_extra_url_params(self): 91 | req = drest.request.RequestHandler() 92 | req.add_url_param('username__icontains', 'ad') 93 | eq_(req._extra_url_params, dict(username__icontains='ad')) 94 | response = req.make_request('GET', '%s/users/' % MOCKAPI) 95 | eq_(response.data['objects'][0]['username'], 'admin') 96 | 97 | def test_extra_headers(self): 98 | req = drest.request.RequestHandler(serialization_handler=None) 99 | req.add_header('some_key', 'some_value') 100 | eq_(req._extra_headers, dict(some_key='some_value')) 101 | response = req.make_request('GET', '%s/users/' % MOCKAPI) 102 | 103 | @raises(drest.exc.dRestRequestError) 104 | def test_handle_response(self): 105 | req = drest.request.RequestHandler() 106 | response = req.make_request('GET', '%s/users/1/' % MOCKAPI) 107 | response.status = 404 108 | try: 109 | req.handle_response(response) 110 | except drest.exc.dRestRequestError as e: 111 | eq_(e.msg, 'Received HTTP Code 404 - Not Found') 112 | raise 113 | 114 | def test_ignore_ssl_validation(self): 115 | req = drest.request.RequestHandler(serialization_handler=None, 116 | ignore_ssl_validation=True) 117 | req.make_request('GET', '%s/users/' % MOCKAPI) 118 | 119 | def test_get_request_allow_get_body(self): 120 | # lighttpd denies GET requests with data in the body by default 121 | class MyRequestHandler(drest.request.RequestHandler): 122 | class Meta: 123 | allow_get_body = False 124 | 125 | request = MyRequestHandler() 126 | request._get_http = mock.Mock() 127 | request._get_http().request.return_value = ({'status': 200}, '') 128 | url = '%s/users/' % MOCKAPI 129 | request.make_request('GET', url, {"param1": "value1"}) 130 | headers = {'Content-Type': 'application/json'} 131 | 132 | url = url + '?param1=value1' 133 | request._get_http()\ 134 | .request.assert_called_with(url, 'GET', '', headers=headers) 135 | -------------------------------------------------------------------------------- /tests/resource_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.resource.""" 2 | 3 | import os 4 | import re 5 | import unittest 6 | import copy 7 | from random import random 8 | from nose.tools import eq_, ok_, raises 9 | 10 | import drest 11 | from drest.testing import MOCKAPI 12 | 13 | class ResourceTestCase(unittest.TestCase): 14 | def test_rest_get_all(self): 15 | api = drest.api.API(MOCKAPI) 16 | api.add_resource('users') 17 | response = api.users.get() 18 | eq_(response.data['objects'][0]['username'], 'admin') 19 | 20 | def test_rest_get_one(self): 21 | api = drest.api.API(MOCKAPI) 22 | api.add_resource('users') 23 | response = api.users.get(2) 24 | eq_(response.data['username'], 'john.doe') 25 | 26 | @raises(drest.exc.dRestRequestError) 27 | def test_rest_get_one_bad(self): 28 | api = drest.api.API(MOCKAPI) 29 | api.add_resource('users', path='/bogus_path/') 30 | try: 31 | response = api.users.get(1) 32 | except drest.exc.dRestRequestError as e: 33 | eq_(e.msg, 'Received HTTP Code 404 - Not Found (resource: users, id: 1)') 34 | raise 35 | 36 | def test_rest_post(self): 37 | api = drest.api.TastyPieAPI(MOCKAPI) 38 | api.auth(user='john.doe', api_key='JOHNDOE_API_KEY') 39 | rand_label = "Test Project %s" % random() 40 | response = api.projects.post(dict(label=rand_label)) 41 | ok_(response.status, 200) 42 | 43 | m = re.match('http://(.*):8000\/api\/v0\/(.*)\/', 44 | response.headers['location']) 45 | ok_(m) 46 | 47 | @raises(drest.exc.dRestRequestError) 48 | def test_rest_post_bad(self): 49 | api = drest.api.API(MOCKAPI) 50 | api.add_resource('users', path='/bogus_path/') 51 | try: 52 | response = api.users.post({}) 53 | except drest.exc.dRestRequestError as e: 54 | eq_(e.msg, 'Received HTTP Code 404 - Not Found (resource: users)') 55 | raise 56 | 57 | def test_rest_create(self): 58 | api = drest.api.TastyPieAPI(MOCKAPI) 59 | api.auth(user='john.doe', api_key='JOHNDOE_API_KEY') 60 | rand_label = "Test Project %s" % random() 61 | response = api.projects.create(dict(label=rand_label)) 62 | ok_(response.status, 200) 63 | 64 | def test_rest_put(self): 65 | rand_label = "Test Project %s" % random() 66 | api = drest.api.TastyPieAPI(MOCKAPI) 67 | response = api.projects.get(1) 68 | 69 | response.data['label'] = rand_label 70 | response = api.projects.put(1, response.data) 71 | 72 | response = api.projects.get(1) 73 | eq_(response.data['label'], rand_label) 74 | 75 | @raises(drest.exc.dRestRequestError) 76 | def test_rest_put_bad(self): 77 | api = drest.api.API(MOCKAPI) 78 | api.add_resource('users', path='/bogus_path/') 79 | try: 80 | response = api.users.put(1) 81 | except drest.exc.dRestRequestError as e: 82 | eq_(e.msg, 'Received HTTP Code 404 - Not Found (resource: users, id: 1)') 83 | raise 84 | 85 | def test_rest_update(self): 86 | rand_label = "Test Project %s" % random() 87 | api = drest.api.TastyPieAPI(MOCKAPI) 88 | response = api.projects.get(1) 89 | 90 | response.data['label'] = rand_label 91 | response = api.projects.update(1, response.data) 92 | 93 | response = api.projects.get(1) 94 | eq_(response.data['label'], rand_label) 95 | 96 | def test_rest_patch(self): 97 | rand_label = "Test Project %s" % random() 98 | api = drest.api.TastyPieAPI(MOCKAPI) 99 | response = api.projects.get(1) 100 | 101 | new_data = dict() 102 | new_data['label'] = rand_label 103 | response = api.projects.patch(1, new_data) 104 | eq_(response.status, 202) 105 | 106 | response = api.projects.get(1) 107 | eq_(response.data['label'], rand_label) 108 | 109 | 110 | @raises(drest.exc.dRestRequestError) 111 | def test_rest_patch_bad(self): 112 | api = drest.api.API(MOCKAPI) 113 | api.add_resource('users', path='/bogus_path/') 114 | try: 115 | response = api.users.patch(1) 116 | except drest.exc.dRestRequestError as e: 117 | eq_(e.msg, 'Received HTTP Code 404 - Not Found (resource: users, id: 1)') 118 | raise 119 | 120 | def test_rest_delete(self): 121 | api = drest.api.TastyPieAPI(MOCKAPI) 122 | rand_label = "Test Project %s" % random() 123 | 124 | response = api.projects.create(dict(label=rand_label)) 125 | ok_(response.status, 200) 126 | 127 | response = api.projects.get(params=dict(label__exact=rand_label)) 128 | response = api.projects.delete(response.data['objects'][0]['id']) 129 | eq_(response.status, 204) 130 | 131 | @raises(drest.exc.dRestRequestError) 132 | def test_rest_delete_bad(self): 133 | api = drest.api.API(MOCKAPI) 134 | api.add_resource('users', path='/bogus_path/') 135 | try: 136 | response = api.users.delete(100123123) 137 | except drest.exc.dRestRequestError as e: 138 | eq_(e.msg, 'Received HTTP Code 404 - Not Found (resource: users, id: 100123123)') 139 | raise 140 | 141 | def test_tastypie_resource_handler(self): 142 | api = drest.api.TastyPieAPI(MOCKAPI) 143 | api.auth(user='john.doe', api_key='JOHNDOE_API_KEY') 144 | response = api.users.get_by_uri('/api/v0/users/1/') 145 | eq_(response.data['username'], 'admin') 146 | 147 | def test_tastypie_schema(self): 148 | api = drest.api.TastyPieAPI(MOCKAPI) 149 | eq_(api.users.schema['allowed_list_http_methods'], ['get']) 150 | 151 | def test_tastypie_patch_list(self): 152 | api = drest.api.TastyPieAPI(MOCKAPI) 153 | api.auth(user='john.doe', api_key='JOHNDOE_API_KEY') 154 | # Test Creating: 155 | new_project1 = dict( 156 | update_date='2013-02-27T21:07:26.403343', 157 | create_date='2013-02-27T21:07:26.403323', 158 | label='NewProject1' 159 | ) 160 | 161 | new_project2 = dict( 162 | update_date='2013-02-27T21:07:27.403343', 163 | create_date='2013-02-27T21:07:27.403323', 164 | label='NewProject2' 165 | ) 166 | 167 | response = api.projects.patch_list([new_project1, new_project2]) 168 | eq_(response.status, 202) 169 | 170 | projects = api.projects.get().data['objects'] 171 | labels = [p['label'] for p in projects] 172 | 173 | res = new_project1['label'] in labels 174 | ok_(res) 175 | 176 | res = new_project2['label'] in labels 177 | ok_(res) 178 | 179 | new_labels = ['NewProject1', 'NewProject2'] 180 | new_uris = [p['resource_uri'] for p in projects \ 181 | if p['label'] in new_labels] 182 | 183 | # Test Deleting: 184 | response = api.projects.patch_list([], new_uris) 185 | eq_(response.status, 202) 186 | 187 | projects = api.projects.get().data['objects'] 188 | labels = [p['label'] for p in projects] 189 | 190 | res = 'NewProject1' not in labels 191 | ok_(res) 192 | 193 | res = 'NewProject2' not in labels 194 | ok_(res) 195 | -------------------------------------------------------------------------------- /tests/response_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.response.""" 2 | 3 | import os 4 | import unittest 5 | from nose.tools import ok_, eq_, raises 6 | 7 | import drest 8 | from drest.testing import MOCKAPI 9 | 10 | api = drest.api.TastyPieAPI(MOCKAPI) 11 | 12 | class ResponseTestCase(unittest.TestCase): 13 | def test_good_status(self): 14 | response = api.users.get() 15 | eq_(response.status, 200) 16 | 17 | @raises(drest.exc.dRestRequestError) 18 | def test_bad_status(self): 19 | try: 20 | response = api.users.get(132412341) 21 | except drest.exc.dRestRequestError as e: 22 | eq_(e.response.status, 404) 23 | raise 24 | -------------------------------------------------------------------------------- /tests/serialization_tests.py: -------------------------------------------------------------------------------- 1 | """Tests for drest.serialization.""" 2 | 3 | import os 4 | import unittest 5 | 6 | try: 7 | import json 8 | except ImportError as e: 9 | import simplejson as json 10 | 11 | from nose.tools import eq_, raises 12 | import drest 13 | 14 | class SerializationTestCase(unittest.TestCase): 15 | def test_serialization(self): 16 | s = drest.serialization.SerializationHandler() 17 | s.get_headers() 18 | 19 | @raises(NotImplementedError) 20 | def test_serialization_serialize(self): 21 | s = drest.serialization.SerializationHandler() 22 | s.get_headers() 23 | s.serialize({}) 24 | 25 | @raises(NotImplementedError) 26 | def test_serialization_deserialize(self): 27 | s = drest.serialization.SerializationHandler() 28 | s.get_headers() 29 | s.deserialize(json.dumps({})) 30 | 31 | -------------------------------------------------------------------------------- /utils/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z $1 ]; then 4 | echo 'a version argument is required.' 5 | exit 1 6 | fi 7 | 8 | full=$1 9 | major=$(echo $1 | awk -F . {' print $1"."$2 '}) 10 | uname=$(uname) 11 | 12 | if [ "$uname" == "Darwin" ]; then 13 | sed -i '' "s/RELEASE = \'.*\'/RELEASE = '${full}'/g" doc/source/conf.py 14 | sed -i '' "s/VERSION = \'.*\'/VERSION = '${major}'/g" doc/source/conf.py 15 | find ./ -iname "setup.py" -exec sed -i '' "s/VERSION = '.*'/VERSION = '${full}'/g" {} \; 16 | elif [ "$uname" == "Linux" ]; then 17 | sed -i "s/RELEASE = \'.*\'/RELEASE = '${full}'/g" doc/source/conf.py 18 | sed -i "s/VERSION = \'.*\'/VERSION = '${major}'/g" doc/source/conf.py 19 | find ./ -iname "setup.py" -exec sed -i "s/VERSION = '.*'/VERSION = '${full}'/g" {} \; 20 | fi -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datafolklabs/drest/12b053bced20102c8a9986e5621b78688720dd6e/utils/drest.mockapi/mockapi/__init__.py -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/api.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib.auth.models import User 3 | 4 | from tastypie import fields 5 | from tastypie.authentication import ApiKeyAuthentication, BasicAuthentication 6 | from tastypie.authentication import DigestAuthentication 7 | from tastypie.authorization import Authorization 8 | from tastypie.validation import FormValidation 9 | from tastypie.http import HttpUnauthorized 10 | from tastypie.resources import ModelResource, ALL 11 | from tastypie.api import Api 12 | from tastypie.utils import trailing_slash 13 | 14 | from mockapi.projects.models import Project 15 | 16 | ### READ ONLY API V0 17 | 18 | class UserResource(ModelResource): 19 | class Meta: 20 | queryset = User.objects.all() 21 | resource_name = 'users' 22 | filtering = { 23 | 'username': ALL, 24 | } 25 | allowed_methods = ['get'] 26 | 27 | class UserResourceViaApiKeyAuth(ModelResource): 28 | class Meta: 29 | queryset = User.objects.all() 30 | authentication = ApiKeyAuthentication() 31 | resource_name = 'users_via_apikey_auth' 32 | filtering = { 33 | 'label': ALL, 34 | } 35 | allowed_methods = ['get'] 36 | 37 | class UserResourceViaBasicAuth(ModelResource): 38 | class Meta: 39 | queryset = User.objects.all() 40 | authentication = BasicAuthentication() 41 | resource_name = 'users_via_basic_auth' 42 | filtering = { 43 | 'label': ALL, 44 | } 45 | allowed_methods = ['get'] 46 | 47 | class UserResourceViaDigestAuth(ModelResource): 48 | class Meta: 49 | queryset = User.objects.all() 50 | authentication = DigestAuthentication() 51 | resource_name = 'users_via_digest_auth' 52 | filtering = { 53 | 'label': ALL, 54 | } 55 | allowed_methods = ['get'] 56 | 57 | class ProjectResource(ModelResource): 58 | class Meta: 59 | queryset = Project.objects.all() 60 | authorization = Authorization() 61 | resource_name = 'projects' 62 | filtering = { 63 | 'label': ALL, 64 | } 65 | allowed_methods = ['get', 'put', 'post', 'delete', 'patch'] 66 | 67 | v0_api = Api(api_name='v0') 68 | v0_api.register(UserResource()) 69 | v0_api.register(UserResourceViaApiKeyAuth()) 70 | v0_api.register(UserResourceViaBasicAuth()) 71 | v0_api.register(UserResourceViaDigestAuth()) 72 | v0_api.register(ProjectResource()) 73 | -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/fixtures/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "sites.site", 5 | "fields": { 6 | "domain": "dmirr.com", 7 | "name": "dMirr" 8 | } 9 | }, 10 | { 11 | "pk": 1, 12 | "model": "auth.user", 13 | "fields": { 14 | "username": "admin", 15 | "first_name": "Admin", 16 | "last_name": "", 17 | "is_active": true, 18 | "is_superuser": true, 19 | "is_staff": true, 20 | "last_login": "2008-09-04 14:25:29", 21 | "groups": [], 22 | "user_permissions": [], 23 | "password": "sha1$96e7e$fb36818e7582f86952eef1e073c5326513fa2431", 24 | "email": "admin@example.com", 25 | "date_joined": "2008-09-04 14:25:29" 26 | } 27 | }, 28 | { 29 | "pk": 2, 30 | "model": "auth.user", 31 | "fields": { 32 | "username": "john.doe", 33 | "first_name": "John", 34 | "last_name": "Doe", 35 | "is_active": true, 36 | "is_superuser": false, 37 | "is_staff": true, 38 | "last_login": "2008-09-04 14:25:29", 39 | "groups": [], 40 | "user_permissions": [], 41 | "password": "sha1$65957$5aef73618574bcf6d0bddb07de8218d11def9712", 42 | "email": "john.doe@example.com", 43 | "date_joined": "2008-09-04 14:25:29" 44 | } 45 | }, 46 | { 47 | "pk": 1, 48 | "model": "tastypie.apikey", 49 | "fields": { 50 | "user": 1, 51 | "key": "ADMIN_API_KEY", 52 | "created": "2008-09-04 14:25:29" 53 | } 54 | }, 55 | { 56 | "pk": 2, 57 | "model": "tastypie.apikey", 58 | "fields": { 59 | "user": 2, 60 | "key": "JOHN_DOE_API_KEY", 61 | "created": "2008-09-04 14:25:29" 62 | } 63 | }, 64 | { 65 | "pk": 1, 66 | "model": "projects.project", 67 | "fields": { 68 | "update_date": "2008-09-04 14:25:29", 69 | "create_date": "2008-09-04 14:25:29", 70 | "label": "Test Project" 71 | } 72 | } 73 | ] -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mockapi.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/projects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datafolklabs/drest/12b053bced20102c8a9986e5621b78688720dd6e/utils/drest.mockapi/mockapi/projects/__init__.py -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/projects/models.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db import models 3 | 4 | class Project(models.Model): 5 | class Meta: 6 | db_table = 'projects' 7 | 8 | create_date = models.DateTimeField(auto_now_add=True) 9 | update_date = models.DateTimeField(auto_now_add=True, auto_now=True) 10 | label = models.CharField(max_length=128, blank=False, unique=True) 11 | 12 | def __unicode__(self): 13 | return unicode(self.label) 14 | 15 | def __str__(self): 16 | return self.label -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/projects/views.py: -------------------------------------------------------------------------------- 1 | 2 | from django.http import HttpResponse 3 | from time import sleep 4 | 5 | def fake_long_request(request): 6 | seconds = int(request.GET['seconds']) 7 | sleep(seconds) 8 | html = "Slept %s seconds." % seconds 9 | return HttpResponse(html) -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/settings.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | DEBUG = True 5 | TEMPLATE_DEBUG = DEBUG 6 | 7 | ADMINS = ( 8 | # ('Your Name', 'your_email@example.com'), 9 | ) 10 | 11 | MANAGERS = ADMINS 12 | 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', 16 | 'NAME': ':memory:', 17 | 'USER': '', 18 | 'PASSWORD': '', 19 | 'HOST': '', 20 | 'PORT': '', 21 | } 22 | } 23 | 24 | TIME_ZONE = 'America/Chicago' 25 | LANGUAGE_CODE = 'en-us' 26 | SITE_ID = 1 27 | USE_I18N = True 28 | USE_L10N = True 29 | MEDIA_ROOT = '' 30 | MEDIA_URL = '' 31 | STATIC_ROOT = '' 32 | STATIC_URL = '/static/' 33 | ADMIN_MEDIA_PREFIX = '/static/admin/' 34 | STATICFILES_DIRS = () 35 | STATICFILES_FINDERS = ( 36 | 'django.contrib.staticfiles.finders.FileSystemFinder', 37 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 38 | ) 39 | SECRET_KEY = '^kg0a9j$s^yja&ruzp9w#9j2k^c!mt9z3n@2j$zqgcywf#-wa2' 40 | TEMPLATE_LOADERS = ( 41 | 'django.template.loaders.filesystem.Loader', 42 | 'django.template.loaders.app_directories.Loader', 43 | ) 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.contrib.sessions.middleware.SessionMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | ) 51 | ROOT_URLCONF = 'mockapi.urls' 52 | TEMPLATE_DIRS = () 53 | FIXTURE_DIRS = ( 54 | os.path.join(os.path.dirname(__file__), 'fixtures'), 55 | ) 56 | 57 | INSTALLED_APPS = ( 58 | 'django.contrib.auth', 59 | 'django.contrib.contenttypes', 60 | 'django.contrib.sessions', 61 | 'django.contrib.sites', 62 | 'django.contrib.messages', 63 | 'django.contrib.staticfiles', 64 | 'tastypie', 65 | 'mockapi.projects', 66 | ) 67 | LOGGING = { 68 | 'version': 1, 69 | 'disable_existing_loggers': False, 70 | 'handlers': { 71 | 'mail_admins': { 72 | 'level': 'ERROR', 73 | 'class': 'django.utils.log.AdminEmailHandler' 74 | } 75 | }, 76 | 'loggers': { 77 | 'django.request': { 78 | 'handlers': ['mail_admins'], 79 | 'level': 'ERROR', 80 | 'propagate': True, 81 | }, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /utils/drest.mockapi/mockapi/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.http import HttpResponse 3 | from django.conf.urls import patterns, include, url 4 | from mockapi.api import v0_api 5 | 6 | def render_null(request): 7 | return HttpResponse('') 8 | 9 | urlpatterns = patterns('', 10 | url(r'^api/', include(v0_api.urls)), 11 | url(r'^favicon.ico/$', render_null), 12 | url(r'^fake_long_request/$', 13 | 'mockapi.projects.views.fake_long_request') 14 | ) 15 | -------------------------------------------------------------------------------- /utils/drest.mockapi/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-tastypie 3 | python-digest 4 | mimeparse 5 | mock 6 | -------------------------------------------------------------------------------- /utils/drest.mockapi/setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import setup, find_packages 3 | import sys, os 4 | 5 | VERSION = '0.9.13' 6 | 7 | setup(name='drest.mockapi', 8 | version=VERSION, 9 | description="dRest Mock API for Testing", 10 | long_description="dRest Mock API for Testing", 11 | classifiers=[], 12 | keywords='', 13 | author='BJ Dierkes', 14 | author_email='wdierkes@5dollarwhitebox.org', 15 | url='http://github.com/derks/drest', 16 | license='None', 17 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 18 | include_package_data=True, 19 | zip_safe=False, 20 | test_suite='nose.collector', 21 | install_requires=[ 22 | "django", 23 | "django-tastypie", 24 | "python-digest", 25 | ], 26 | setup_requires=[ 27 | ], 28 | entry_points=""" 29 | """, 30 | namespace_packages=[], 31 | ) 32 | -------------------------------------------------------------------------------- /utils/make-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z $1 ]; then 4 | echo 'a version argument is required.' 5 | exit 1 6 | fi 7 | 8 | 9 | #status=$(git status --porcelain) 10 | #version=$(cat src/drest/setup.py | grep VERSION | head -n1 | awk -F \' {' print $2 '}) 11 | version=$1 12 | 13 | res=$(git tag | grep $version) 14 | if [ $? != 0 ]; then 15 | echo "Git tag ${version} does not exist." 16 | exit 17 | fi 18 | 19 | # run tests 20 | ./utils/run-tests.sh 21 | 22 | short=$(echo $version | awk -F . {' print $1"."$2 '}) 23 | dir=~/drest-${version} 24 | tmpdir=$(mktemp -d -t drest-$version) 25 | 26 | #if [ "${status}" != "" ]; then 27 | # echo 28 | # echo "WARNING: not all changes committed" 29 | #fi 30 | 31 | mkdir ${dir} 32 | mkdir ${dir}/doc 33 | mkdir ${dir}/sources 34 | 35 | # all 36 | git archive ${version} --prefix=drest-${version}/ | gzip > ${dir}/sources/drest-${version}.tar.gz 37 | cp -a ${dir}/sources/drest-${version}.tar.gz $tmpdir/ 38 | 39 | pushd $tmpdir 40 | tar -zxvf drest-${version}.tar.gz 41 | pushd drest-${version}/ 42 | sphinx-build doc/source ${dir}/doc 43 | popd 44 | popd 45 | -------------------------------------------------------------------------------- /utils/run-mockapi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting drest.mockapi..." 4 | pushd utils/drest.mockapi 5 | pip install -r requirements.txt 6 | python setup.py develop --no-deps 7 | python mockapi/manage.py testserver fixtures/initial_data.json DREST_MOCKAPI_PROCESS 8 | sleep 5 9 | popd 10 | -------------------------------------------------------------------------------- /utils/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$VIRTUAL_ENV" ]; then 4 | echo "Not running in a virtualenv??? You've got 10 seconds to CTRL-C ..." 5 | sleep 10 6 | virtualenv .env/ 7 | source .env/bin/activate 8 | fi 9 | 10 | pip install nose coverage 11 | pip install -r requirements.txt 12 | python setup.py develop --no-deps 13 | 14 | if [ "$1" == "--without-mockapi" ]; then 15 | echo "Not running drest.mockapi..." 16 | else 17 | ./utils/run-mockapi.sh DREST_MOCKAPI_PROCESS 2>/dev/null 1>/dev/null & 18 | sleep 5 19 | fi 20 | 21 | rm -rf coverage_report/ 22 | coverage erase 23 | python setup.py nosetests 24 | RET=$? 25 | 26 | # This is a hack to wait for tests to run 27 | sleep 5 28 | 29 | if [ "$1" == "--without-mockapi" ]; then 30 | echo "Not killing drest.mockapi (I didn't start it) ..." 31 | else 32 | echo "Killing drest.mockapi..." 33 | # Then kill the mock api 34 | ps auxw \ 35 | | grep 'DREST_MOCKAPI_PROCESS' \ 36 | | awk {' print $2 '} \ 37 | | xargs kill 2>/dev/null 1>/dev/null 38 | fi 39 | 40 | echo 41 | if [ "$RET" == "0" ]; then 42 | echo "TESTS PASSED OK" 43 | else 44 | echo "TESTS FAILED" 45 | fi 46 | echo 47 | 48 | exit $RET 49 | -------------------------------------------------------------------------------- /utils/travis-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install this to the default virtualenv - it fails on python3 which is why 4 | # we put it here rather than in .travis.yml 5 | pip install simplejson --use-mirrors 6 | pip install -r requirements.txt --use-mirrors 7 | 8 | 9 | # forces python 3 builds to run the mockapi as python 2.7 10 | source /home/vagrant/virtualenv/python2.7/bin/activate 11 | pip install -r ./utils/drest.mockapi/requirements.txt --use-mirrors 12 | ./utils/run-mockapi.sh DREST_MOCKAPI_PROCESS 2>./mockapi.err 1>./mockapi.out & 13 | -------------------------------------------------------------------------------- /utils/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bash ./utils/travis-bootstrap.sh 4 | sleep 10 5 | bash ./utils/run-tests.sh --without-mockapi 6 | RET="$?" 7 | 8 | if [ "$RET" != "0" ]; then 9 | echo '----------------------------------------------------------------------' 10 | echo 'MOCKAPI STDOUT' 11 | echo '----------------------------------------------------------------------' 12 | cat ./mockapi.out 13 | echo 14 | echo '----------------------------------------------------------------------' 15 | echo 'MOCKAPI STDERR' 16 | echo '----------------------------------------------------------------------' 17 | cat ./mockapi.err 18 | echo 19 | exit 1 20 | else 21 | exit 0 22 | fi --------------------------------------------------------------------------------