File: tutorial.rst

package info (click to toggle)
python-restless 2.0.1-5~bpo8%2B1
  • links: PTS, VCS
  • area: main
  • in suites: jessie-backports
  • size: 472 kB
  • sloc: python: 1,879; makefile: 149
file content (681 lines) | stat: -rw-r--r-- 22,187 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
.. _tutorial:

=================
Restless Tutorial
=================

Restless is an alternative take on REST frameworks. While other frameworks
attempt to be very complete, include special features or tie deeply to ORMs,
Restless is a trip back to the basics.

It is fast, lightweight, and works with a small (but growing) number of
different web frameworks. If you're interested in more of the backstory &
reasoning behind Restless, please have a gander at the :ref:`philosophy`
documentation.

You can find some complete example implementation code in `the repository`_.

.. _`the repository`: https://github.com/toastdriven/restless/tree/master/examples


Why Restless?
=============

Restless tries to be RESTful by default, but flexible enough. The main
``Resource`` class has data methods (that you implement) for all the main
RESTful actions. It also uses HTTP status codes as correctly as possible.

Restless is BYOD (bring your own data) and hence, works with almost any
ORM/data source. If you can import a module to work with the data & can
represent it as JSON, Restless can work with it.

Restless is small & easy to keep in your head. Common usages involve
overridding just a few easily remembered method names. Total source code is
a under a thousand lines of code.

Restless supports Python 3 **first**, but has backward-compatibility to work
with Python 2.6+ code. Because the future is here.

Restless is JSON-only by default. Most everything can speak JSON, it's a *data*
format (not a *document* format) & it's pleasant for both computers and humans
to work with.

Restless is well-tested.


Installation
============

Installation is a relatively simple affair. For the most recent stable release,
simply use pip_ to run::

    $ pip install restless

Alternately, you can download the latest development source from Github::

    $ git clone https://github.com/toastdriven/restless.git
    $ cd restless
    $ python setup.py install

.. _pip: http://pip-installer.org/


Getting Started
===============

Restless currently supports Django_, Flask_, Pyramid_ & Itty_.
For the purposes of most of this
tutorial, we'll assume you're using Django. The process for developing &
interacting with the API via Flask is nearly identical (& we'll be covering the
differences at the end of this document).

There are only two steps to getting a Restless API up & functional. They are:

#. Implement a ``restless.Resource`` subclass
#. Hook up the resource to your URLs

Before beginning, you should be familiar with the common understanding of the
behavior of the various `REST methods`_.

.. _Django: http://djangoproject.com/
.. _Flask: http://flask.pocoo.org/
.. _Pyramid: http://www.pylonsproject.org/
.. _Itty: https://pypi.python.org/pypi/itty
.. _`REST methods`: http://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_Web_Services


About Resources
===============

The main class in Restless is :py:class:`restless.resources.Resource`. It provides
all the dispatching/authentication/deserialization/serialization/response
stuff so you don't have to.

Instead, you define/implement a handful of methods that strictly do data
access/modification. Those methods are:

