开始

入门Piston非常容易。用Piston写出来的API代码,无论是形式还是表现都与其他Django应用无异。 API代码使用URL映射一组handlers对资源进行定义。

(wrongway在这里强调一下:原文档部分文字含糊,一些概念混淆,比如handler,可能是一个Handler类,也可能是一个Hanlder实例对象,也可能是Handler类的read/update等方法,阅读时请注意)

在入门之前,建议您为API代码创建一个单独的目录,比如 ‘API’ 。

我们的应用结构如下::

  1. urls.py
  2. settings.py
  3. myapp/
  4. __init__.py
  5. views.py
  6. models.py
  7. api/
  8. __init__.py
  9. urls.py
  10. handlers.py

接下来,在最上层urls.py中定义一个’namespace’,以对应API,如下::

  1. #!python
  2. urlpatterns = patterns('',
  3. # all my other url mappings
  4. (r'^api/', include('mysite.api.urls')),
  5. )

如上所设,包含API的urls.py将处理所有以’api/‘开头的网址。

接下来将展示如何创建资源,以及如何将URL与资源挂钩。


资源(Resources)

资源(Resource)指的是代码中映射各种数据的实体。可以是博文、评论以及其他任何数据。

(Wrongway吐槽:其实这一节是讲Handler的…)

下面,我们在handlers.py中创建一个简单的handler::

  1. #!python
  2. from piston.handler import BaseHandler
  3. from myapp.models import Blogpost
  4. class BlogpostHandler(BaseHandler):
  5. allowed_methods = ('GET',)
  6. model = Blogpost
  7. def read(self, request, post_slug):
  8. ...

Piston通过handler将资源与models进行映射,这个过程中Piston做了很多繁重的幕后工作,为开发者节省了大量精力。

Resource必须是一个类,通常情况下,Resource要实现下列四个方法中的一个或多个:

:read: 由 GET 动作调用,获取对象而不做数据修改(该方法是幂等的)

:create: 由 POST 动作调用,创建新对象(们)并返回该对象(们)(或是返回 :ref:rc.CREATED.)

:update: 由 PUT 动作调用,更新某个已存在的对象并返回该对象(或是返回 :ref:rc.ALL_OK.)

:delete: 由 DELETE 动作调用,删除某个已存的对象,只返回 :ref:rc.DELETED

除此之外,我们还可以定义其他我们所需的方法。只要在 fields 中填写方法名称,该方法就会被调用, 并在调用时自动传入 model 的实例做为参数。 该方法的返回值将用做该key(方法名称)的值,返回值不做限制,可以为任意类型。

NB: 上述自定义的 “resource methods” 应该被 @classmethod 修饰。因为使用这些自定义方法时Piston未必会对Handler进行实例化。 假设你已经定义了一个UserHandler类,并在该类中自定义了一个返回User对象的方法,在这种情况下,Piston是不会调用UserHandler实例的自定义对象方法, 只会调用UserHander类的自定义的类方法。

一个handler即可以表示单独的一个对象,也可以表示多个对象的集合,因此我们可以在read()方法中分别处理这两种情况::

  1. #!python
  2. from piston.handler import BaseHandler
  3. from myapp.models import Blogpost
  4. class BlogpostHandler(BaseHandler):
  5. allowed_methods = ('GET',)
  6. model = Blogpost
  7. def read(self, request, blogpost_id=None):
  8. """
  9. Returns a single post if `blogpost_id` is given,
  10. otherwise a subset.
  11. """
  12. base = Blogpost.objects
  13. if blogpost_id:
  14. return base.get(pk=blogpost_id)
  15. else:
  16. return base.all() # Or base.filter(...)

发射器(Emitters)

发射器(Emitters) 表示输出的数据类型,可以是 YAML, JSON, XML, Pickle 或 Django ,分别对应 emitters.py 中的 XMLEmitter, JSONEmitter, YAMLEmitter, PickleEmitterDjangoEmitter

编写自己的emitters也很容易,要做的仅仅是创建一个继承 Emitter 的派生类,然后在其中创建 render 方法。 render方法接收一个 ‘request’ 参数,该参数是一个请求(request)对象的拷贝。 多了解一下request.GET是很有必要的。 (比如定义回调,JSON的输出)

