Giter Club home page Giter Club logo

restless's Introduction

restless

https://travis-ci.org/toastdriven/restless.svg?branch=master https://coveralls.io/repos/github/toastdriven/restless/badge.svg?branch=master

A lightweight REST miniframework for Python.

Documentation is at https://restless.readthedocs.io/.

Works great with Django, Flask, Pyramid, Tornado & Itty, but should be useful for many other Python web frameworks. Based on the lessons learned from Tastypie & other REST libraries.

Features

  • Small, fast codebase
  • JSON output by default, but overridable
  • RESTful
  • Python 3.6+
  • Django 2.2+
  • Flexible

Anti-Features

(Things that will never be added...)

  • Automatic ORM integration
  • Authorization (per-object or not)
  • Extensive filtering options
  • XML output (though you can implement your own)
  • Metaclasses
  • Mixins
  • HATEOAS

Why?

Quite simply, I care about creating flexible & RESTFul APIs. In building Tastypie, I tried to create something extremely complete & comprehensive. The result was writing a lot of hook methods (for easy extensibility) & a lot of (perceived) bloat, as I tried to accommodate for everything people might want/need in a flexible/overridable manner.

But in reality, all I really ever personally want are the RESTful verbs, JSON serialization & the ability of override behavior.

This one is written for me, but maybe it's useful to you.

Manifesto

Rather than try to build something that automatically does the typically correct thing within each of the views, it's up to you to implement the bodies of various HTTP methods.

Example code:

# posts/api.py
from django.contrib.auth.models import User

from restless.dj import DjangoResource
from restless.preparers import FieldsPreparer

from posts.models import Post


class PostResource(DjangoResource):
    # Controls what data is included in the serialized output.
    preparer = FieldsPreparer(fields={
        'id': 'id',
        'title': 'title',
        'author': 'user.username',
        'body': 'content',
        'posted_on': 'posted_on',
    })

    # GET /
    def list(self):
        return Post.objects.all()

    # GET /pk/
    def detail(self, pk):
        return Post.objects.get(id=pk)

    # POST /
    def create(self):
        return Post.objects.create(
            title=self.data['title'],
            user=User.objects.get(username=self.data['author']),
            content=self.data['body']
        )

    # PUT /pk/
    def update(self, pk):
        try:
            post = Post.objects.get(id=pk)
        except Post.DoesNotExist:
            post = Post()

        post.title = self.data['title']
        post.user = User.objects.get(username=self.data['author'])
        post.content = self.data['body']
        post.save()
        return post

    # DELETE /pk/
    def delete(self, pk):
        Post.objects.get(id=pk).delete()

Hooking it up:

# api/urls.py
from django.conf.urls.default import url, include

from posts.api import PostResource

urlpatterns = [
    # The usual suspects, then...

    url(r'^api/posts/', include(PostResource.urls())),
]

Licence

BSD

Running the Tests

The test suite uses tox for simultaneous support of multiple versions of both Python and Django. The current versions of Python supported are:

  • CPython 3.6
  • CPython 3.7
  • CPython 3.8
  • CPython 3.9
  • PyPy

You just need to install the Python interpreters above and the tox package (available via pip), then run the tox command.

restless's People

Contributors

anacarolinats avatar andreasnuesslein avatar artcz avatar binarymatt avatar cawal avatar elsaico avatar frewsxcv avatar hellokashif avatar hugovk avatar issackelly avatar jmwohl avatar jphalip avatar karmux avatar leonsmith avatar marcelo-theodoro avatar mindflayer avatar mission-liao avatar pablocastellano avatar r-lelis avatar schmitch avatar seocam avatar skevy avatar streeter avatar timgates42 avatar toastdriven avatar tonybajan avatar toxinu avatar viniciuscainelli avatar xarg avatar xiaoli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

restless's Issues

raise Unauthorized()

When doing auth with restless it raises a restless.exceptions.Unauthorized, however Flask knows only about werkzeug.exceptions.Unauthorized.

