[Python-modules-commits] [python-restless] 01/07: Import python-restless_2.0.3.orig.tar.gz

Wolfgang Borgert debacle at moszumanska.debian.org
Mon Dec 26 03:21:02 UTC 2016


This is an automated email from the git hooks/post-receive script.

debacle pushed a commit to branch master
in repository python-restless.

commit 15a5e04ae4fc0a19a8d512bbf6ccec7e4cb618a4
Author: W. Martin Borgert <debacle at debian.org>
Date:   Mon Dec 26 02:41:30 2016 +0000

    Import python-restless_2.0.3.orig.tar.gz
---
 .gitignore                    |   8 +++-
 .travis.yml                   |  13 +++---
 AUTHORS                       |  19 ++++++++
 README.rst                    |  40 +++++++----------
 docs/conf.py                  |  11 ++---
 docs/cookbook.rst             |   8 ++++
 docs/extending.rst            |   8 ++--
 docs/index.rst                |   5 ++-
 docs/releasenotes/v2.0.2.rst  |  36 +++++++++++++++
 docs/releasenotes/v2.0.3.rst  |  14 ++++++
 docs/tutorial.rst             |  14 +++---
 examples/django/posts/urls.py |   6 +--
 restless/__init__.py          |   3 +-
 restless/constants.py         |  14 ++++++
 restless/dj.py                |  22 +++++----
 restless/exceptions.py        |  80 ++++++++++++++++++++++++++++++++-
 restless/fl.py                |  10 ++++-
 restless/it.py                |  13 ++++--
 restless/preparers.py         |   4 +-
 restless/pyr.py               |  13 ++++--
 restless/resources.py         |  27 ++++-------
 restless/serializers.py       |  10 +++--
 restless/tnd.py               |  12 +++--
 restless/utils.py             |   4 +-
 rtfd-requirements.txt         |   1 +
 setup.py                      |   5 ++-
 test2_requirements.txt        |   3 --
 test3_requirements.txt        |   8 ----
 tests/test_dj.py              |  71 ++++++++++++++++++++++-------
 tests/test_fl.py              |  27 ++++++-----
 tests/test_it.py              |  19 ++++----
 tests/test_preparers.py       |  10 ++++-
 tests/test_pyr.py             |  26 ++++++-----
 tests/test_resources.py       |   8 ++--
 tests/test_serializers.py     |   7 +++
 tests/test_tnd.py             | 102 ++++++++++++++++++++++++++++++++++--------
 tox.ini                       |  29 ++++++++++++
 37 files changed, 524 insertions(+), 186 deletions(-)

diff --git a/.gitignore b/.gitignore
index 68e0779..69095b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,6 @@
 __pycache__
 build
 dist
-env2
-env3
 docs/_build
 tests/.coverage
 tests/cover
@@ -12,3 +10,9 @@ tests/htmlcov
 .coverage
 htmlcov
 *.egg-info
+.cache/
+.eggs/
+.tox/
+*.swp
+.idea/
+*.whl
diff --git a/.travis.yml b/.travis.yml
index a404880..f5d08bc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,16 @@
 language: python
 
 python:
+  - "3.5"
   - "3.4"
-  - "3.3"
   - "2.7"
+  - "pypy"
+  - "pypy3"
 
 # command to install dependencies
 install:
-    - pip install -r test${TRAVIS_PYTHON_VERSION:0:1}_requirements.txt
-    - pip install coveralls
+    - pip install tox-travis
 
 # command to run tests
 script:
-    - py.test --cov=restless
-
-# command to notify coveralls
-after_success:
-    - coveralls
+    - tox
diff --git a/AUTHORS b/AUTHORS
index fe23116..fee754d 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -2,7 +2,26 @@ Primary authors:
 
 * Daniel Lindsley
 
+Maintainers:
+
+* Bruno Marques
+* Sergio Oliveira
 
 Contributors:
 
 * Matt George
+* socketubs
+* mission-liao
+* frewsxcv
+* viniciuscainelli
+* schmitch
+* karmux
+* binarydud
+* issackelly
+* jmwohl
+* jphalip
+* mindflayer
+* czartur
+* leonsmith
+* PabloCastellano
+* skevy
diff --git a/README.rst b/README.rst
index f92d619..ac8fa00 100644
--- a/README.rst
+++ b/README.rst
@@ -4,8 +4,6 @@ restless
 
 .. image:: https://travis-ci.org/toastdriven/restless.png?branch=master
         :target: https://travis-ci.org/toastdriven/restless