要将数据进行序列化/渲染,需要调用 self.construct() ,该方法始终返回一个数据字典,我们就可以对字典做任何想做的操作,再将其返回(返回值须是unicode字符串)。

NB: 可以用 Emitter.register 函式注册Emitters,同样也可以用 Emitter.unregister 函式来移除注册(假使你想移除一个内置emitter)。

内置的emitter注册::

  1. #!python
  2. class JSONEmitter(Emitter):
  3. ...
  4. Emitter.register('json', JSONEmitter, 'application/json; charset=utf-8')

自定义emitter时,可以引入Emitter模块并调用 ‘register’ 对其注册,从而使自定义的emitter生效。 也可以利用同名数据类型(即传入的第一个参数)来覆写内置或是已存在的emitters。

上述实践,使得Piston添加其他形式的Emitter扩展变得非常容易,比如protocal buffers或是CSV。

可以通过 ‘?format=’ GET参数(例如 ‘/api/blogposts/?format=yaml’)为Emitter设置格式。不过新版本的Piston中,我们可以在URL映射中配置 ‘emitter_format’ 关键字参数来设置Emitters(与’format’关键字并不冲突),如下::

  1. #!python
  2. urlpatterns = patterns('',
  3. url(r'^blogposts(?P<emitter_format>.+)$', ...),
  4. )

这样,/blogposts.json 就会使用 JSON emitter。

此外,我们还可以在URL映射中直接设置关键字参数来指定emitter格式::

  1. #!python
  2. urlpatterns = patterns('',
  3. url(r'^blogposts$', resource_here, { 'emitter_format': 'json' }),
  4. )

URL映射(Mapping URLs)

Piston的URL映射规则与Django无异。以BlogpostHandler为例:

在 urls.py 中::

  1. #!python
  2. from django.conf.urls.defaults import *
  3. from piston.resource import Resource
  4. from mysite.myapp.api.handlers import BlogpostHandler
  5. blogpost_handler = Resource(BlogpostHandler)
  6. urlpatterns = patterns('',
  7. url(r'^blogpost/(?P<post_slug>[^/]+)/', blogpost_handler),
  8. url(r'^blogposts/', blogpost_handler),
  9. )

见上,任何对 /api/blogpost/some-slug-here/ 和 /api/blogposts/ 的访问都映射到BlogpostHandler, 分别对应同一个handler的两种不同数据集。 要注意:一个单独的handler可以处理单个对象,也可以处理多个对象的集合。

.. _anonymous_resources:

匿名资源(Anonymous Resources)

资源也可以是匿名的(“anonymous”)。匿名资源即是一种可以实例化的特殊资源, 它用于未经授权(未通过OAuth,Basic或是其他认证handler)的请求。

举个例子,本文前面的BlogpostHandler,允许匿名访问博文。 但我们并不想让匿名用户也能够创建/更新/删除博文,也不想让未经授权的用户看到所有字段。

类似的需求可以通过创建继承自AnonymousBaseHandler(而不是BaseHandler)的新handler来实现。 这样做可以省却很多繁重的工作量。

如下::

  1. #!python
  2. from piston.handler import AnonymousBaseHandler, BaseHandler
  3. class AnonymousBlogpostHandler(AnonymousBaseHandler):
  4. model = Blogpost
  5. fields = ('title', 'content')
  6. class BlogpostHandler(BaseHandler):
  7. anonymous = AnonymousBlogpostHandler
  8. # same stuff as before

我们没必要为了使用匿名handlers,就设置一个继承自BaseHandler的代理handler(“proxy handler”)。 反而是象上例这般直接指向一个匿名资源,更为实用。

.. _working_with_models:


使用Models(Working with Models)

Piston可以绑定某个model,但并不依赖该model。这样做的好处很明显:

  • 没有覆写read/create/update/delete时,Piston就提供适用的默认处理(前提是方法已经出现在 allow_methods 中)。
  • 没必要必须指定 fieldsexclude (但你可以这么做,因为它们之间并不是互斥的)
  • 如果已经在某个handler中使用了某个model,那么Piston会记住该model的 fields/exclude 设置,并在同样返回该model的其他handlers中继续使用该设置(除非被覆写)。

正如我们之前所见的,在handler中绑定某个model就只需设置 model 类参数这么简单。