How would you recommend going about making without monkey-patching stuff?

API versions and throttling

Not sure if you plan to add support for api version and throttling. However, I'd be really interested in your opinion on the subject and your recommendation for restless-based projects.

JSON fields output not following the same order as defined

If I define a preparer like this:

preparer = FieldsPreparer(fields={
    'id': 'id',
    'city': 'city',
    'state': 'state'
})

The JSON output:

'{"objects": [{"city": "Sao Paulo",  "state": "SP", "id": 1}]}'

And I think the fields should be in the same order as I defined them. Is there a way to change this behavior?

By the way, this happens if I use an OrderedDict.

Bug in documentation/code

Hi.

I was reading restless code as an exercise and I found a bug:

File: restless/pyr.py
Method: build_routename

In documentation example there is a part which says:

"""
: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``
"""

Looking at the code of the mentioned function it will actually return: 'api_blogpost_list'.

JSON deserialization error should raise BadRequest

Currently, sending malformed JSON to a resource throws a ValueError, which simply turns into a 500 error on most frameworks (including Django).

This is both semantically invalid (since the issue was not caused by the server) and opaque (since it doesn't explain the issue to the client).
Raising a BadRequest with a proper explanation should address both issues.

Special handling of Http404 exception for Django

There's a common paradigm in Django where the framework automatically returns a 404 response when a Http404 response is raised. This is pretty neat in particular when using the get_object_or_404() utility function. restless, however, currently doesn't follow the same paradigm; for example consider the following code:

class UnicornResource(DjangoResource):

    def detail(self, pk):
        ...
        return get_object_or_404(queryset, pk=pk)

When requesting a non-existing object (e.g. /api/unicorns/9999/), you would receive a 500 response with the following JSON: "error": "No Unicorn matches the given query."}.

Would you consider adding this special exception handling to match that of Django? If so, I'm happy to submit a patch for it.

Thanks!

Save the endpoint in self, for a more fine grained authentication/authorization

If we save the endpoint in self we could have a fine grained authentication/authorization.
Like so:

    def handle(self, endpoint, *args, **kwargs):
        self.endpoint = endpoint

And know we could use our is_authenticated method to check the authentication per endpoint.

Like so:

     def is_authenticated(self):
         if self.endpoint = 'list':
              return True
         else:
              return False

hooking up the urls in Flask.

I'm trying to hooking up the urls in Flask but I haven't got success. I'm getting 501 always:

init.py

myblueprint = Blueprint('appname', __name__, template_folder='templatefolder')

MyResource.add_url_rules(myblueprint, rule_prefix='/v1/myresource/')

resources.py

class MyResource(FlaskResource):
    def __init__(self, *args, **kwargs):
        super(MyResource, self).__init__(*args, **kwargs)
        self.httpmethods.update({
            'auth': {
                'POST': 'auth',
            },
        })

    def auth(): 
        # code

    @classmethod
    def add_url_rules(cls, app, rule_prefix, endpoint_prefix=None):
       super(MyResource, cls).add_url_rules(app, rule_prefix, endpoint_prefix)
       app.add_url_rule(
            rule_prefix,
            endpoint=cls.build_endpoint_name('auth', endpoint_prefix),
            view_func=cls.as_view('auth'),
            methods=cls.methods
        )

is there something wrong with my code above?

thanks in advance.

str vs bytes

Just been playing with this using Pyramid, and mildly following the tutorial.

I am hitting an issue with an error:

the JSON object must be str, not 'bytes'

I believe this is a python 2 vs 3.4 issue. But I am not sure if it's an error I am making, or something within the framework!

This occurs when trying to post with CURL to create a new minion. It occurs before it even hits my function ( I believe! )

curl -X POST -H "Content-Type: application/json" -d '{"title": "New library released!", "author": "daniel", "body": "I just released a new thing!"}' http://127.0.0.1:6543/minions/

Traceback:

