0%

Python Pecan web 框架简介

Pecan 是轻量级的 Python web 开发框架,它用于填补 Python web 框架世界中的一个空白:即提供对象分发(object-dispatch)风格的路由。Pecan 并不打算成为一个全栈框架,因此并不包含一些开箱即用的工具:例如对 sessions、databases 的支持,相反 Pecan 聚焦于 HTTP 本身。

尽管 Pecan 非常轻量,但是仍然为构建基于 HTTP 的应用程序提供大量扩展支持:

  • 采用对象分发式的路由风格
  • 对 REST 风格控制器的完全支持
  • 可扩展的安全框架
  • 可扩展的模板语言支持
  • 可扩展的 JSON 输出
  • 简易的基于 Python 的配置

安装 Pecan

首先创建一个虚拟环境:

1
2
3
$ virtualenv pecan-env
$ cd pecan-env
$ source bin/activate

之后使用 pip 安装 Pecan

1
pip install pecan

牛刀小试

接下来将创建一个 Pecan 应用程序,这里直接使用 pecan create 命令,它会基于模板创建一个 Pecan 应用程序:

1
pecan create hello_pecan

之后进入 hello_pecan 程序目录,使用开发模式安装到系统,这样对源码的修改可以立即生效:

1
2
3
cd hello_pecan

python setup.py develop

Pecan 基于模板创建的程序目录中包含如下主要文件:

  • public 目录:存放所有的静态文件。Pecan 自带一个简答的文件服务器,因此开发模式下可以直接访问这些文件
  • config.py:包含了运行 Pecan 应用程序所需要的基本配置。为了使用方便,Pecan 使用纯 Python 文件作为配置文件

Pecan 应用程序的结构也遵循 MVC 模式,因此包含如下目录:

  • hello_pecan/controllers:存放所有的 controller 文件
  • hello_pecan/templates:存放所有的 template 文件
  • hello_pecan/templates:存放所有的 model 文件

还包含一个测试目录:

  • hello_pecan/tests:用于存放单元测试和集成测试文件:

最后再分析 hello_pecan/app.py 文件,它用于控制 Pecan 程序如何创建。它包含一个 setup_app() 函数用于返回一个 WSGI 应用程序。

接下来使用 pecan serve 命令启动一个开放服务器,并运行该应用程序:

1
2
3
$ pecan serve config.py
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080

应用程序入口点

RootController 是应用程序的入口点。我们测试程序的 RootController 位于 hello_pecan/controllers/root.py 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pecan import expose, redirect
from webob.exc import status_map


class RootController(object):

@expose(generic=True, template='index.html')
def index(self):
return dict()

@index.when(method='POST')
def index_post(self, q):
redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q)

@expose('error.html')
def error(self, status):
try:
status = int(status)
except ValueError: # pragma: no cover
status = 500
message = getattr(status_map.get(status), 'explanation', '')
return dict(status=status, message=message)

简单分析一下这段代码:

  • index 方法通过 expose() 装饰器被标记为可公开访问,所以访问应用程序的 root(即 /)的 HTTP GET 请求都会被路由到该方法
  • index 方法返回一个字典,该字典将被用于渲染特定的模板,这里即为 index.html,从而返回一个 HTML。这也是将数据从 controller 传给 template 的主要机制
  • index_post()@index.when(method='POST') 装饰器修饰,因此所有访问应用程序 root 的 HTTP POST 请求都会被路由到该方法。POST 请求的参数也会被传递给 index_post 方法
  • error() 方法允许对 HTTP 错误显示自定义页面

Controller 和 路由

Pecan 使用对象分发(object-dispatch)的路由策略来将 HTTP 请求映射到某一个 controller,然后调用其相应的方法。对象分发会把访问 URI 分割为一系列的组件,然后根据路径访问对象,首先是 root controller。可以把应用程序所有的 controller 想象为 objects 树,对象树的某个分支就直接映射为相应的 URL 路径。

举个例子:

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
from pecan import expose

class BooksController(object):
@expose()
def index(self):
return "Welcome to book section."

@expose()
def bestsellers(self):
return "We have 5 books in the top 10."

class CatalogController(object):
@expose()
def index(self):
return "Welcome to the catalog."

books = BooksController()

class RootController(object):
@expose()
def index(self):
return "Welcome to store.example.com!"

@expose()
def hours(self):
return "Open 24/7 on the web."

catalog = CatalogController()

如果某一个请求访问路径 /catalog/books/bestsellers,那么 Pecan 会把该请求分割为 catelogbooksbestsellers、然后会在 RootController 下寻找 catelog 对象,然后使用 catelog 对象,寻找 books 对象,之后再在 books 对象中寻找 bestsellers。如果 URI 的最后部分是一个 controller 对象,则调用该 controller 对象的 index 方法。