扩展阅读: 为什么Piston要记住之前的handlers所记录的fields信息 <http://bitbucket.org/jespern/django-piston/wiki/FAQ#why-does-piston-use-fields-from-previous-handlers>_


配置Handlers(Configuring Handlers)

Handlers使用用四个参数进行配置。

Model

用于绑定model,查看 :ref:working_with_models.

.. _fields_and_exclude:

Fields/Exclude

返回的数据中应包含和排除的字段列表。允许内嵌,可以是外键字段以及多对多字段。

也可以是编译后的正则表达式,例如::

  1. #!python
  2. import re
  3. class FooHandler(BaseHandler):
  4. fields = ('title', 'content', ('author', ('username', 'first_name')))
  5. exclude = ('id', re.compile('^private_'))

用户可以通过Many2many/ForeignKey字段访问博文,如下::

  1. class UserHandler(BaseHandler):
  2. model = User
  3. fields = ('name', ('posts', ('title', 'date')))

返回的数据会包含用户名称以及该用户发布的博文标题和日期。

对于fields中列表为空的内嵌资源,Piston会使用默认的handler,如下::

  1. class PostHandler(BaseHandler):
  2. model = Post
  3. exclude = ('date',)
  4. class UserHandler(BaseHandler):
  5. model = User
  6. fields = ('name', ('posts', ()))

UserHandler会显示一个用户所有博文的所有字段,但不包括博文的发布日期date。

fieldsexclude 都不是必须的,二者皆无时Piston也可以使用。

Anonymous

指向可替代的匿名资源。查看 :ref:anonymous_resources


认证(Authentication)

Piston通过一个简单的接口支持可插换的认证,自带两种内置的认证机制,分别是 piston.authentication.HttpBasicAuthenticationpiston.authentication.OAuthAuthentication 。 Basic auth handler非常简单,如果我们要编写自有的认证handler,它无疑很有参考价值。

Note: 在apache或nginx下使用 piston.authentication.HttpBasicAuthentication ,须要在服务器或虚拟主机配置时添加 WSGIPassAuthorization On 指令,否则django-piston不会 从 request.METAHTTP_AUTHORIZATION 中读取认证数据。详看: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization.

认证Handler必须是一个类,且必须实现 is_authenticatedchallenge 两个方法。

is_authenticated 只接收一个参数,即Django接收的 request 对象的一个拷贝。 该对象包含认证一个用户所需的全部信息,比如 request.META.get('HTTP_AUTHENTICATION').

认证成功之后,该函式必须将 request.user 设为正确的 django.contrib.auth.models.User 对象。 这样做是考虑到随后的handlers要确认是哪个用户已经登录。

该函式必须返回True或False,以表示该用户是否登录成功。

对于认证失败的情况,就交由 challenge 处理。

challenge 不接收任何参数,必须返回一个包含正确cahllenge指令的 HttpResponse 对象。 对于Basic auth来说,该函式返回一个带有 WWW-Authenticate 报头的,状态码为401的空响应(response)。 并通知接收端要提供认证信息。

针对匿名handlers,Piston提供了一个特珠的类: piston.authentication 中的 NoAuthentication 。 该类始终返回 is_authenticated 为 True。

OAuth

OAuth是首选的认证方式,因为它能分辨消费者”consumers”,也就是说,它能识别使用该API的已认证的应用。 Piston深知和尊重这一点,并对此善加利用,比如当我们使用 @throttle 装饰器时, 它于底层限制每一个消费者应用,维系服务运转,即使当中某个服务已经达到限制配额。


表单验证(Form Validation)

Django拥有一套出色的内置表单验证机制,Piston对此善加利用。

我们可以对某个方法使用@validate装饰器,该装饰器接收两个参数。 第一个参数是必须的,即用于验证的表单,第二个参数是可选的,即提交数据的动作。 对于 create ,默认的动作是 ‘POST’ ,对于 update ,动作就是 ‘PUT’ 。

举个例子(使用ModelForm)::

  1. #!python
  2. from django import forms
  3. from piston.utils import validate
  4. from mysite.myapp.models import Blogpost
  5. class BlogpostForm(forms.ModelForm):
  6. class Meta:
  7. model = Blogpost
  8. ...
  9. @validate(BlogpostForm)
  10. def create(request, ...):
  11. ...