-.. image:: https://coveralls.io/repos/toastdriven/restless/badge.png
-        :target: https://coveralls.io/r/toastdriven/restless
 
 A lightweight REST miniframework for Python.
 
@@ -21,6 +19,7 @@ many other Python web frameworks. Based on the lessons learned from Tastypie_
 .. _Itty: https://pypi.python.org/pypi/itty
 .. _Tastypie: http://tastypieapi.org/
 .. _Tornado: http://www.tornadoweb.org/
+.. _tox: https://tox.readthedocs.io/
 
 
 Features
@@ -29,7 +28,7 @@ Features
 * Small, fast codebase
 * JSON output by default, but overridable
 * RESTful
-* Python 3.3+ (with shims to make broke-ass Python 2.6+ work)
+* Python 3.2+ (with shims to make broke-ass Python 2.6+ work)
 * Flexible
 
 
@@ -130,15 +129,15 @@ Hooking it up:
 .. code:: python
 
     # api/urls.py
-    from django.conf.urls.default import url, patterns, include
+    from django.conf.urls.default import url, include
 
     from posts.api import PostResource
 
-    urlpatterns = patterns('',
+    urlpatterns = [
         # The usual suspects, then...
 
         url(r'^api/posts/', include(PostResource.urls())),
-    )
+    ]
 
 
 Licence
@@ -150,24 +149,15 @@ BSD
 Running the Tests
 =================
 
-Getting the tests running looks like:
+The test suite uses tox_ for simultaneous support of multiple versions of both
+Python and Django. The current versions of Python supported are:
 
-.. code:: sh
+* CPython 2.7
+* CPython 3.4
+* CPython 3.5
+* PyPy (Python 2.7)
+* PyPy3 (Python 3.2)
+* PyPy3 beta (Python 3.3)
 
-    $ virtualenv -p python3 env3
-    $ . env3/bin/activate
-    $ pip install -r test3_requirements.txt
-    $ export PYTHONPATH=`pwd`
-    $ py.test -s -v --cov=restless --cov-report=html tests
-
-For Python 2:
-
-.. code:: sh
-
-    $ virtualenv env2
-    $ . env2/bin/activate
-    $ pip install -r test2_requirements.txt
-    $ export PYTHONPATH=`pwd`
-    $ py.test -s -v --cov=restless --cov-report=html tests
-
-Coverage is at about 94%, so please don't make it worse. :D
+You just need to install the Python interpreters above and the `tox` package
+(available via `pip`), then run the `tox` command.
diff --git a/docs/conf.py b/docs/conf.py
index 48a8be4..74054d1 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -52,10 +52,11 @@ copyright = u'2014, Daniel Lindsley'
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
-# The short X.Y version.
-version = '2.0.1'
+import restless  # noqa
 # The full version, including alpha/beta/rc tags.
-release = '2.0.1'
+release = restless.VERSION
+# The short X.Y version.
+version = restless.VERSION
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
@@ -243,8 +244,8 @@ man_pages = [
 #  dir menu entry, description, category)
 texinfo_documents = [
   ('index', 'restless', u'restless Documentation',
-   u'Daniel Lindsley', 'restless', 'One line description of project.',
-   'Miscellaneous'),
+   u'Daniel Lindsley', 'restless',
+   u'A lightweight REST miniframework for Python.', 'Miscellaneous'),
 ]
 
 # Documents to append as an appendix to all manuals.
diff --git a/docs/cookbook.rst b/docs/cookbook.rst
index 6b5967d..eeb8637 100644
--- a/docs/cookbook.rst
+++ b/docs/cookbook.rst
@@ -18,3 +18,11 @@ do something like::
         def is_authenticated(self):
             return self.request.user.is_authenticated()
 
+If you need a more fine graned authentication you could check your current endpoint and do something like that:
+
+    class MyResource(DjangoResource):
+        def is_authenticated(self):
+            if self.endpoint in ('update', 'create'):
+                return self.request.user.is_authenticated()
+            else:
+                return True
diff --git a/docs/extending.rst b/docs/extending.rst
index 29c9670..02f5d66 100644
--- a/docs/extending.rst
+++ b/docs/extending.rst
@@ -92,7 +92,7 @@ Finally, it's just a matter of hooking up the URLs as well. You can do this
 manually or (once again) by extending a built-in method.::
 
     # Add the correct import here.