例如访问如下路径:

1
2
3
4
5
└── /
├── /hours
└── /catalog
└── /catalog/books
└── /catalog/books/bestsellers

最终会调用如下 controller 的方法:

1
2
3
4
5
└── RootController.index
├── RootController.hours
└── CatalogController.index
└── BooksController.index
└── BooksController.bestsellers

导出 controller

通过 expose 装饰器可以告诉 Pecan 类中的哪些方法是公共可见的,如果类中的方法没有使用 expose() 装饰,Pecan 不会将该请求路由到该方法上。expose() 可以有多种用途:

  • 如果没有传递任何参数,那么 controller 方法返回的内容将以字符串的形式作为 HTML 响应体
  • 更常见的场景是在 expose() 方法中将需要渲染的模板作为参数,然后 controller 方法返回一个字典,作为渲染的输入(namespace)
  • expose() 方法可以叠加,这样可以根据请求(例如根据请求头中的 Accept 字段) 返回不同的样式内容。

指定显式的路径分段

有时,你可能会碰到这样一个问题:URI 中的路径分段和 Pecan 控制器中的方法名称不一致(例如受限于 Python 的语法)。举个例子:访问的路径中包含 -,那么在 Python 中如下方法是非法的:

1
2
3
4
5
class RootController(object):

@pecan.expose()
def some-path(self):
return dict()

为了解决该问题,可以在 expose 装饰器指定显式的路径分段:

1
2
3
4
5
class RootController(object):

@pecan.expose(route='some-path')
def some_path(self):
return dict()

此时可以顺利访问 /some-path,但是访问 /some-path 则会失败。

还有一种方式是直接在 Pecan 中使用 route 方法,如下所示:

1
2
3
4
5
6
7
class RootController(object):

@pecan.expose()
def some_path(self):
return dict()

pecan.route('some-path', RootController.some_path)

基于请求方法进行路由

expose() 方法的 generic 参数使得我们可以基于请求方法来重载 URL。例如下面的例子中,虽然是同一个 URL,但是可以被两个不同的方法访问,每个请求方法路由到对应的 controller method 上。这里使用到了 generic controller

1
2
3
4
5
6
7
8
9
10
11
12
class RootController(object):

# HTTP GET /
@expose(generic=True, template='json')
def index(self):
return dict()

# HTTP POST /
@index.when(method='POST', template='json')
def index_POST(self, **kw):
uuid = create_something()
return dict(uuid=uuid)

使用 _lookup 路由到 Subcontrollers

有时标准的对象分发机制不足以将某个 URL 映射到对应的 controller。 Pecan 提供一些方法可以对路由机制提供更多控制,从而为 URL 路由提供更大的灵活性。

_lookup() 方法提供了一种方式:只处理 URL 中的一部分,然后返回一个新的控制器,用来处理 URL 的剩余部分。_lookup() 方法可以接受一个或多个参数,用于表示 URL 中要处理的分段。另外它也包含一个可变长位置参数,用于表示不处理的 URL 分段。而且,所有不处理的 URL 分段也需要被返回。