使用一个普通form::

  1. #!python
  2. from django import forms
  3. from piston.utils import validate
  4. class DataForm(forms.Form):
  5. data = forms.CharField(max_length=128)
  6. is_private = forms.BooleanField(default=True, required=False)
  7. ...
  8. @validate(DataForm, 'PUT')
  9. def update(...):
  10. ...

若某个方法被@validate装饰,那么发送给该方法的数据如果没有通过表单本身的 is_clean 方法验证, Piston就会禹客户端返回错误,而不运行任何操作。如果通过了验证, 表单对象就会附加到请求(request)对象中。 然后我们可以通过 request.form 获取表单(可以进一步通过cleaned_data取得验证后的数据),如下例::

  1. #!python
  2. @validate(EchoForm, 'GET')
  3. def read(self, request):
  4. return {'msg': request.form.cleaned_data['msg']}

辅助方法,工具类 & 装饰器(Helpers, utils & @decorators)

出于方便,Piston提供了一组辅助和工具方法。 其中一个便是 piston.utils 中的 rc ,它包含了一组标准的响应返回。 在Piston的动作中做为响应码返回给客户端,以表示某个特定的状态。

但在最新版本的Piston, rc 返回的则是一个 新的 HttpResponse的实例(此前版本仅仅是返回状态码), 使用如下::

  1. #!python
  2. resp = rc.CREATED
  3. resp.write("Everything went fine!")
  4. return resp
  5. resp = rc.CREATED
  6. resp.write("This will not have the previous 'fine' text in it.")
  7. return resp

新版本的Piston在返回码上的更改是后端兼容的,因为Piston仅仅是覆写了 __getattr__ 方法以返回一个新实例,而非之前的符号。

================== ================================ ====================== 变量(Variable) 结果(Result) 描述(Description) ================== ================================ ====================== rc.ALL_OK 200 OK 操作成功(Everything went well) rc.CREATED 201 Created 对象创建成功(Object was created) rc.DELETED 204 (Empty body, as per RFC2616) 对象删除成功(Object was deleted) rc.BAD_REQUEST 400 Bad Request 客户端请求错误或无法理解(Request was malformed/not understood) rc.FORBIDDEN 401 Forbidden 权限不足(Permission denied) rc.NOT_FOUND 404 Not Found 资源未找到(Resource not found) rc.DUPLICATE_ENTRY 409 Conflict/Duplicate 对象已存在(Object already exists) rc.NOT_HERE 410 Gone 对象不存在(Object does not exist) rc.NOT_IMPLEMENTED 501 Not Implemented 动作不可用(Action not available) rc.THROTTLED 503 Throttled 客户端请求受限(Request was throttled) ================== ================================ ======================


限制客户端请求(Throttling)

有些场合,我们并不想让用户在短时间内过于频繁地调用某个动作。 这时,可以让Piston在全局的基础层面上限制用户请求,在限制期限内有效地阻止客户端访问。

Piston在使用OAuth的情况下,会利用OAhth在每个消费者应用的底层对客户端访问进行限制。 若没有使用OAuth,受限规则会取决于已登录用户;对于匿名请求,受限规则则取决于客户端IP地址。

限制客户端请求是通过添加@throttle装饰器实现的。该装饰器有两个必填参数和一个可选参数。

第一个参数是某个时间段内允许的请求数量。

第二个参数就是该时间段的时长,以秒为单位。

第三个参数是一个可选的字符串, 会被添加到缓存key中。我们可以利用该参数为一组或一个动作进行限制。不提供该参数的话,Piston默认只对当前动作进行限制。

举个例子::

  1. #!python
  2. @throttle(5, 10*60)
  3. def create(...):

在这个例子中,客户端在10分钟内调用’create’的次数会限制在5次。

也可以对动作分组限制::

  1. #!python
  2. @throttle(5, 10*60, 'user_writes')
  3. def create(...):
  4. @throttle(5, 10*60, 'user_writes')
  5. def update(...):

生成文档(Generating Documentation)

公开发布API时提供相应的API使用文档。写文档本来就很沉闷繁琐,更改代码后再同步更新文档就更是如此。

幸运地是,Piston为此已经做了很多工作,使得我们可以方便地书写文档。