{"traceback": "Traceback (most recent call last):
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/waitress/task.py", line 74, in handler_thread
    task.service()
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/waitress/channel.py", line 337, in service
    task.service()
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/waitress/task.py", line 173, in service
    self.execute()
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/waitress/task.py", line 392, in execute
    app_iter = self.channel.server.application(env, start_response)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/router.py", line 242, in __call__
    response = self.invoke_subrequest(request, use_tweens=True)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/router.py", line 217, in invoke_subrequest
    response = handle_request(request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid_debugtoolbar/toolbar.py", line 177, in toolbar_tween
    response = _handler(request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid_debugtoolbar/panels/performance.py", line 57, in resource_timer_handler
    result = handler(request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/tweens.py", line 21, in excview_tween
    response = handler(request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid_tm/__init__.py", line 63, in tm_tween
    response = handler(request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/router.py", line 163, in handle_request
    response = view_callable(context, request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/config/views.py", line 329, in attr_view
    return view(context, request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/config/views.py", line 305, in predicate_wrapper
    return view(context, request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/config/views.py", line 385, in viewresult_to_response
    result = view(context, request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/pyramid/config/views.py", line 501, in _requestonly_view
    response = view(request)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/resources.py", line 139, in _wrapper
    return inst.handle(view_type, *args, **kwargs)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/resources.py", line 294, in handle
    return self.handle_error(err)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/resources.py", line 313, in handle_error
    return self.build_error(err)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/resources.py", line 289, in handle
    self.data = self.deserialize(method, endpoint, self.request_body())
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/resources.py", line 335, in deserialize
    return self.deserialize_list(body)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/resources.py", line 349, in deserialize_list
    return self.serializer.deserialize(body)
  File "/home/overfl0w/c0d3/experiments/minion-database/env/lib/python3.4/site-packages/restless/serializers.py", line 62, in deserialize
    return json.loads(body)
  File "/usr/lib64/python3.4/json/__init__.py", line 312, in loads
    s.__class__.__name__))
TypeError: the JSON object must be str, not 'bytes'", "error": "the JSON object must be str, not 'bytes'"}

What is the difference between `create` and `create_detail` views?

Well, create seems the normal 'create a single new object' view, so my question is really "what is the intent of the create_detail view?"

It is based on the detail endpoint which suggests it relates somehow to an existing record.

Docstring says:
"Creates a subcollection of data for a POST on a detail-style endpoint."

But that's pretty vague.

It returns a 'collection of data' i.e. a list of objects, instead of a single item.

Is it for creating the related objects of an existing object, eg m2m and foreign keys, all in one place rather than via individual endpoints?

Backward-incompatible change introduced

This simple change results in the breaking of custom resource endpoints: https://github.com/toastdriven/restless/blob/master/restless/dj.py#L93

The documentation for custom endpoints (http://restless.readthedocs.io/en/latest/extending.html#custom-endpoints) establishes that the following will add the desired URL pattern (for a GET request) and work as desired:

    # Finally, extend the URLs.
    @classmethod
    def urls(cls, name_prefix=None):
        urlpatterns = super(PostResource, cls).urls(name_prefix=name_prefix)
        return urlpatterns + [
            url(r'^schema/$', cls.as_view('schema'), name=cls.build_url_name('schema', name_prefix)),
        ]

However, this is not the case anymore: http://127.0.0.1/api/posts/schema/ will now get pattern-matches against the PK pattern at https://github.com/toastdriven/restless/blob/master/restless/dj.py#L93. Either the docs need to be updated to change the order of how the custom URL patterns are built for such a case or the change should be reverted - regardless, I would consider this a backwards incompatible change.

Something like this in docs, maybe?

    # Finally, extend the URLs.
    @classmethod
    def urls(cls, name_prefix=None):
        urlpatterns = super(PostResource, cls).urls(name_prefix=name_prefix)
        return [
            url(r'^schema/$', cls.as_view('schema'), name=cls.build_url_name('schema', name_prefix)),
        ] + urlpatterns

logging uncaught exceptions

If an exception is raised during the processing of a handler method, it's always handled gracefully, returning a response object that contains the error message and (when is_debug == True) the traceback. It's fine most of the time, unless the exception is "unexpected". Such exceptions will result in a Server Error (500) response.

The problem I have with this behavior is that the exception is "silenced", especially in production where is_debug will be set to False, effectively hiding important debugging information that is required to track down a bug or system failure.

There's always the option of overriding the bubble_exceptions method on all endpoints and handle the exceptions myself using a middleware, but I find such a solution to be noisy and cumbersome.

I suggest to simply log the exception using the standard logging library in the restless.resource.Resource.handle method.

I can provide a patch.

Thanks!

Class-level `http_methods` attribute breaks Resource subclasses

http_methods is defined as a class-level attribute on the Resource class.

But then the documentation for Custom Endpoints recommends overriding http_methods in your resource's __init__ method.

The trouble with this technique is that there is only ever a single copy of http_methods and the last call to .update(your_custom_endpoints_dict) will clobber other resource definitions across the entire site.

This works fine for the example in the documentation, because it is defining a new endpoint to be hooked up to a urlconf using cls.as_view('schema'). As long as no other resource similarly overrides the schema http_method with a different mapping, this will work.

But in the following situation, it breaks down a bit:

class CustomEndpointResource(Resource):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.http_methods.update({
            'detail': {
                'GET': 'detail',
                'OPTIONS': 'cors_preflight'
            }
        })

class InnocentBystanderResource(Resource):
    def update(self):
        return {"error": "sometimes reached, sometimes not"}

In this example, we want to add the ability for the CustomEndpointResource to respond to OPTIONS requests, for CORS purposes. We expect that when we call self.http_methods.update() we're only updating the http_methods on our own resource. Instead, we're updating our parent class' attribute Resource.http_methods. A race condition ensues where until some request hits CustomEndpointResource, the global http_methods will still contain the default mapping of the PUT HTTP-level method to the update method on InnocentBystanderResource, but once a request comes in for CustomEndpointResource, all further requests to InnocentBystanderResource will fail with an error similar to:

restless.exceptions.MethodNotImplemented: Unsupported method 'PUT' for detail endpoint.

A simple fix for this is to simply instantiate a copy of http_methods inside the __init__ method of the Resource class, but other techniques would work as well. I'm sure this is an intentional API choice, but it contains a nasty gotcha bug for this corner case.

self.data should populate from querystring on GET requests

I notice that request.data is not populated on GET requests because almost no HTTP clients send a request body with GET requests, even though some client libraries allow it. On the contrary, when clients want to pass data via GET, they (almost always) use querystring arguments. Wouldn't it make sense for restless to conditionally populate request.data from wherever the data is?

Serializer's serialize() method should accept dict/list not str

Probably a typo, in docstrings of Serializer.serialize(self, data) and JSONSerializer.serialize(self, data), the data attribute is mentioned to be:

    :param data: The body for the response
    :type data: string

I suppose this should be:

    :param data: The body for the response
    :type data: ``list`` or ``dict``

or I am missing something?

how to use array in post\put request

Please help me to understand how to do
I sent in request uid[]=1&uid[]=2&groupname=blabla
In django i can get uid with request.POST.getlist('uid[]')
but in restless i receive only last value. uid[] : 2
it must be uid: [1, 2, ]
how can i fix this correctly? (sorry for bad english)

uid[0]=1&uid[1]=2&groupname=blabla
this code work for me but not for all.

        post = {}
        for k,v in a.items():
            if k.find('[')>0:
                k = k[0:k.find('[')]
                n = post.get(k, [])
                n.append(v)
                post.update({k: n })
            else:
                post.update({k:v})

then variable post is

post = {u'uid': [u'1', u'2'], u'groupname': u'blabla'}

Example for not-common frameworks

Hello,
I am working with a custom web framework and I would like to ask if there is an example to bind it (e.g. with the raw embedded webserver in python.

Allow to use strings for pk in DjangoResource

Currently the urls method in the DjangoResource looks for urls using the pattern r'^(?P<pk>\d+)/$'. The problem is that in some cases is ok to have string identifiers. One example would be to use UUIDs instead of integers (common if you want to use distributed database with replications).

Another example would be to use slugs on codes instead of the pk.

I see at least two possibilities here:

  1. Change the r'^(?P<pk>\d+)/$' to r'^(?P<pk>[\w-]+)/$'
  2. Add a parameters to the urls method allowing the user to override the regexp. Something like pk_re or pk_regexp.

If you (maintainers) agree I could implement either option.

Does it support nested data on the `fields` declaration?

As a django-piston user, it is common to have nested data on the fields declaration:

fields = (
    'id', 'username',
    ('group', ('id', 'name'))
)

On django-tastypie, this is similar to use full = True on ForeignKey fields.

Is there a way to work with nested data?

a problem with JSON conversion

Hello, it looks like something is wrong with JSON-conversion.

Have this code:

from restless.dj import DjangoResource
from restless.preparers import FieldsPreparer
from web.models import *

class SearchRoutesResource(DjangoResource):

    def list(self):
        return {
            'title': 'post.title',
            'author': {
                'id': 'post.user.id',
                'username': 'post.user.username',
                'date_joined': 'post.user.date_joined',
            },
            'body': 'post.content',
        }

It returns the following JSON:

{
objects: [
"title",
"body",
"author"
]
}

Environment:

python 3.4.0
Django (1.8.3)
pip (7.1.0)
querystring-parser (1.2.0)
restless (2.0.1)
setuptools (18.0.1)
six (1.9.0)

Decorator to add routes to a resource

I use this on almost every project I use restless with. It would be great if you could add it to the project.

from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt


def routes(*configs):
    """
    A class decorator that adds extra routes to the resource.

    Usage:

        @routes(('POST', r'url_pattern/$', 'new_method1'),
                (('GET', 'POST'), r'url_pattern/$', 'new_method2'))
        class MyResource(Resource):
            def new_method1(self):
                pass
            def new_method2(self):
                pass
    """

    def decorator(cls):
        old_init = getattr(cls, '__init__')
        def __init__(self, *args, **kwargs):
            old_init(self, *args, **kwargs)
            for methods, path_regex, target in configs:
                if isinstance(methods, basestring):
                    methods = (methods, )
                conf = {}
                for method in methods:
                    conf[method] = target
                self.http_methods[target] = conf
        cls.__init__ = __init__

        old_urls = getattr(cls, 'urls')
        @classmethod
        def urls(cls, name_prefix=None):
            urls = old_urls(name_prefix=name_prefix)
            for method, path_regex, target in configs:
                name = cls.build_url_name(target, name_prefix)
                view = csrf_exempt(cls.as_view(target))
                urls.insert(0, url(path_regex, view, name=name))
            return urls
        cls.urls = urls

        return cls

    return decorator

including relationships in fields

As with the PostResource example, I'd like to include the author like

{'author':'user.username'}

But what if, as in my case, the user field is null=True? Then an error is returned stating that "None has no attribute: 'username'". How would I cange that? How flexible is the fields dict?

How would it handle a ManyToMany field?

Support for UUIDs

Currently UUIDs fail both in that they can't be serialized and can't be used as a pk

I fould this pull request: https://github.com/toastdriven/restless/pull/49/files
and notice that Django it self will support this in 1.8.4: https://docs.djangoproject.com/en/1.8/releases/1.8.4/

I've manually updated my urls to this:

    @classmethod
    def urls(cls, name_prefix=None):
        return patterns('',
                        url(r'^$', cls.as_list(), name=cls.build_url_name('list', name_prefix)),
                        url(r'^(?P<pk>[-\w]+)/$', cls.as_detail(), name=cls.build_url_name('detail', name_prefix)),
                        )

I'm not a regex wiz, so there might be a better one. Would love this to be build in.

Unable to add pagination meta data to response dict

I want to add pagination meta data to a response when i do a GET request to my API.

I can handle the pagination djangos built -in Paginator class but the problem is that my list() expects a colection/itterator of items to serialize. So I cannot return the collection from my pagiinator + the meta data from that paginator object.

Reverse Foreign Key Objects

Is it possible with restless to get reverse foreign key objects in Django? I.e. I have 2 classes called "Quiz" and "Question" and "Question" has a foreign-key to "Quiz". Now in the detail-view of "Quiz" I would like to show the quiz's title and all related questions.

Unit test for Django Resource containing an ImageField

Hi,

I am trying to write an unit test for my resource, but I am getting the error:

{"error": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)"}

Here is my model:

class MyModel(models.Model):
    input_image = models.ImageField('Input image', upload_to='process')

My resource:

class MyModelResource(DjangoResource):
    preparer = FieldsPreparer(fields={
        'input_image': 'input_image_url',
        'id': 'id',
    })

    def is_authenticated(self):
        return True

    @skip_prepare
    def list(self):
        return list(MyModel.objects.all().values('id'))

    def update(self):
        raise MethodNotAllowed()

    def delete(self):
       raise MethodNotAllowed()

    def detail(self, pk):
        return MyModel.objects.get(id=pk)

    def create(self):
        form = MyModelForm(self.request, self.data, self.request.FILES)
        if form.is_valid():
            obj = form.save()

My unit test

class CountProcessResourceTest(TestCase):

    def setUp(self):
        self.client = Client()
        self.img_url = 'https://www.djangoproject.com/s/img/small-fundraising-heart.png'
        image = urllib.urlopen(self.img_url)
        self.image = SimpleUploadedFile(name='test_image.jpg', content=image.read(), content_type='image/png')
        self.object = MyModel.objects.create(input_image=self.image)

    def test_create(self):
        api_url = reverse('api_mymodel_list')

        t = {"input_image": self.image}
        response = self.client.post(api_url, t, content_type='application/json')

        print response.request
        print response.content

What am I doing wrong?

BadRequest and JSON

It would be cool if we could change the behavior of build_error.

Currently when we raise a BadRequest the field data.error will be text only, but It would be way better to have both, either text or dict (json), so that people could raise BadRequest(error_dict) and it would dump the error as json so that a client has more info about the error which occured (like only some fields were wrong)

Edit: I try to make an implementation about that on the weekend.

Edit2: I mean this is how I currently create BadRequests:

if errors:
    self.status_map['update'] = constants.BAD_REQUEST
    return data.Data(errors, should_prepare=False)

test python35 django==1.6

Test failed, change the version of the django1.6 to 1.9

../../../../.virtualenvs/sprint1/lib/python3.5/site-packages/django/utils/html_parser.py:12: in <module>
    HTMLParseError = _html_parser.HTMLParseError
E   AttributeError: module 'html.parser' has no attribute 'HTMLParseError'

Better handling for failed authentication?

This might be more of a question than a feature request or a bug report. I'm not sure :)

Currently, if the authentication fails then an Unauthorized exception is raised: https://github.com/toastdriven/restless/blob/1.0.0/restless/resources.py#L238-L239

When raised, this exception isn't handled, causing Django to return a 500 response. Wouldn't it be more appropriate for restless to directly return a 401 HTTP response instead? To do this I've overridden the handle() method to catch the exception. I'm not sure if that would be the recommended way.

Any thoughts? Thanks a lot for this great app by the way!

Restless doesn't work with nullable foreign keys

I have a Category model with three fields: id, parent and title. Parent can be null.
I set up preparer like this:

preparer = FieldsPreparer(fields={
    'id': 'id',
    'parent': 'parent.id',
    'title': 'title',
})

If I request /api/categories/ I get an error:

'NoneType' object has no attribute 'id'

Uploading Multipart files

I can not upload files with restless.
It raises exception before my Resource's def create(*args ,**kwargs) method and seems to serialize file content as json.
I am using httpie for testing uploads from CLI.

https://github.com/jkbrzt/httpie#file-upload-forms

blog git:(master) ✗ http -f POST :8888/api/snippets [email protected] -p Hh 
POST /api/snippets HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 188
Content-Type: multipart/form-data; boundary=35eaef06eea1424184e438ce4d907ed3
Host: localhost:8888
User-Agent: HTTPie/0.8.0

HTTP/1.1 500 Internal Server Error
Content-Encoding: gzip
Content-Length: 1130
Content-Type: application/json; charset=UTF-8
Date: Fri, 11 Mar 2016 13:24:44 GMT
Server: TornadoServer/4.3
Vary: Accept-Encoding

Traceback

Traceback (most recent call last):
  File "/home/ahmed/opt/pycharm-5.0.4/helpers/pydev/pydevd.py", line 2411, in <module>
    globals = debugger.run(setup['file'], None, None, is_module)
  File "/home/ahmed/opt/pycharm-5.0.4/helpers/pydev/pydevd.py", line 1802, in run
    launch(file, globals, locals)  # execute the script
  File "/home/ahmed/PycharmProjects/blog/application.py", line 55, in <module>
    main()
  File "/home/ahmed/PycharmProjects/blog/application.py", line 50, in main
    tornado.ioloop.IOLoop.current().start()
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/ioloop.py", line 883, in start
    handler_func(fd_obj, events)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/stack_context.py", line 275, in null_wrapper
    return fn(*args, **kwargs)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/netutil.py", line 274, in accept_handler
    callback(connection, address)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/tcpserver.py", line 269, in _handle_connection
    future = self.handle_stream(stream, address)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/httpserver.py", line 180, in handle_stream
    conn.start_serving(self)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/http1connection.py", line 693, in start_serving
    self._serving_future = self._server_request_loop(delegate)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 282, in wrapper
    yielded = next(result)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/http1connection.py", line 706, in _server_request_loop
    ret = yield conn.read_response(request_delegate)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/http1connection.py", line 151, in read_response
    return self._read_message(delegate)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 294, in wrapper
    Runner(result, future, yielded)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 956, in __init__
    self.run()
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 1017, in run
    yielded = self.gen.send(value)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/http1connection.py", line 238, in _read_message
    delegate.finish()
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/httpserver.py", line 291, in finish
    self.delegate.finish()
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/web.py", line 2022, in finish
    self.execute()
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/web.py", line 2055, in execute
    **self.path_kwargs)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 282, in wrapper
    yielded = next(result)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/web.py", line 1443, in _execute
    result = method(*self.path_args, **self.path_kwargs)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 282, in wrapper
    yielded = next(result)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/tnd.py", line 40, in _method
    yield self.resource_handler.handle(self.__resource_view_type__, *args, **kwargs)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/tornado/gen.py", line 282, in wrapper
    yielded = next(result)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/tnd.py", line 168, in handle
    raise gen.Return(self.handle_error(err))
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/resources.py", line 315, in handle_error
    return self.build_error(err)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/tnd.py", line 160, in handle
    self.data = self.deserialize(method, endpoint, self.request_body())
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/resources.py", line 337, in deserialize
    return self.deserialize_list(body)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/resources.py", line 351, in deserialize_list
    return self.serializer.deserialize(body)
  File "/home/ahmed/PycharmProjects/blog/venv/local/lib/python2.7/site-packages/restless/serializers.py", line 62, in deserialize
    return json.loads(body.decode('utf-8'))
  File "/usr/lib/python2.7/json/__init__.py", line 338, in loads
    return _default_decoder.decode(s)
  File "/usr/lib/python2.7/json/decoder.py", line 366, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/lib/python2.7/json/decoder.py", line 384, in raw_decode
    raise ValueError("No JSON object could be decoded")
ValueError: No JSON object could be decoded", "error": "No JSON object could be decoded"}

Support pagination natively

What do you guys think about native support to pagination? We could implement that using Django paginator (for example) since it doesn't have external dependencies.

That's helpful in most listing APIs. Here is just an example on how it could be implemented in the Resource:

    DEFAULT_RESULTS_PER_PAGE = 20

class Resource(object):

   ...

    def serialize_list(self, data):
        if getattr(self, 'paginate', False):
            page_size = getattr(self, 'page_size', DEFAULT_RESULTS_PER_PAGE)
            paginator = Paginator(data, page_size)

            try:
                page_number = int(self.request.GET.get('p', 1))
            except ValueError:
                page_number = None

            if page_number not in paginator.page_range:
                raise BadRequest('Invalid page number')

            self.page = paginator.page(page_number)
            data = self.page.object_list

        return super().serialize_list(data)

    def wrap_list_response(self, data):
        response_dict = super().wrap_list_response(data)

        if hasattr(self, 'page'):
            if self.page.has_next():
                next_page = self.page.next_page_number()
            else:
                next_page = None

            if self.page.has_previous():
                previous_page = self.page.previous_page_number()
            else:
                previous_page = None

            response_dict['pagination'] = {
                'num_pages': self.page.paginator.num_pages,
                'count': self.page.paginator.count,
                'page': self.page.number,
                'start_index': self.page.start_index(),
                'end_index': self.page.end_index(),
                'next_page': next_page,
                'previous_page': previous_page,
            }

        return response_dict

And to use simply add to your resource:

MyResource(Resource):
    paginate = True
    page_size = 50  # optional

If that seems useful I'm willing to create a PR with tests and docs.

Tests failed for Flask about get_loader

All Flask tests failed for Python 3 but nothing about restless.
There is already an issue on Flask repository, here. I just create an issue for people who will find why it just failed.

And it's allow us to track Flask issue easily.

Drop support for Python < 3.4

What are your opinions about dropping support for Python < 3.4?

Python 2.7 is reaching end-of-life soon. And Python 3.0 to 3.3 are not supported anymore.

Maybe we could give an step foward and drop support for 3.4 too, because it's end of life will be in two months.

This will help us to support new versions of some frameworks while keeping the code clear.

Example tornado script doesn't work

On my macbook pro, localhost:8001/pets throws a 500 with the example script for tornado.

Tested on python 2.7 and 3.4 - tornado and restless were the default pip installs on a fresh virtualenv as of 10/16/2014

unclear request object being used in example about alternative serializations

I was going through the restless docs about extending restless.
In the section about handling multiple forms of serialization
around line 470 in the MultiSerializer example,

class MultiSerializer(Serializer):
        def deserialize(self, body):
            # This is Django-specific, but all frameworks can handle GET
            # parameters...
            ct = request.GET.get('format', 'json')
            if ct == 'yaml':
                return yaml.safe_load(body)
            else:
                return json.load(body)

        def serialize(self, data):
            # Again, Django-specific.
            ct = request.GET.get('format', 'json')
            if ct == 'yaml':
                return yaml.dump(body)
            else:
                return json.dumps(body, cls=MoreTypesJSONEncoder)

in line ct = request.GET.get....
Its not clear where the request variable being used is coming from. I experimented a little and realized that neither the serializer instance nor the supplied arguments contain the request instance.

Can you please clarify if I am missing something here - in which case we can just improve the docs around the same.

Also if the request instance is not really present, my ideas around making it available would be as follows

  • make the resource instance available as Serializer.resource - so that it can be used as self.resource (I dont recommend this one since it makes too much of the details available to the serializer)
  • make the request instance available as Serializer.request - so that it can be used as self.request - but this one can raise thread safety issues.

I would be happy to make the changes and raise PR for you if you would like that

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.