除了使用 _lookup 来动态创建 controllers_lookup() 也是 Pecan 最后尝试调用的函数(当 controller 的所有方法都不匹配 URL 且没有 _default() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pecan import expose, abort
from somelib import get_student_by_name

class StudentController(object):
def __init__(self, student):
self.student = student

@expose()
def name(self):
return self.student.name

class RootController(object):
@expose()
def _lookup(self, primary_key, *remainder):
student = get_student_by_primary_key(primary_key)
if student:
return StudentController(student), remainder
else:
abort(404)

这样访问 /8/name 的 HTTP GET 请求会返回指定 Student 的 name,该 Student 的 primary_key == 8

_default() 方法

在通过标准的对象分发对 URL 进行路由时,_default() 方法是最后尝试调用的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pecan import expose

class RootController(object):
@expose()
def english(self):
return 'hello'

@expose()
def french(self):
return 'bonjour'

@expose()
def _default(self):
return 'I cannot say hello in that language'

在该例子中,访问 /spanish,会导致请求路由到 RootController._default() 方法。

_route 自定义路由

_route() 方法允许控制器完全覆盖 Pecan 的路由机制,Pecan 使用 _route() 方法来实现它的 RestController。如果你想在 Pecan 之上设计另一套路由系统,可以定义一个包含自定义 _route() 方法的基控制器类。

和 Request 和 Response 对象进行交互

对于每一个 HTTP 请求,Pecan 都会维护一个对 request/response 对象的 thread-local 引用:pecan.requestpecan.response

1
2
3
4
5
6
7
8
@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!'

可以通过 abort() 函数来抛出一个 WSGIHTTPException 类型的异常,Pecan 会使用该异常来渲染 HTTP 错误的默认响应 body。

WebOp 提供的请求和响应实现已经足够强大了,但是有时我们还是对 requestresponse 进行扩展。如果想实现这一点,可以继承 pecan.Requestpecan.Response 创建自定义对应类型:

1
2
3
4
5
class MyRequest(pecan.Request):
pass

class MyResponse(pecan.Response):
pass

然后修改程序配置:

1
2
3
4
5
6
7
8
9
10
from myproject import MyRequest, MyResponse

app = {
'root' : 'project.controllers.root.RootController',
'modules' : ['project'],
'static_root' : '%(confdir)s/public',
'template_path' : '%(confdir)s/project/templates',
'request_cls': MyRequest,
'response_cls': MyResponse
}

控制器参数

HTTP GET 和 POST 请求中的未被路由过程处理的的变量都会被传递给控制器的方法,并作为其参数。取决于方法的签名,这些参数可以进行显示映射:

1
2
3
4
5
6
7
8
9
10
from pecan import expose

class RootController(object):
@expose()
def index(self, arg):
return arg

@expose()
def kwargs(self, **kwargs):
return str(kwargs)
1
2
3
4
$ 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'}

HTTP POST 请求 body 中的变量也可以达到同样的效果:

1
2
3
4
5
6
from pecan import expose

class RootController(object):
@expose()
def index(self, arg):
return arg
1
2
$ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo"
foo

Pecan 中的模板

Pecan 支持各种模板引擎,而且也很容易对其进行扩展以添加新的模板引擎。当前,Pecan 支持 MakoGenshiKajikiJinja2Json 的模板引擎。默认的模板引擎是 mako,可以在应用程序中通过 default_renderer 修改模板引擎。

通过 expose() 装饰器的 template 参数,可以指定控制器方法所需要使用的模板文件:

1
2
3
4
class MyController(object):
@expose('path/to/mako/template.html')
def index(self):
return dict(message='I am a mako template')

expose() 方法会使用默认的模板引擎来对模板文件进行渲染,除非模板文件中显式指定了模板引擎:

1
2
3
@expose('kajiki:path/to/kajiki/template.html')
def my_controller(self):
return dict(message='I am a kajiki template')

override_template() 允许你覆盖控制器方法中所要使用的模板文件。而render() 允许你使用 Pecan 模板框架手动进行渲染:

1
2
3
@expose()
def controller(self):
return render('my_template.html', dict(message='I am the namespace'))

为了自定义渲染器,可以创建一个自定义类,其实现了渲染器协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyRenderer(object):
def __init__(self, path, extra_vars):
'''
Your renderer is provided with a path to templates,
as configured by your application, and any extra
template variables, also as configured
'''
pass

def render(self, template_path, namespace):
'''
Lookup the template based on the path, and render
your output based upon the supplied namespace
dictionary, as returned from the controller.
'''
return str(namespace)

使用 Generic 控制器编写 RESTful web 服务

通过 Pecan 的 generic 控制器,简化了 RESTful web 服务的编写,因为 通用控制器 允许你基于请求方法来重载 URL。下面是一个例子程序:

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
from pecan import abort, expose

# Note: this is *not* thread-safe. In real life, use a persistent data store.
BOOKS = {
'0': 'The Last of the Mohicans',
'1': 'Catch-22'
}


class BookController(object):

def __init__(self, id_):
self.id_ = id_
assert self.book

@property
def book(self):
if self.id_ in BOOKS:
return dict(id=self.id_, name=BOOKS[self.id_])
abort(404)

# HTTP GET /<id>/
@expose(generic=True, template='json')
def index(self):
return self.book

# HTTP PUT /<id>/
@index.when(method='PUT', template='json')
def index_PUT(self, **kw):
BOOKS[self.id_] = kw['name']
return self.book

# HTTP DELETE /<id>/
@index.when(method='DELETE', template='json')
def index_DELETE(self):
del BOOKS[self.id_]
return dict()


class RootController(object):

@expose()
def _lookup(self, id_, *remainder):
return BookController(id_), remainder

# HTTP GET /
@expose(generic=True, template='json')
def index(self):
return [dict(id=k, name=v) for k, v in BOOKS.items()]

# HTTP POST /
@index.when(method='POST', template='json')
def index_POST(self, **kw):
id_ = str(len(BOOKS))
BOOKS[id_] = kw['name']
return dict(id=id_, name=kw['name'])

Reference