piston.doc 库提供了一组方法,方便我们使用Django的Views和Template生成文档。

generate_doc 函式返回一个 HandlerDocumentation 实例,该实例有下列几个方法::

  • .model (get_model) 返回该handler的名称。
  • .doc (get_doc) 返回给定的handler的docstring。
  • .get_methods 返回一组可用的方法列表。该方法接收一个可选的关键字参数 include_defaults (默认为False),在参数为True且并没有重写默认方法的情况下,该方法的返回列表会包含默认方法。我们想使用默认方法并想将其包含在文档中时,该参数就能派上用场。

get_methods 包含一系列有趣的 HandlerMethod

  • .signature (get_signature) 返回 //signature// 方法, 前两个固定参数是Piston自动指定的 ‘self’ 和 ‘request’ 。客户端无须指定这两个参数,所以不必关心。还有一个可选的关键字参数 parse_optional (默认为 True),用来将默认为None的关键字参数转变成 ““ 。
  • .doc (get_doc) 返回某个动作的docstring,所以我们应该保证handler/action都有详细的文档描述。
  • .iter_args() 会返回一个结构为(参数名,默认参数值/None)的二元组。如果默认参数值也是None,该参数值会被转换成字符串’None’。这样做是为了避免和None相混淆.因此我们就能区分返回的默认参数值终究是None还是空。

例子::

  1. #!python
  2. from piston.handler import BaseHandler
  3. from piston.doc import generate_doc
  4. class BlogpostHandler(BaseHandler):
  5. model = Blogpost
  6. def read(self, request, post_slug=None):
  7. """
  8. Reads all blogposts, or a specific blogpost if
  9. `post_slug` is supplied.
  10. """
  11. ...
  12. @staticmethod
  13. def resource_uri():
  14. return ('api_blogpost_handler', ['id'])
  15. doc = generate_doc(BlogpostHandler)
  16. print doc.name # -> 'BlogpostHandler'
  17. print doc.model # -> <class 'Blogpost'>
  18. print doc.resource_uri_template # -> '/api/post/{id}'
  19. methods = doc.get_methods()
  20. for method in methods:
  21. print method.name # -> 'read'
  22. print method.signature # -> 'read(post_slug=<optional>)'
  23. sig = ''
  24. for argn, argdef in method.iter_args():
  25. sig += argn
  26. if argdef:
  27. sig += "=%s" % argdef
  28. sig += ', '
  29. sig = sig.rstrip(",")
  30. print sig # -> 'read(repo_slug=None)'

资源URL(Resource URIs)

每个资源都可以对应一个URI。调用资源的 .resource_uri() 方法就可以在Handler中访问该资源。

