├── .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 | [](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
--------------------------------------------------------------------------------