参考文档:
http://pecan.readthedocs.io/en/latest/quick_start.html
http://blog.csdn.net/allison_ywt/article/details/54140730
Pecan是一个路由对象分发的oython web框架。本质上可以将url通过分割为每一部分,然后对每一部分查找对应处理该URL部分的处理类,处理后,继续交给后面部分的URL处理,直到所有URL部分都被处理后,调用最后分割的URL对应的处理函数处理。
1 基础知识介绍
安装: sudo pip install pecan
创建项目:pecan create test_project
部署: python setup.py develop
开启服务: 进入含有config.py的目录下,执行: pecan serve config.py
测试:curl http://localhost:8080/catalog/books/bestsellers
解析过程应该是将每个路径例如: catalog加上controller拼接为CatalogController,转到该类处理
RootController:可以认为是应用入口,根url路径,例如:
http://localhost:8080/
index():是公共可访问的,通过expose()使用index.html,返回的是映射,映射渲染特定模板
@expose(generic=True, template='index.html')
def index(self):
return dict()
index_post():方法接收post的参数请求q,所有使用post的请求都会转到index_post方法
pecan采用对象分发来映射来将http请求映射到controller,你可以想想你的应用的
控制器是一颗树,对象树的分支直接映射到url路径
如果方法不提供expose()装饰器,Pecan将不会将请求路由给它
@expose():
@expose('html_template.mako'):可以指定模板,然后将方法中的映射传入到模板中做渲染
@expose('json') : 将模板命名空间渲染为json文本
@pecan.expose(route='some-path'):指定明确的路径
pecan.route('some-path', RootController.some_path)
分发到子控制器
pecan.route(RootController, 'child-path', ChildController())
路由算法: _lookup(), _default(), and _route()
_lookup():可以对部分url处理,返回一个新的controller对象来继续处理剩余部分
_default():该方法在没有其他控制器可以匹配url的时候调用
_route():该方法允许控制器controller完全重写Pecan的路由机制
pecan.Request 和 pecan.Response:可以与Pecan控制器交互
abort() :抛出http异常,例如abort(404)
2 下面内容来自于: http://blog.csdn.net/allison_ywt/article/details/54140730
使用Pecan框架时,OpenStack项目一般会把API服务的实现都放在一个api目录下,比如magnum项目是这样的:
➜ ~/openstack/env/p/magnum git:(master) $ tree magnum/api
magnum/api
├── app.py
├── auth.py
├── config.py
├── controllers
│ ├── base.py
│ ├── __init__.py
│ ├── link.py
│ ├── root.py
│ └── v1
│ ├── base.py
│ ├── baymodel.py
│ ├── bay.py
│ ├── certificate.py
│ ├── collection.py
│ ├── container.py
│ ├── __init__.py
│ ├── magnum_services.py
│ ├── node.py
│ ├── pod.py
│ ├── replicationcontroller.py
│ ├── service.py
│ ├── types.py
│ ├── utils.py
│ └── x509keypair.py
├── expose.py
├── hooks.py
├── __init__.py
├── middleware
│ ├── auth_token.py
│ ├── __init__.py
│ └── parsable_error.py
├── servicegroup.py
└── validation.py
你也可以在Ceilometer项目中看到类似的结构。介绍一下几个主要的文件,这样你以后看到一个使用Pecan的OpenStack项目时就会比较容易找到入口。
-
app.py 一般包含了Pecan应用的入口,包含应用初始化代码
-
config.py 包含Pecan的应用配置,会被app.py使用
-
controllers/ 这个目录会包含所有的控制器,也就是API具体逻辑的地方
-
controllers/root.py 这个包含根路径对应的控制器
-
controllers/v1/ 这个目录对应v1版本的API的控制器。如果有多个版本的API,你一般能看到v2等目录。
代码变少了:application的配置
Pecan的配置很容易,通过一个Python源码式的配置文件就可以完成基本的配置。这个配置的主要目的是指定应用程序的root,然后用于生成WSGI application。我们来看Magnum项目的例子。Magnum项目有个API服务是用Pecan实现的,在magnum/api/config.py文件中可以找到这个文件,主要内容如下:
app = {
'root': 'magnum.api.controllers.root.RootController',
'modules': ['magnum.api'],
'debug': False,
'hooks': [
hooks.ContextHook(),
hooks.RPCHook(),
hooks.NoExceptionTracebackHook(),
],
'acl_public_routes': [
'/'
],
}
上面这个app对象就是Pecan的配置,每个Pecan应用都需要有这么一个名为app的配置。app配置中最主要的就是root的值,这个值表示了应用程序的入口,也就是从哪个地方开始解析HTTP的根path:/。hooks对应的配置是一些Pecan的hook,作用类似于WSGI Middleware。
有了app配置后,就可以让Pecan生成一个WSGI application。在Magnum项目中,magnum/api/app.py文件就是生成WSGI application的地方,我们来看一下这个的主要内容:
def get_pecan_config():
# Set up the pecan configuration
filename = api_config.__file__.replace('.pyc', '.py')
return pecan.configuration.conf_from_file(filename)
def setup_app(config=None):
if not config:
config = get_pecan_config()
app_conf = dict(config.app)
app = pecan.make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
wrap_app=middleware.ParsableErrorMiddleware,
**app_conf
)
return auth.install(app, CONF, config.app.acl_public_routes)
get_pecan_config()
方法读取我们上面提到的config.py文件,然后返回一个pecan.configuration.Config对象。setup_app()
函数首先调用get_pecan_config()
函数获取application的配置,然后调用pecan.make_app()
函数创建了一个WSGI application,最后调用了auth.install()
函数(也就是magnum.api.auth.install()函数)为刚刚生成的WSGI application加上Keystone的认证中间件(确保所有的请求都会通过Keystone认证)。
到这边为止,一个Pecan的WSGI application就已经准备好了,只要调用这个setup_app()
函数就能获得。至于如何部署这个WSGI application,请参考WSGI简介这篇文章。
从Magnum这个实际的例子可以看出,使用了Pecan之后,我们不再需要自己写那些冗余的WSGI application代码了,直接调用Pecan的make_app()
函数就能完成这些工作。另外,对于之前使用PasteDeploy时用到的很多WSGI中间件,可以选择使用Pecan的hooks机制来实现,也选择使用WSGI中间件的方式来实现。在Magnum的API服务就同时使用了这两种方式。其实,Pecan还可以和PasteDeploy一起使用,Ceilometer项目就是这么做的,大家可以看看。
确定路由变得容易了:对象分发式的路由
Pecan不仅缩减了生成WSGI application的代码,而且也让开发人员更容易的指定一个application的路由。Pecan采用了一种对象分发风格(object-dispatch style)的路由模式。我们直接通过例子来解释这种路由模式,还是以Magnum项目为例。
上面提到了,Magnum的API服务的root是magnum.api.controllers.root.RootController。这里的RootController的是一个类,我们来看它的代码:
class RootController(rest.RestController):
_versions = ['v1']
"""All supported API versions"""
_default_version = 'v1'
"""The default API version"""
v1 = v1.Controller()
@expose.expose(Root)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return Root.convert()
@pecan.expose()
def _route(self, args):
"""Overrides the default routing behavior.
It redirects the request to the default version of the magnum API
if the version number is not specified in the url.
"""
if args[0] and args[0] not in self._versions:
args = [self._default_version] + args
return super(RootController, self)._route(args)
别看这个类这么长,我来解释一下你就懂了。首先,你可以先忽略掉_route()
函数,这个函数是用来覆盖Pecan的默认路由实现的,在这里去掉它不妨碍我们理解Pecan(这里的_route()
函数的作用把所有请求重定向到默认的API版本去)。去掉_route()
和其他的东西后,整个类就变成这么短:
class RootController(rest.RestController):
v1 = v1.Controller()
@expose.expose(Root)
def get(self):
return Root.convert()
-
首先,你要记住,这个RootController对应的是URL中根路径,也就是path中最左边的/。
-
RootController继承自rest.RestController,是Pecan实现的RESTful控制器。这里的
get()
函数表示,当访问的是GET /时,由该函数处理。get()
函数会返回一个WSME对象,表示一个形式化的HTTP Response,这个下面再讲。get()
函数上面的expose
装饰器是Pecan实现路由控制的一个方式,被expose的函数才会被路由处理。 -
这里的
v1 = v1.Controller()
表示,当访问的是GET /v1或者GET /v1/...时,请求由一个v1.Controller
实例来处理。
为了加深大家的理解,我们再来看下v1.Controller
的实现:
class Controller(rest.RestController):
"""Version 1 API controller root."""
bays = bay.BaysController()
baymodels = baymodel.BayModelsController()
containers = container.ContainersController()
nodes = node.NodesController()
pods = pod.PodsController()
rcs = rc.ReplicationControllersController()
services = service.ServicesController()
x509keypairs = x509keypair.X509KeyPairController()
certificates = certificate.CertificateController()
@expose.expose(V1)
def get(self):
return V1.convert()
...
上面这个Controller也是继承自rest.RestController。所以它的get函数表示,当访问的是GET /v1的时候,要做的处理。然后,它还有很多类属性,这些属性分别表示不同URL路径的控制器:
-
/v1/bays 由bays处理
-
/v1/baymodels 由baymodels处理
-
/v1/containers 由containers处理
其他的都是类似的。我们再继续看bay.BaysController
的代码:
class BaysController(rest.RestController):
"""REST controller for Bays."""
def __init__(self):
super(BaysController, self).__init__()
_custom_actions = {
'detail': ['GET'],
}
def get_all(...):
def detail(...):
def get_one(...):
def post(...):
def patch(...):
def delete(...):
这个controller中只有函数,没有任何类属性,而且没有实现任何特殊方法,所以/v1/bays开头的URL处理都在这个controller中终结。这个类会处理如下请求:
-
GET /v1/bays
-
GET /v1/bays/{UUID}
-
POST /v1/bays
-
PATCH /v1/bays/{UUID}
-
DELETE /v1/bays/{UUID}
-
GET /v1/bays/detail/{UUID}
看了上面的3个controller之后,你应该能大概明白Pecan是如何对URL进行路由的。这种路由方式就是对象分发:根据类属性,包括数据属性和方法属性来决定如何路由一个HTTP请求。Pecan的文档中对请求的路由有专门的描述,要想掌握Pecan的路由还是要完整的看一下官方文档。
内置RESTful支持
我们上面举例的controller都是继承自pecan.rest.RestController
,这种controller称为RESTful controller,专门用于实现RESTful API的,因此在OpenStack中使用特别多。Pecan还支持普通的controller,称为Generic controller。Generic controller继承自object对象,默认没有实现对RESTful请求的方法。简单的说,RESTful controller帮我们规定好了get_one()
, get_all()
, get()
,post()
等方法对应的HTTP请求,而Generic controller则没有。关于这两种controller的区别,可以看官方文档Writing RESTful Web Services with Generic Controllers,有很清楚的示例。
对于RestController中没有预先定义好的方法,我们可以通过控制器的_custom_actions
属性来指定其能处理的方法。
class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}
@expose()
def test(self):
return 'hello'
上面这个控制器是一个根控制器,指定了/test路径支持GET方法,效果如下:
$ curl http://localhost:8080/test
hello%
那么HTTP请求和HTTP响应呢?
上面讲了这么多,我们都没有说明在Pecan中如何处理请求和如何返回响应。这个将在下一章中说明,同时我们会引入一个新的库WSME。
WSME
Pecan对请求和响应的处理
在开始提到WSME之前,我们先来看下Pecan自己对HTTP请求和响应的处理。这样你能更好的理解为什么会再引入一个WSME库。
Pecan框架为每个线程维护了单独的请求和响应对象,你可以直接在请求处理函数中访问。pecan.request和pecan.response分别代表当前需要处理的请求和响应对象。你可以直接操作这两个对象,比如指定响应的状态码,就像下面这个例子一样(例子来自官方文档):
@pecan.expose()
def login(self):
assert pecan.request.path == '/login'
username = pecan.request.POST.get('username')
password = pecan.request.POST.get('password')
pecan.response.status = 403
pecan.response.text = 'Bad Login!'
这个例子演示了访问POST请求的参数以及返回403。你也可以重新构造一个pecan.Response
对象作为返回值(例子来自官方文档):
from pecan import expose, Response
class RootController(object):
@expose()
def hello(self):
return Response('Hello, World!', 202)
另外,HTTP请求的参数也会可以作为控制器方法的参数,还是来看几个官方文档的例子:
class RootController(object):
@expose()
def index(self, arg):
return arg
@expose()
def kwargs(self, **kwargs):
return str(kwargs)
这个控制器中的方法直接返回了参数,演示了对GET请求参数的处理,效果是这样的:
$ curl http://localhost:8080/?arg=foo
foo
$ curl http://localhost:8080/kwargs?a=1&b=2&c=3
{u'a': u'1', u'c': u'3', u'b': u'2'}
有时候,参数也可能是URL的一部分,比如最后的一段path作为参数,就像下面这样:
class RootController(object):
@expose()
def args(self, *args):
return ','.join(args)
效果是这样的:
$ curl http://localhost:8080/args/one/two/three
one,two,three
另外,我们还要看一下POST方法的参数如何处理(例子来自官方文档):
class RootController(object):
@expose()
def index(self, arg):
return arg
效果如下,就是把HTTP body解析成了控制器方法的参数:
$ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo"
foo
返回JSON还是HTML?
如果你不是明确的返回一个Response对象,那么Pecan中方法的返回内容类型就是由expose()
装饰器决定的。默认情况下,控制器的方法返回的content-type是HTML。
class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}
@expose()
def test(self):
return 'hello'
效果如下:
$ curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Tue, 15 Sep 2015 14:31:28 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 5
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0
hello%
也可以让它返回JSON:
class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}
@expose('json')
def test(self):
return 'hello'
效果如下:
curl -v http://localhost:8080/test
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Tue, 15 Sep 2015 14:33:27 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 18
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
{"hello": "world"}%
甚至,你还可以让一个控制器方法根据URL path的来决定是返回HTML还是JSON:
class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}
@expose()
@expose('json')
def test(self):
return json.dumps({'hello': 'world'})
返回JSON:
$ curl -v http://localhost:8080/test.json
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.json HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 14:26:27 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 24
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
"{\"hello\": \"world\"}"%
返回HTML:
$ curl -v http://localhost:8080/test.html
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test.html HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 14:26:24 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 18
< Content-Type: text/html; charset=UTF-8
<
* Closing connection 0
{"hello": "world"}%
这里要注意一下:
-
同一个字符串作为JSON返回和作为HTML返回是不一样的,仔细看一下HTTP响应的内容。
-
我们的例子中在URL的最后加上了.html后缀或者.json后缀,请尝试一下不加后缀的化是返回什么?然后,调换一下两个
expose()
的顺序再试一下。
从上面的例子可以看出,决定响应类型的主要是传递给expose()
函数的参数,我们看下expose()
函数的完整声明:
pecan.decorators.expose(template=None,
content_type='text/html',
generic=False)
-
template参数用来指定返回值的模板,如果是'json'就会返回JSON内容,这里可以指定一个HTML文件,或者指定一个mako模板。
-
content_type指定响应的content-type,默认值是'text/html'。
-
generic参数表明该方法是一个“泛型”方法,可以指定多个不同的函数对应同一个路径的不同的HTTP方法。
看过参数的解释后,你应该能大概了解expose()
函数是如何控制HTTP响应的内容和类型的。
用WSME来做什么?
上面两节已经说明了Pecan可以比较好的处理HTTP请求中的参数以及控制HTTP返回值。那么为什么我们还需要WSME呢?因为Pecan在做下面这个事情的时候比较麻烦:请求参数和响应内容的类型检查(英文简称就是typing)。当然,做是可以做的,不过你需要自己访问pecan.request和pecan.response,然后检查指定的值的类型。WSME就是为解决这个问题而生的,而且适用场景就是RESTful API。
WSME简介
WSME的全称是Web Service Made Easy,是专门用于实现REST服务的typing库,让你不需要直接操作请求和响应,而且刚好和Pecan结合得非常好,所以OpenStack的很多项目都使用了Pecan + WSME的组合来实现API(好吧,我看过的项目,用了Pecan的都用了WSME)。WSME的理念是:在大部分情况下,Web服务的输入和输出对数据类型的要求都是严格的。所以它就专门解决了这个事情,然后把其他事情都交给其他框架去实现。因此,一般WSME都是和其他框架配合使用的,支持Pecan、Flask等。WSME的文档地址是http://wsme.readthedocs.org/en/latest/index.html。
WSME的使用
用了WSME后的好处是什么呢?WSME会自动帮你检查HTTP请求和响应中的数据是否符合预先设定好的要求。WSME的主要方式是通过装饰器来控制controller方法的输入和输出。WSME中主要使用两个控制器:
-
@signature: 这个装饰器用来描述一个函数的输入和输出。
-
@wsexpose: 这个装饰器包含@signature的功能,同时会把函数的路由信息暴露给Web框架,效果就像Pecan的expose装饰器。
这里我们结合Pecan来讲解WSME的使用。先来看一个原始类型的例子:
from wsmeext.pecan import wsexpose
class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}
@wsexpose(int, int)
def test(self, number):
return number
如果不提供参数,访问会失败:
$ curl http://localhost:8080/test
{"debuginfo": null, "faultcode": "Client", "faultstring": "Missing argument: \"number\""}%
如果提供的参数不是整型,访问也会失败:
$ curl http://localhost:8080/test\?number\=a
{"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid input for field/attribute number. Value: 'a'. unable to convert to int"}%
上面这些错误信息都是由WSME框架直接返回的,还没有执行到你写的方法。如果请求正确,那么会是这样的:
$ curl -v http://localhost:8080/test\?number\=1
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test?number=1 HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 15:06:35 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 1
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
1%
请注意返回的content-type,这里返回JSON是因为我们使用的wsexpose
设置的返回类型是XML和JSON,并且JSON是默认值。上面这个例子就是WSME最简单的应用了。
那么现在有下面这些问题需要思考一下:
-
如果想用POST的方式来传递参数,要怎么做呢?提示:要阅读WSME中@signature装饰器的文档。
-
如果我希望使用/test/1这种方式来传递参数要怎么做呢?提示:要阅读Pecan文档中关于路由的部分。
-
WSME中支持对哪些类型的检查呢?WSME支持整型、浮点型、字符串、布尔型、日期时间等,甚至还支持用户自定义类型。提示:要阅读WSME文档中关于类型的部分。
-
WSME支持数组类型么?支持。
上面的问题其实也是很多人使用WSME的时候经常问的问题。我们将在下一篇文章中使用Pecan + WSME来继续开发我们的demo,并且用代码来回答上面所有的问题。