详见 [[FAQ#what-is-a-uri-template|FAQ: What is a URI Template]].


测试(Tests)

Piston的初始测试案例放在在tests/下。要运行该测试需要安装zc.buildout,并使用zc.buildout创建一个Django下的隔离环境。 该案例有两组测试,分别是 tests/bin/test-1.0 和 tests/bin/test-1.1 ,各自对应不同版本的Django。完成下面两个步骤就可以运行测试。

运行测试很简单::

  1. $ python bootstrap.py
  2. Creating directory './bin'.
  3. [snip]
  4. Generated script './bin/buildout'.
  5. $ ./bin/buildout -v
  6. Develop: 'tests/..'
  7. Getting distribution for 'djangorecipe'.
  8. Got djangorecipe 0.17.3.
  9. Getting distribution for 'zc.recipe.egg'.
  10. Got zc.recipe.egg 1.2.2.
  11. Uninstalling django-1.0.
  12. Installing django-1.0.
  13. django: Downloading Django from: http://www.djangoproject.com/download/1.0.2/tarball/
  14. Generated script './bin/django-1.0'.
  15. Generated script './bin/test-1.0'.
  16. $ ./bin/test-1.0
  17. Creating test database...
  18. [snip]
  19. ...
  20. ----------------------------------------------------------------------
  21. Ran 6 tests in 0.283s
  22. OK
  23. Destroying test database...

运行buildout时一定要加上-v参数,之所以这么做,是因为不使用 “-v” 参数的话,在django下创建测试脚本时会导致脚本一直挂起。

如果您愿意为Piston贡献力量,欢迎您添加更多测试。因为当前测试仅仅覆盖了基本操作,还有很多欠缺之处。


接收数据(Receiving data)

Piston运行在HTTP协议上,对于post的数据处理得很好,对于其他格式(JSON或YAML)的数据也处理得不错。

Piston既能接收链值对数据,也很容易接收结构化数据。 Piston会尝试通过一组”loader”来反序列化传入的非form数据,这些”loader”取决于客户端指定的内容类型(Content-type)。

举个例子,如果我们给一个handler发送JSON数据,JSON内容类型(Content-Type)是 “application/json” ,Piston会做下面两件事情:

将反序列化后的数据存放在 request.data

request.content_type 设为 application/json 。对于form提交的数据而言,request.content_type 则总是 None.

可以如下这般使用 (查看 testapp/handlers.py <http://bitbucket.org/jespern/django-piston/src/7042cd328873/tests/test_project/apps/testapp/handlers.py>_)::

  1. #!python
  2. def create(self, request):
  3. if request.content_type:
  4. data = request.data
  5. em = self.model(title=data['title'], content=data['content'])
  6. em.save()
  7. for comment in data['comments']:
  8. Comment(parent=em, content=comment['content']).save()
  9. return rc.CREATED
  10. else:
  11. super(ExpressiveTestModel, self).create(request)

如果我们传送下列JSON数据,也可以被很好的处理::

  1. #!python
  2. {"content": "test", "comments": [{"content": "test1"}, {"content": "test2"}], "title": "test"}

handler接收的任何数据都会被正常的反序列化,所以只要内容一致,无论格式是YAML还是XML,handler都能正常处理。

如果handler不接受客户端发送的数据(可能需要其他格式),可以用 utils.require_mime 装饰器来指定某种数据类型来解决这个问题。

该装饰器可以接收四种数据格式,以其短语名称来指定,分别是 ‘json’, ‘yaml’, ‘xml’ 和 ‘pickle’ 。

比如::

  1. #!python
  2. class SomeHandler(BaseHandler):
  3. @require_mime('json', 'yaml')
  4. def create(...
  5. @require_extended
  6. def update(...

.. _streaming:


流(Streaming)

Piston支持流输出至客户端。不过默认情况下该功能是被禁用的,原因是:

  • Django下的流输出会中止 ConditionalGetMiddlewareCommonMiddleware

要绕过这一点不利因素,Piston提供了两个代理中间件(“proxy middleware classes”),它们在流输出的情况下不会运行,因此不会在客户端收到数据之前 查看和截断数据。如果不使用这两个中间件,Django就会跟踪输出的内容(以计算E-Tags和Content-Length),因此会导致随后的接收的peek内容为空。

piston.middleware 中有两个类用于替换 ConditionalGetMiddlewareCommonMiddleware

在 settings.py::

  1. #!python
  2. MIDDLEWARE_CLASSES = (
  3. # ...
  4. 'piston.middleware.ConditionalMiddlewareCompatProxy',
  5. 'piston.middleware.CommonMiddlewareCompatProxy',
  6. # ...
  7. )

支除任何对 ConditionalGetMiddlewareCommonMiddleware 的引用,或者令这两中间件无效。 若有其他中间件需要在流输出之前查看数据,也要使用代理中间件对其进行封装,如下::

  1. #!python
  2. from piston.middleware import compat_middleware_factory
  3. class MyMiddleware(...):
  4. ...
  5. MyMiddlewareCompatProxy = compat_middleware_factory(MyMiddleware)

然后在setting.py设置 MyMiddlewareCompatProxy 以取代原有中间件。


配置项(Configuration variables)

Piston 可以使用如下几个配置项来在某些方面做精细地控制,而不必修改代码。

============================== ========== Setting Meaning ============================== ========== settings.PISTON_EMAIL_ERRORS 如果Piston崩溃,是否给管理员发邮件记录回溯信息(类似Django在 DEBUG = True 时会显示调试信息一样) settings.PISTON_DISPLAY_ERRORS Piston崩溃之后, 是否在客户端显示简短的回溯信息(包括预期的函数名) settings.PISTON_STREAM_OUTPUT 为True时,Piston会通知Django将内容流输出至客户端,不过之前请详细阅 :ref:streaming 。 ============================== ==========