* ``Resource.list`` - *GET /*
* ``Resource.detail`` - *GET /identifier/*
* ``Resource.create`` - *POST /*
* ``Resource.update`` - *PUT /identifier/*
* ``Resource.delete`` - *DELETE /identifier/*

Restless also supports some less common combinations (due to their more complex
& use-specific natures):

* ``Resource.create_detail`` - *POST /identifier/*
* ``Resource.update_list`` - *PUT /*
* ``Resource.delete_list`` - *DELETE /*

Restless includes modules for various web frameworks. To create a resource to
work with Django, you'll need to subclass from
:py:class:`restless.dj.DjangoResource`.
To create a resource to work with Flask, you'll need to subclass from
:py:class:`restless.fl.FlaskResource`.

.. note:

    The module names ``restless.dj`` & ``restless.fl`` are used (in place of
    ``restless.django`` & ``restless.flask``) to prevent import confusion.

``DjangoResource`` is itself a subclass, inheriting from
``restless.resource.Resource`` & overrides a small number of methods to make
things work smoothly.


The Existing Setup
==================

We'll assume we're creating an API for our super-awesome blog platform. We have
a ``posts`` application, which has a model setup like so...::

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


    class Post(models.Model):
        user = models.ForeignKey(User, related_name='posts')
        title = models.CharField(max_length=128)
        slug = models.SlugField(blank=True)
        content = models.TextField(default='', blank=True)
        posted_on = models.DateTimeField(auto_now_add=True)
        updated_on = models.DateTimeField(auto_now=True)

        class Meta(object):
            ordering = ['-posted_on', 'title']

        def __str__(self):
            return self.title

This is just enough to get the ORM going & use some real data.

The rest of the app (views, URLs, admin, forms, etc.) really aren't important
for the purposes of creating a basic Restless API, so we'll ignore them for now.


Creating A Resource
===================

We'll start with defining the resource subclass. Where you put this code isn't
particularly important (as long as other things can import the class you
define), but a great convention is ``<myapp>/api.py``. So in case of our
tutorial app, we'll place this code in a new ``posts/api.py`` file.

We'll start with the most basic functional example.::

    # posts/api.py
    from restless.dj import DjangoResource
    from restless.preparers import FieldsPreparer

    from posts.models import Post


    class PostResource(DjangoResource):
        preparer = FieldsPreparer(fields={
            'id': 'id',
            'title': 'title',
            'author': 'user.username',
            'body': 'content',
            'posted_on': 'posted_on',
        })

        # GET /api/posts/ (but not hooked up yet)
        def list(self):
            return Post.objects.all()

        # GET /api/posts/<pk>/ (but not hooked up yet)
        def detail(self, pk):
            return Post.objects.get(id=pk)

As we've already covered, we're inheriting from ``restless.dj.DjangoResource``.
We're also importing our ``Post`` model, because serving data out of an API
is kinda important.

The name of the class isn't particularly important, but it should be
descriptive (and can play a role in hooking up URLs later on).

Fields
------

We define a ``fields`` attribute on the class. This dictionary provides a
mapping between what the API will return & where the data is. This allows you
to mask/rename fields, prevent some fields from being exposed or lookup
information buried deep in the data model. The mapping is defined like...::

    FieldsPreparer(fields={
        'the_fieldname_exposed_to_the_user': 'a_dotted_path_to_the_data',
    })

This dotted path is what allows use to drill in. For instance, the ``author``
field above has a path of ``user.username``. When serializing, this will cause
Restless to look for an attribute (or a key on a dict) called ``user``. From
there, it will look further into the resulting object/dict looking for a
``username`` attribute/key, returning it as the final value.

Methods
-------

We're also defining a ``list`` method & a ``detail`` method. Both can take
optional postitional/keyword parameters if necessary.

These methods serve the **data** to present for their given endpoints. You
don't need to manually construct any responses/status codes/etc., just provide
what data should be present.

The ``list`` method serves the ``GET`` method on the collection. It should
return a ``list`` (or similar iterable, like ``QuerySet``) of data. In this
case, we're simply returning all of our ``Post`` model instances.

The ``detail`` method also serves the ``GET`` method, but this time for single
objects **ONLY**. Providing a ``pk`` in the URL allows this method to lookup
the data that should be served.

.. note:

    Restless has this pattern of pairs of methods for each of the RESTful
    HTTP verbs, list variant & detail variant.

    ``create/create_detail`` handle ``POST``. ``update_list/update`` handle
    ``PUT``. And ``delete_list/delete`` handle ``DELETE``.


Hooking Up The URLs
===================

URLs to Restless resources get hooked up much like any other class-based view.
However, Restless's :py:class:`restless.dj.DjangoResource` comes with a
special method called ``urls``, which makes hooking up URLs more convenient.

You can hook the URLs for the resource up wherever you want. The recommended
practice would be to create a URLconf just for the API portion of your site.::

    # The ``settings.ROOT_URLCONF`` file
    # myproject/urls.py
    from django.conf.urls import patterns, url, include

    # Add this!
    from posts.api import PostResource

    urlpatterns = patterns('',
        # The usual fare, then...

        # Add this!
        url(r'api/posts/', include(PostResource.urls())),
    )

Note that unlike some other CBVs (admin specifically), the ``urls`` here is a
**METHOD**, not an attribute/property. Those parens are important!

Manual URLconfs
---------------

You can also manually hook up URLs by specifying something like::

    urlpatterns = patterns('',
        # ...

        # 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
===============

We've done enough to get the API (provided there's data in the DB) going, so
let's make some requests!

Go to a terminal & run::

    $ curl -X GET http://127.0.0.1:8000/api/posts/

You should get something like this back...::

    {
        "objects": [
            {
                "id": 1,
                "title": "First Post!",
                "author": "daniel",
                "body": "This is the very first post on my shiny-new blog platform...",
                "posted_on": "2014-01-12T15:23:46",
            },
            {
                # More here...
            }
        ]
    }

You can also go to the same URL in a browser (http://127.0.0.1:8000/api/posts/)
& you should also get the JSON back.

.. note:

    Consider using browser plugins like JSONView to nicely format the JSON.

    You can get nice formatting at the command line by either piping to
    ``curl -X GET http://127.0.0.1:8000/api/posts/ | python -m json.tool``.
    Alternatively, you can use a tool like httpie_
    (``http http://127.0.0.1:8000/api/posts/``).

.. _httpie: https://pypi.python.org/pypi/httpie

You can then load up an individual object by changing the URL to include a
``pk``.::

    $ curl -X GET http://127.0.0.1:8000/api/posts/1/

You should get back...::

    {
        "id": 1,
        "title": "First Post!",
        "author": "daniel",
        "body": "This is the very first post on my shiny-new blog platform...",
        "posted_on": "2014-01-12T15:23:46",
    }

Note that the ``objects`` wrapper is no longer present & we get back the JSON
for just that single object. Hooray!


Creating/Updating/Deleting Data
===============================

A read-only API is nice & all, but sometimes you want to be able to create data
as well. So we'll implement some more methods.::

    # posts/api.py
    from restless.dj import DjangoResource
    from restless.preparers import FieldsPreparer

    from posts.models import Post


    class PostResource(DjangoResource):
        preparer = FieldsPreparer(fields={
            'id': 'id',
            'title': 'title',
            'author': 'user.username',
            'body': 'content',
            'posted_on': 'posted_on',
        })

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

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

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

        # Add this!
        # PUT /api/posts/<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

        # Add this!
        # DELETE /api/posts/<pk>/
        def delete(self, pk):
            Post.objects.get(id=pk).delete()

By adding the ``create/update/delete`` methods, we now have the ability to
add new items, update existing ones & delete individual items. Most of this
code is relatively straightforward ORM calls, but there are a few interesting
new things going on here.

Note that the ``create`` & ``update`` methods are both using a special
``self.data`` variable. This is created by Restless during deserialization &
is the **JSON** data the user sends as part of the request.

.. warning::

    This data (within ``self.data``) is mostly unsanitized (beyond standard
    JSON decoding) & could contain anything (not just the ``fields`` you
    define).

    You know your data best & validation is **very** non-standard between
    frameworks, so this is a place where Restless punts.

    Some people like cleaning the data with ``Forms``, others prefer to
    hand-sanitize, some do model validation, etc. Do what works best for you.

    You can refer to the :ref:`extending` documentation for recommended
    approaches.

Also note that ``delete`` is the first method with **no return value**. You
can do the same thing on ``create/update`` if you like. When there's no
meaningful data returned, Restless simply sends back a correct status code
& an empty body.

Finally, there's no need to hook up more URLconfs. Restless delegates based
on a list & a detail endpoint. All further dispatching is HTTP verb-based &
handled by Restless.


Testing the API, Round 2
========================

Now let's test out our new functionality! Go to a terminal & run::

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

You should get something like this back...::

    {
        "error": "Unauthorized"
    }

Wait, what?!! But we added our new methods & everything!

The reason you get unauthorized is that by default, **only GET** requests are
allowed by Restless. It's the only sane/safe default to have & it's very easy
to fix.


Error Handling
==============

By default, Restless tries to serialize any exceptions that may be encountered.
What gets serialized depends on two methods: ``Resource.is_debug()`` &
``Resource.bubble_exceptions()``.

``is_debug``
------------

Regardless of the error type, the exception's message will get serialized into
the response under the ``"error"`` key. For example, if an ``IOError`` is
raised during processing, you'll get a response like::

    HTTP/1.0 500 INTERNAL SERVER ERROR
    Content-Type: application/json
    # Other headers...

    {
        "error": "Whatever."
    }

If ``Resource.is_debug()`` returns ``True`` (the default is ``False``), Restless
will also include a traceback. For example::

    HTTP/1.0 500 INTERNAL SERVER ERROR
    Content-Type: application/json
    # Other headers...

    {
        "error": "Whatever.",
        "traceback": "Traceback (most recent call last):\n # Typical traceback..."
    }

Each framework-specific ``Resource`` subclass implements ``is_debug()`` in a
way most appropriate for the framework. In the case of the ``DjangoResource``,
it returns ``settings.DEBUG``, allowing your resources to stay consistent with
the rest of your application.

``bubble_exceptions``
---------------------

If ``Resource.bubble_exceptions()`` returns ``True`` (the default is ``False``),
any exception encountered will simply be re-raised & it's up to your setup to
handle it. Typically, this behavior is undesirable except in development & with
frameworks that can provide extra information/debugging on exceptions. Feel
free to override it (``return True``) or implement application-specific logic
if that meets your needs.


Authentication
==============

We're going to override one more method in our resource subclass, this time
adding the ``is_authenticated`` method.::

    # posts/api.py
    from restless.dj import DjangoResource
    from restless.preparers import FieldsPreparer

    from posts.models import Post


    class PostResource(DjangoResource):
        preparer = FieldsPreparer(fields={
            'id': 'id',
            'title': 'title',
            'author': 'user.username',
            'body': 'content',
            'posted_on': 'posted_on',
        }

        # Add this!
        def is_authenticated(self):
            # Open everything wide!
            # DANGEROUS, DO NOT DO IN PRODUCTION.
            return True

            # Alternatively, if the user is logged into the site...
            # return self.request.user.is_authenticated()

            # Alternatively, you could check an API key. (Need a model for this...)
            # from myapp.models import ApiKey
            # try:
            #     key = ApiKey.objects.get(key=self.request.GET.get('api_key'))
            #     return True
            # except ApiKey.DoesNotExist:
            #     return False

        def list(self):
            return Post.objects.all()

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

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

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

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

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

With that change in place, now our API should play nice...


Testing the API, Round 3
========================

Back to the terminal & again run::

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

You should get something like this back...::

    {
        "body": "I just released a new thing!",
        "title": "New library released!",
        "id": 3,
        "posted_on": "2014-01-13T10:02:55.926857+00:00",
        "author": "daniel"
    }

Hooray! Now we can check for it in the list view
(``GET`` http://127.0.0.1:8000/api/posts/) or by a detail (``GET``
http://127.0.0.1:8000/api/posts/3/).

We can also update it. Restless expects **complete** bodies (don't try to send
partial updates, that's typically reserved for ``PATCH``).::

    $ curl -X 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/

And we can delete our new data if we decide we don't like it.::

    $ curl -X DELETE http://127.0.0.1:8000/api/posts/3/


Conclusion
==========

We've now got a basic, working RESTful API in a short amount of code! And
the possibilities don't stop at the ORM. You could hook up:

* Redis
* the NoSQL flavor of the month
* text/log files
* wrap calls to other (nastier) APIs

You may also want to check the :ref:`cookbook` for other interesting/useful
possibilities & implementation patterns.


Bonus: Flask Support
====================

Outside of the ORM, precious little of what we implemented above was
Django-specific. If you used an ORM like `Peewee`_ or `SQLAlchemy`_, you'd have
very similar-looking code.

In actuality, there are just two changes to make the Restless-portion of the
above work within Flask.

#. Change the inheritance
#. Change how the URL routes are hooked up.

.. _Peewee: http://peewee.readthedocs.org/en/latest/
.. _SQLAlchemy: http://www.sqlalchemy.org/


Change The Inheritance
----------------------

Restless ships with a :py:class:`restless.fl.FlaskResource` class, akin to the
``DjangoResource``. So the first change is dead simple.::

    # Was: from restless.dj import DjangoResource
    # Becomes:
    from restless.fl import FlaskResource

    # ...

    # Was: class PostResource(DjangoResource):
    # Becomes:
    class PostResource(FlaskResource):
        # ...


Change How The URL Routes Are Hooked Up
---------------------------------------

Again, similar to the ``DjangoResource``, the ``FlaskResource`` comes with a
special method to make hooking up the routes easier.

Wherever your ``PostResource`` is defined, import your Flask ``app``, then call
the following::

    PostResource.add_url_rules(app, rule_prefix='/api/posts/')

This will hook up the same two endpoints (list & detail, just like Django above)
within the Flask app, doing similar dispatches.

You can also do this manually (but it's ugly/hurts).::

    app.add_url_rule('/api/posts/', endpoint='api_posts_list', view_func=PostResource.as_list(), methods=['GET', 'POST', 'PUT', 'DELETE'])
    app.add_url_rule('/api/posts/<pk>/', endpoint='api_posts_detail', view_func=PostResource.as_detail(), methods=['GET', 'POST', 'PUT', 'DELETE'])

Done!
-----

Believe it or not, if your ORM could be made to look similar, that's all the
further changes needed to get the same API (with the same end-user interactions)
working on Flask! Huzzah!