-    from django.conf.urls import patterns, url
+    from django.conf.urls import url
 
     from restless.dj import DjangoResource
     from restless.resources import skip_prepare
@@ -128,9 +128,9 @@ manually or (once again) by extending a built-in method.::
         @classmethod
         def urls(cls, name_prefix=None):
             urlpatterns = super(PostResource, cls).urls(name_prefix=name_prefix)
-            return urlpatterns + patterns('',
+            return urlpatterns + [
                 url(r'^schema/$', cls.as_view('schema'), name=cls.build_url_name('schema', name_prefix)),
-            )
+            ]
 
 .. note::
 
@@ -374,7 +374,7 @@ up your code into a validation method. An example of this might look like...::
     from django.forms import ModelForm
 
     from restless.dj import DjangoResource
-    from restless.exceptions import HttpError
+    from restless.exceptions import BadRequest
 
 
     class UserForm(ModelForm):
diff --git a/docs/index.rst b/docs/index.rst
index ef8ed03..075a4f9 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -14,6 +14,7 @@ many other Python web frameworks. Based on the lessons learned from Tastypie_
 .. _Itty: https://pypi.python.org/pypi/itty
 .. _Tastypie: http://tastypieapi.org/
 .. _Tornado: http://www.tornadoweb.org/
+.. _tox: https://tox.readthedocs.io/
 
 
 Features
@@ -22,7 +23,7 @@ Features
 * Small, fast codebase
 * JSON output by default, but overridable
 * RESTful
-* Python 3.3+ (with shims to make broke-ass Python 2.6+ work)
+* Python 3.2+ (with shims to make broke-ass Python 2.6+ work)
 * Flexible
 
 
@@ -75,6 +76,8 @@ Release Notes
 .. toctree::
    :maxdepth: 1
 
+   releasenotes/v2.0.3
+   releasenotes/v2.0.2
    releasenotes/v2.0.1
    releasenotes/v2.0.0
    releasenotes/v1.4.0
diff --git a/docs/releasenotes/v2.0.2.rst b/docs/releasenotes/v2.0.2.rst
new file mode 100644
index 0000000..1ecdc89
--- /dev/null
+++ b/docs/releasenotes/v2.0.2.rst
@@ -0,0 +1,36 @@
+restless v2.0.2
+===============
+
+:date: 2016-11-14
+
+This release makes some long-needed changes on error handling for ``Resource``
+and its subclasses, plus support for both Django >= 1.9 and Tornado >= 4.0 and
+allowing alphanumeric PKs on all supported frameworks.
+
+
+Features
+--------
+
+* Allowed PKs with dashes and alphanumeric digits. (SHA: e52333b)
+* Reworked test suite so that it uses ``tox`` for simultaneously testing on
+  CPython and PyPy, both 2.x and 3.x (SHA: 2035e21, SHA: 9ca0e8c, SHA: 3915980
+  & SHA: a1d2d96)
+* Reworked ``Resource`` so that it throws a ``NotImplementedError`` instead of
+  returning an ``HttpResponse`` from Django. (SHA: 27859c8)
+* Added several ``HttpError`` subclasses. (SHA: e2aff93)
+* Changed ``Resource`` so that it allows any serializable object on the response
+  body. (SHA: 1e3522b & SHA: b70a492)
+
+
+Bugfixes
+--------
+
+* Changed ``JSONSerializer`` to throw a ``BadRequest`` upon a serialization
+  error. (SHA: 8471463)
+* Updated ``DjangoResource`` to use lists instead of the deprecated
+  ``django.conf.urls.patterns`` object. (SHA: f166e4d & SHA: f94c500)
+* Fixed ``FieldsPreparer`` behavior when parsing objects with a custom
+  ``__getattr__``. (SHA: 665ef31)
+* Applied Debian's fix to Tornado tests for version 4.0.0 onwards. (SHA: 372e00a)
+* Skips tests for all unavailable frameworks. (SHA: 8b81b17)
+
diff --git a/docs/releasenotes/v2.0.3.rst b/docs/releasenotes/v2.0.3.rst
new file mode 100644
index 0000000..51f47fd
--- /dev/null
+++ b/docs/releasenotes/v2.0.3.rst
@@ -0,0 +1,14 @@
+restless v2.0.3
+===============
+
+:date: 2016-11-21
+
+This release adds a change which was in restkiss v2.0.2 but got lost in the
+backporting process - sorry, everybody!
+
+
+Features
+--------
+
+* Changed all ``Resource`` subclasses so that a 204 No Content response sends
+  ``text/plain`` on ``Content-Type``. (SHA: 116da9f & SHA: b10be61)
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index ececea8..e6b8521 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -81,7 +81,7 @@ behavior of the various `REST methods`_.
 .. _Flask: http://flask.pocoo.org/
 .. _Pyramid: http://www.pylonsproject.org/
 .. _Itty: https://pypi.python.org/pypi/itty
-.. _`REST methods`: http://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_Web_Services
+.. _`REST methods`: http://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_web_services
 
 
 About Resources
@@ -252,17 +252,17 @@ practice would be to create a URLconf just for the API portion of your site.::
 
     # The ``settings.ROOT_URLCONF`` file
     # myproject/urls.py
-    from django.conf.urls import patterns, url, include
+    from django.conf.urls import url, include
 
     # Add this!
     from posts.api import PostResource
 
-    urlpatterns = patterns('',
+    urlpatterns = [
         # The usual fare, then...
 
         # Add this!
         url(r'api/posts/', include(PostResource.urls())),
-    )
+    ]
 
 Note that unlike some other CBVs (admin specifically), the ``urls`` here is a
 **METHOD**, not an attribute/property. Those parens are important!
@@ -272,13 +272,13 @@ Manual URLconfs
 
 You can also manually hook up URLs by specifying something like::
 
-    urlpatterns = patterns('',
+    urlpatterns = [
         # ...
 
         # Identical to the above.
         url(r'api/posts/$', PostResource.as_list(), name='api_post_list'),
         url(r'api/posts/(?P<pk>\d+)/$', PostResource.as_detail(), name='api_post_detail'),
-    )
+    ]
 
 
 Testing the API
@@ -597,7 +597,7 @@ http://127.0.0.1:8000/api/posts/3/).
 We can also update it. Restless expects **complete** bodies (don't try to send
 partial updates, that's typically reserved for ``PATCH``).::
 
-    $ curl -X POST -H "Content-Type: application/json" -d '{"title": "Another new library released!", "author": "daniel", "body": "I just released a new piece of software!"}' http://127.0.0.1:8000/api/posts/3/
+    $ curl -X PUT -H "Content-Type: application/json" -d '{"title": "Another new library released!", "author": "daniel", "body": "I just released a new piece of software!"}' http://127.0.0.1:8000/api/posts/3/
 
 And we can delete our new data if we decide we don't like it.::
 
diff --git a/examples/django/posts/urls.py b/examples/django/posts/urls.py
index 7e78962..4d235aa 100644
--- a/examples/django/posts/urls.py
+++ b/examples/django/posts/urls.py
@@ -1,12 +1,12 @@
-from django.conf.urls import patterns, url, include
+from django.conf.urls import url, include
 
 from .api import PostResource
 
 
-urlpatterns = patterns('',
+urlpatterns = [
     url(r'^posts/', include(PostResource.urls())),
 
     # Alternatively, if you don't like the defaults...
     # url(r'^posts/$', PostResource.as_list(), name='api_posts_list'),
     # url(r'^posts/(?P<pk>\d+)/$', PostResource.as_detail(), name='api_posts_detail'),
-)
+]
diff --git a/restless/__init__.py b/restless/__init__.py
index de706bf..c1ac75c 100644
--- a/restless/__init__.py
+++ b/restless/__init__.py
@@ -1,6 +1,7 @@
 __author__ = 'Daniel Lindsley'
 __license__ = 'BSD'
-__version__ = (2, 0, 1)
+__version__ = (2, 0, 3)
+VERSION = '.'.join(map(str, __version__))
 
 
 from .resources import Resource
diff --git a/restless/constants.py b/restless/constants.py
index a715e54..f322d1e 100644
--- a/restless/constants.py
+++ b/restless/constants.py
@@ -6,8 +6,22 @@ NO_CONTENT = 204
 
 BAD_REQUEST = 400
 UNAUTHORIZED = 401
+FORBIDDEN = 403
 NOT_FOUND = 404
 METHOD_NOT_ALLOWED = 405
+NOT_ACCEPTABLE = 406
+CONFLICT = 409
+GONE = 410
+PRECONDITION_FAILED = 412
+UNSUPPORTED_MEDIA_TYPE = 415
+EXPECTATION_FAILED = 417
+I_AM_A_TEAPOT = 418
+UNPROCESSABLE_ENTITY = 422
+LOCKED = 423
+FAILED_DEPENDENCY = 424
+TOO_MANY_REQUESTS = 429
+UNAVAILABLE_FOR_LEGAL_REASONS = 451
 
 APPLICATION_ERROR = 500
 METHOD_NOT_IMPLEMENTED = 501
+UNAVAILABLE = 503
diff --git a/restless/dj.py b/restless/dj.py
index 3ffb778..4503b4c 100644
--- a/restless/dj.py
+++ b/restless/dj.py
@@ -1,11 +1,12 @@
 import six
 
 from django.conf import settings
-from django.conf.urls import patterns, url
+from django.conf.urls import url
 from django.core.exceptions import ObjectDoesNotExist
 from django.http import HttpResponse, Http404
 from django.views.decorators.csrf import csrf_exempt
 
+from .constants import OK, NO_CONTENT
 from .exceptions import NotFound
 from .resources import Resource
 
@@ -27,12 +28,15 @@ class DjangoResource(Resource):
         return csrf_exempt(super(DjangoResource, self).as_detail(*args, **kwargs))
 
     def is_debug(self):
-        # By default, Django-esque.
         return settings.DEBUG
 
-    def build_response(self, data, status=200):
-        # By default, Django-esque.
-        resp = HttpResponse(data, content_type='application/json')
+    def build_response(self, data, status=OK):
+        if status == NO_CONTENT:
+            # Avoid crashing the client when it tries to parse nonexisting JSON.
+            content_type = 'text/plain'
+        else:
+            content_type = 'application/json'
+        resp = HttpResponse(data, content_type=content_type)
         resp.status_code = status
         return resp
 
@@ -82,9 +86,9 @@ class DjangoResource(Resource):
             ``api_blogpost_list``
         :type name_prefix: string
 
-        :returns: A ``patterns`` object for ``include(...)``
+        :returns: A list of ``url`` objects for ``include(...)``
         """
-        return patterns('',
+        return [
             url(r'^$', cls.as_list(), name=cls.build_url_name('list', name_prefix)),
-            url(r'^(?P<pk>\d+)/$', cls.as_detail(), name=cls.build_url_name('detail', name_prefix)),
-        )
+            url(r'^(?P<pk>[\w-]+)/$', cls.as_detail(), name=cls.build_url_name('detail', name_prefix)),
+        ]
diff --git a/restless/exceptions.py b/restless/exceptions.py
index f1ad719..79ca042 100644
--- a/restless/exceptions.py
+++ b/restless/exceptions.py
@@ -1,5 +1,10 @@
-from .constants import APPLICATION_ERROR, UNAUTHORIZED, NOT_FOUND, BAD_REQUEST
-from .constants import METHOD_NOT_ALLOWED, METHOD_NOT_IMPLEMENTED
+from .constants import (APPLICATION_ERROR, UNAUTHORIZED, NOT_FOUND, BAD_REQUEST,
+                        FORBIDDEN, NOT_ACCEPTABLE, GONE, PRECONDITION_FAILED,
+                        CONFLICT, UNSUPPORTED_MEDIA_TYPE, EXPECTATION_FAILED,
+                        I_AM_A_TEAPOT, TOO_MANY_REQUESTS, UNPROCESSABLE_ENTITY,
+                        UNAVAILABLE_FOR_LEGAL_REASONS, FAILED_DEPENDENCY,
+                        LOCKED)
+from .constants import METHOD_NOT_ALLOWED, METHOD_NOT_IMPLEMENTED, UNAVAILABLE
 
 
 class RestlessError(Exception):
@@ -44,6 +49,11 @@ class Unauthorized(HttpError):
     msg = "Unauthorized."
 
 
+class Forbidden(HttpError):
+    status = FORBIDDEN
+    msg = "Permission denied."
+
+
 class NotFound(HttpError):
     status = NOT_FOUND
     msg = "Resource not found."
@@ -54,6 +64,72 @@ class MethodNotAllowed(HttpError):
     msg = "The specified HTTP method is not allowed."
 
 
+class NotAcceptable(HttpError):
+    # TODO: make serializers handle it?
+    status = NOT_ACCEPTABLE
+    msg = "Unable to send content specified on the request's Accept header(s)."
+
+
+class Conflict(HttpError):
+    status = CONFLICT
+    msg = "There was a conflict when processing the request."
+
+
+class Gone(HttpError):
+    status = GONE
+    msg = "Resource removed permanently."
+
+
+class PreconditionFailed(HttpError):
+    status = PRECONDITION_FAILED
+    msg = "Unable to satisfy one or more request preconditions."
+
+
+class UnsupportedMediaType(HttpError):
+    status = UNSUPPORTED_MEDIA_TYPE
+    msg = "Type of media provided on request is not supported."
+
+
+class ExpectationFailed(HttpError):
+    status = EXPECTATION_FAILED
+    msg = "Unable to satisfy requirements of Expect header."
+
+
+class IAmATeapot(HttpError):
+    status = I_AM_A_TEAPOT
+    msg = "This is a teapot; do not attempt to brew coffee with it."
+
+
+class UnprocessableEntity(HttpError):
+    status = UNPROCESSABLE_ENTITY
+    msg = "Request cannot be followed due to a semantic error."
+
+
+class Locked(HttpError):
+    status = LOCKED
+    msg = "Resource is locked."
+
+
+class FailedDependency(HttpError):
+    status = FAILED_DEPENDENCY
+    msg = "Request failed due to a previous failed request."
+
+
+class TooManyRequests(HttpError):
+    status = TOO_MANY_REQUESTS
+    msg = "There was a conflict when processing the request."
+
+
+class UnavailableForLegalReasons(HttpError):
+    status = UNAVAILABLE_FOR_LEGAL_REASONS
+    msg = "Resource made unavailable by a legal decision."
+
+
 class MethodNotImplemented(HttpError):
     status = METHOD_NOT_IMPLEMENTED
     msg = "The specified HTTP method is not implemented."
+
+
+class Unavailable(HttpError):
+    status = UNAVAILABLE
+    msg = "There was a conflict when processing the request."
diff --git a/restless/fl.py b/restless/fl.py
index 851e596..ea34c06 100644
--- a/restless/fl.py
+++ b/restless/fl.py
@@ -1,6 +1,7 @@
 from flask import make_response
 from flask import request
 
+from .constants import OK, NO_CONTENT
 from .resources import Resource
 
 
@@ -44,9 +45,14 @@ class FlaskResource(Resource):
         from flask import current_app
         return current_app.debug
 
-    def build_response(self, data, status=200):
+    def build_response(self, data, status=OK):
+        if status == NO_CONTENT:
+            # Avoid crashing the client when it tries to parse nonexisting JSON.
+            content_type = 'text/plain'
+        else:
+            content_type = 'application/json'
         return make_response(data, status, {
-            'Content-Type': 'application/json'
+            'Content-Type': content_type,
         })
 
     @classmethod
diff --git a/restless/it.py b/restless/it.py
index 277c17b..36e5566 100644
--- a/restless/it.py
+++ b/restless/it.py
@@ -1,6 +1,8 @@
 import re
 
 import itty
+
+from restless.constants import OK, NO_CONTENT
 from restless.resources import Resource
 
 
@@ -16,8 +18,13 @@ class IttyResource(Resource):
     def is_debug(self):
         return self.debug
 
-    def build_response(self, data, status=200):
-        return itty.Response(data, status=status, content_type='application/json')
+    def build_response(self, data, status=OK):
+        if status == NO_CONTENT:
+            # Avoid crashing the client when it tries to parse nonexisting JSON.
+            content_type = 'text/plain'
+        else:
+            content_type = 'application/json'
+        return itty.Response(data, status=status, content_type=content_type)
 
     @classmethod
     def setup_urls(cls, rule_prefix):
@@ -30,7 +37,7 @@ class IttyResource(Resource):
         :returns: ``None``
         """
         list_url = "%s" % itty.add_slash(rule_prefix)
-        detail_url = "%s" % itty.add_slash(rule_prefix + "/(?P<pk>\d+)")
+        detail_url = "%s" % itty.add_slash(rule_prefix + "/(?P<pk>[\w-]+)")
 
         list_re = re.compile("^%s$" % list_url)
         detail_re = re.compile("^%s$" % detail_url)
diff --git a/restless/preparers.py b/restless/preparers.py
index 5d9fed9..1373fa3 100644
--- a/restless/preparers.py
+++ b/restless/preparers.py
@@ -101,10 +101,10 @@ class FieldsPreparer(Preparer):
         part = parts[0]
         remaining_lookup = '.'.join(parts[1:])
 
-        if hasattr(data, 'keys') and hasattr(data, '__getitem__'):
+        if callable(getattr(data, 'keys', None)) and hasattr(data, '__getitem__'):
             # Dictionary enough for us.
             value = data[part]
-        else:
+        elif data is not None:
             # Assume it's an object.
             value = getattr(data, part)
 
diff --git a/restless/pyr.py b/restless/pyr.py
index 2d6d162..a94faf2 100644
--- a/restless/pyr.py
+++ b/restless/pyr.py
@@ -1,7 +1,9 @@
 from pyramid.response import Response
 
+from .constants import OK, NO_CONTENT
 from .resources import Resource
 
+
 class PyramidResource(Resource):
     """
     A Pyramid-specific ``Resource`` subclass.
@@ -26,8 +28,13 @@ class PyramidResource(Resource):
 
         return _wrapper
 
-    def build_response(self, data, status=200):
-        resp = Response(data, status_code=status, content_type="application/json")
+    def build_response(self, data, status=OK):
+        if status == NO_CONTENT:
+            # Avoid crashing the client when it tries to parse nonexisting JSON.
+            content_type = 'text/plain'
+        else:
+            content_type = 'application/json'
+        resp = Response(data, status_code=status, content_type=content_type)
         return resp
 
     @classmethod
@@ -42,7 +49,7 @@ class PyramidResource(Resource):
         :param routename_prefix: (Optional) A prefix for the URL's name (for
             resolving). The default is ``None``, which will autocreate a prefix
             based on the class name. Ex: ``BlogPostResource`` ->
-            ``api_blog_post_list``
+            ``api_blogpost_list``
         :type routename_prefix: string
 
         :returns: The final name
diff --git a/restless/resources.py b/restless/resources.py
index c260e7f..f6ad6c9 100644
--- a/restless/resources.py
+++ b/restless/resources.py
@@ -4,7 +4,7 @@ import sys
 from .constants import OK, CREATED, ACCEPTED, NO_CONTENT
 from .data import Data
 from .exceptions import MethodNotImplemented, Unauthorized
-from .preparers import Preparer, FieldsPreparer
+from .preparers import Preparer
 from .serializers import JSONSerializer
 from .utils import format_traceback
 
@@ -145,9 +145,8 @@ class Resource(object):
         """
         Returns the HTTP method for the current request.
 
-        The default implementation is Django-specific, so if you're integrating
-        with a new web framework, you'll need to override this method within
-        your subclass.
+        If you're integrating with a new web framework, you might need to
+        override this method within your subclass.
 
         :returns: The HTTP method in uppercase
         :rtype: string
@@ -161,9 +160,8 @@ class Resource(object):
 
         Useful for deserializing the content the user sent (typically JSON).
 
-        The default implementation is Django-specific, so if you're integrating
-        with a new web framework, you'll need to override this method within
-        your subclass.
+        If you're integrating with a new web framework, you might need to
+        override this method within your subclass.
 
         :returns: The body of the request
         :rtype: string
@@ -175,9 +173,8 @@ class Resource(object):
         """
         Given some data, generates an HTTP response.
 
-        The default implementation is Django-specific, so if you're integrating
-        with a new web framework, you'll need to override this method within
-        your subclass.
+        If you're integrating with a new web framework, you **MUST**
+        override this method within your subclass.
 
         :param data: The body of the response to send
         :type data: string
@@ -188,13 +185,7 @@ class Resource(object):
 
         :returns: A response object
         """
-        # TODO: Remove the Django.
-        #       This should be plain old WSGI by default, if possible.
-        # By default, Django-esque.
-        from django.http import HttpResponse
-        resp = HttpResponse(data, content_type='application/json')
-        resp.status_code = status
-        return resp
+        raise NotImplementedError()
 
     def build_error(self, err):
         """
@@ -208,7 +199,7 @@ class Resource(object):
         :returns: A response object
         """
         data = {
-            'error': six.text_type(err),
+            'error': err.args[0],
         }
 
         if self.is_debug():
diff --git a/restless/serializers.py b/restless/serializers.py
index 89104ce..b372993 100644
--- a/restless/serializers.py
+++ b/restless/serializers.py
@@ -1,3 +1,4 @@
+from .exceptions import BadRequest
 from .utils import json, MoreTypesJSONEncoder
 
 
@@ -58,9 +59,12 @@ class JSONSerializer(Serializer):
         :returns: The deserialized data
         :rtype: ``list`` or ``dict``
         """
-        if isinstance(body, bytes):
-            return json.loads(body.decode('utf-8'))
-        return json.loads(body)
+        try:
+            if isinstance(body, bytes):
+                return json.loads(body.decode('utf-8'))
+            return json.loads(body)
+        except ValueError:
+            raise BadRequest('Request body is not valid JSON')
 
     def serialize(self, data):
         """
diff --git a/restless/tnd.py b/restless/tnd.py
index 00ed3c8..206163a 100644
--- a/restless/tnd.py
+++ b/restless/tnd.py
@@ -1,5 +1,5 @@
 from tornado import web, gen
-from .constants import OK
+from .constants import OK, NO_CONTENT
 from .resources import Resource
 from .exceptions import MethodNotImplemented, Unauthorized
 
@@ -128,8 +128,14 @@ class TornadoResource(Resource):
     def request_body(self):
         return self.request.body 
 
-    def build_response(self, data, status=200):
-        self.ref_rh.set_header("Content-Type", "application/json; charset=UTF-8")
+    def build_response(self, data, status=OK):
+        if status == NO_CONTENT:
+            # Avoid crashing the client when it tries to parse nonexisting JSON.
+            content_type = 'text/plain'
+        else:
+            content_type = 'application/json'
+        self.ref_rh.set_header("Content-Type", "{}; charset=UTF-8"
+                               .format(content_type))
 
         self.ref_rh.set_status(status)
         self.ref_rh.finish(data)
diff --git a/restless/utils.py b/restless/utils.py
index 00d8929..96acffa 100644
--- a/restless/utils.py
+++ b/restless/utils.py
@@ -1,6 +1,7 @@
 import datetime
 import decimal
 import traceback
+import uuid
 
 try:
     import json
@@ -18,12 +19,13 @@ class MoreTypesJSONEncoder(json.JSONEncoder):
         * ``datetime.date``
         * ``datetime.time``
         * ``decimal.Decimal``
+        * ``uuid.UUID``
 
     """
     def default(self, data):
         if isinstance(data, (datetime.datetime, datetime.date, datetime.time)):
             return data.isoformat()
-        elif isinstance(data, decimal.Decimal):
+        elif isinstance(data, decimal.Decimal) or isinstance(data, uuid.UUID):
             return str(data)
         else:
             return super(MoreTypesJSONEncoder, self).default(data)
diff --git a/rtfd-requirements.txt b/rtfd-requirements.txt
new file mode 100644
index 0000000..94a0e83
--- /dev/null
+++ b/rtfd-requirements.txt
@@ -0,0 +1 @@
+Django
diff --git a/setup.py b/setup.py
index 14c3354..44dbb9e 100644
--- a/setup.py
+++ b/setup.py
@@ -2,10 +2,12 @@
 # -*- coding: utf-8 -*-
 from setuptools import setup
 
+import restless
+
 
 setup(
     name='restless',
-    version='2.0.1',
+    version=restless.VERSION,
     description='A lightweight REST miniframework for Python.',
     author='Daniel Lindsley',
     author_email='daniel at toastdriven.com',
@@ -22,6 +24,7 @@ setup(
     ],
     tests_require=[
         'mock',
+        'tox',
     ],
     classifiers=[
         'Development Status :: 5 - Production/Stable',
diff --git a/test2_requirements.txt b/test2_requirements.txt
deleted file mode 100644
index bbed9a4..0000000
--- a/test2_requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
--r test3_requirements.txt
-
-itty==0.8.2
diff --git a/test3_requirements.txt b/test3_requirements.txt
deleted file mode 100644
index 677f055..0000000
--- a/test3_requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-six>=1.4.0
-pytest==2.5.2
-pytest-cov==1.6
-Django==1.6.1
-Flask==0.10.1
-pyramid==1.4.5
-tornado==3.2.2
-
diff --git a/tests/test_dj.py b/tests/test_dj.py
index bcdc5be..9aaf1bd 100644
--- a/tests/test_dj.py
+++ b/tests/test_dj.py
@@ -1,13 +1,18 @@
 import unittest
 
-from django.http import Http404
-from django.core.exceptions import ObjectDoesNotExist
+try:
+    from django.http import Http404
... 724 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-restless.git



More information about the Python-modules-commits mailing list