0%

Python PasteDeploy 详解

今天将分析 Python 里的一个开源项目:Paste Deployment(后面简写为 PasteDeploy),它是一套框架,可以用于发现并配置 WSGI 应用程序和 WSGI Servers。对于 WSGI 应用程序的使用者而言,它可以提供单一的入口(loadpp)来从配置文件或者 Python Egg 中加载 WSGI 应用程序,而对于 WSGI 应用程序的开发者而言,只需要按照 PasteDeploy协议,为应用程序提供相应的 entry_points,PasteDeploy 即可发现这些 WSGI 应用程序,WSGI 应用程序的实现细节里也无需感知应用程序的用户。

PasteDeploy 最核心的一点,就是它预先定义好了接口(可调用的 Python 对象),只要应用程序开发者提供了这些接口(即遵守 PasteDeploy的协议),就能被 PasteDeploy 识别。

PasteDeploy 目前并不依赖 Paste,它可以作为一个独立的包进行发布。

基本使用方法

PasteDeploy 最主要的用途就是加载 WSGI 应用程序,可以通过如下方式加载应用程序:

1
2
from paste.deploy import loadapp
wsgi_app = loadapp('config:/path/to/config.ini')

loadapp 函数从指定的 URI 中加载应用程序,目前支持两种 URI 格式:config: 以及 egg:。如果配置文件中定义了多个 app,可以通过 name 关键字参数来指定要加载的 app。如果没有指定 app 的 name,同时 loadapp 中的 URI 参数不包含也 #,则默认加载的 app 为 main

PasteDeploy 中共有 3 种类型的对象,分别是 applicationfilter、和 server。因此除了使用 loadapp 来加载app 外,还可以使用 loadfilter 来加载 filter、使用 loadserver 来加载 server。

PasteDeploy 配置文件

可以通过配置文件来使用 PasteDeploy。PasteDeploy 的配置文件采用 INI 文件格式,包含不同的 sections,每一个 section 都有一个前缀,例如 app:mainfilter:errors。前缀的格式为:type:name

典型的配置文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[composite:main]
use = egg:Paste#urlmap
/ = home
/blog = blog
/wiki = wiki
/cms = config:cms.ini

[app:home]
use = egg:Paste#static
document_root = %(here)s/htdocs

[filter-app:blog]
use = egg:Authentication#auth
next = blogapp
roles = admin
htpasswd = /home/me/users.htpasswd

[app:blogapp]
use = egg:BlogApp
database = sqlite:/home/me/blog.db

[app:wiki]
use = call:mywiki.main:application
database = sqlite:/home/me/wiki.db

application object

可以在配置文件中定义多个 application 对象,每个应用程序都在自己的 section 中。application 也可以根据他们的功能,划分为不同的种类。

Applications

最基本的 application 对象,section 前缀以 app: 为开头,后面是该应用程序的名称(如果没有指定则为 main)。有两种方式来指定该应用程序的 Python 代码:

  • 通过 URI 或名称指定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[app:myapp]
use = config:another_config_file.ini#app_name

# or any URI:
[app:myotherapp]
use = egg:MyApp

# or a callable from a module:
[app:mythirdapp]
use = call:my.project:myapplication

# or even another section:
[app:mylastapp]
use = myotherapp
  • 显示指定 Python 代码,此时配置项采用 协议 作为键
1
2
[app:myapp]
paste.app_factory = myapp.modulename:app_factory

section 下除了 use 关键字之外的其他配置,都会作为关键字参数传给工厂代码。另外 [DEFAULT] section 下的配置是全局配置,全局配置会传递给所有 application,而且可以在 section 下通过 set key=value 来覆盖全局配置。

Composite Applications

Composite Applications 组合应用程序由一系列应用程序构成,最典型的例子是 URL mapper,这样根据 URL 挂载不同的应用程序:

1
2
3
4
5
6
7
8
9
10
11
[composite:main]
use = egg:Paste#urlmap
/ = mainapp
/files = staticapp

[app:mainapp]
use = egg:MyApp

[app:staticapp]
use = egg:Paste#static
document_root = /path/to/docroot

这里就是定义了一个名为的 main 的 composite 应用,它使用的是 Paste 包中的 urlmap 应用。urlmap 是 Paste 提供的一套通用 composite 应用,根据根据 URL 将请求分发到不同的 WSGI 应用,这里访问 / 将被映射到 mainapp,访问 /files 将映射到 staticapp。

同样也可以通过协议来指定 Composite Applications,协议名称为 paste.composite_factory

Filter Composition

Filter Composition 可以对于 application 可以应用 filter,然后再执行 application。有多种方式来创建 Filter Composition application

  • 使用 filter-with
1
2
3
4
5
6
7
[app:main]
use = egg:MyEgg
filter-with = printdebug

[filter:printdebug]
use = egg:Paste#printdebug
# and you could have another filter-with here, and so on...
  • 使用 [filter-app:...] section:在该 section 中通过 use 定义了一个 filter,同时通过 next 指定该 filter 应用的 app
1
2
3
4
5
[filter-app:blog]
use = egg:Authentication#auth
next = blogapp
roles = admin
htpasswd = /home/me/users.htpasswd
  • 使用 [pipeline:...] section:通过 pipeline 可以定义一系列的 filter,以及这些 filter 最终应用的 app
1
2
3
4
5
[pipeline:main]
pipeline = filter1 egg:FilterEgg#filter2 filter3 app

[filter:filter1]
# ...

可以通过协议 paste.filter_app_factory 来指定 Filter Composition

filter object

接下来介绍 filterfilter 是过滤器,它接受应用程序作为参数,然后返回一个新的应用程序,新的应用程序包含了 filter 的过滤逻辑。使用 [filter:...] section 可以创建 filter

1
2
3
[filter:printdebug]
use = egg:Paste#printdebug
# and you could have another filter-with here, and so on...

也可以通过协议 paste.filter_factory 来指定 filter

server object

server 是指 WSGI 服务器,可以为 WSGI 应用程序提供服务。使用 [server:...] 可以指定 server 对象。另外通过协议 paste.server_factorypaste.server_runner 也可以指定 server。

加载对象

如上所述,有两种方式来加载对象,一种是通过 egg URI 的方式,另外一种就是通过 protocol 的方式。

egg: URIs

通过 setuptools 安装的软件包,都可以指定 entry_points。如果我们编写的 WSGI 软件包,想被 PasteDeploy 识别,可以按照如下方式编写 setup()

1
2
3
4
5
6
7
8
9
setup(
name='MyApp',
# ...
entry_points={
'paste.app_factory': [
'main=myapp.mymodule:app_factory',
'ob2=myapp.mymodule:ob_factory'],
},
)

这里通过 paste.app_factory entry_points 组定义了 2 个 app_factory,这样就可以被 PasteDeploy 识别,然后可以在 PasteDeploy 的配置文件中可以通过 egg:MyApp#mainegg:MyApp#ob2 来加载这两个 application。如果想提供其他的 object,只需要将 entry_points 的 group 设置为相应的 protocol 名称即可。

Defining Factories

除了 entry_points 的方式,还可以直接指定应用程序工厂的代码,即采用类似于 paste.app_factory = myapp.modulename:app_factory 的形式。应用程序工厂的代码需要遵循 PasteDeploy 的协议,即工厂代码需要是可调用对象(参数和返回值需要符合预期)。PasteDeploy 定义了如下协议:

  • paste.app_factory:它接受全局配置、本地配置作为参数,返回一个 WSGI 应用

例如:

1
2
def app_factory(global_config, **local_conf):
return wsgi_app
  • paste.composite_factory:接受三个参数:loader、全局配置、本地配置。loader 对象提供一系列方法:get_app(name_or_uri, global_conf=None) 根据名称返回 WSGI 应用程序、get_filter 和 get_server 也是类似。

例如:

1
2
def composite_factory(loader, global_config, **local_conf):
return wsgi_app

如下是一个更复杂的例子,它实现了一个 pipeline application:

1
2
3
4
5
6
7
8
9
def pipeline_factory(loader, global_config, pipeline):
# space-separated list of filter and app names:
pipeline = pipeline.split()
filters = [loader.get_filter(n) for n in pipeline[:-1]]
app = loader.get_app(pipeline[-1])
filters.reverse() # apply in reverse order!
for filter in filters:
app = filter(app)
return app

通过如下方式来使用这个 pipeline:

1
2
3
4
5
6
7
8
9
10
[composite:main]
use = <pipeline_factory_uri>
pipeline = egg:Paste#printdebug session myapp

[filter:session]
use = egg:Paste#session
store = memory

[app:myapp]
use = egg:MyApp
  • paste.filter_factory:类似于 app_facotry,但是返回的是 filters。Filter 也是可调用对象,但是接受一个 WSGI 应用程序作为参数,返回一个包含过滤逻辑的 app。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def auth_filter_factory(global_conf, req_usernames):
# space-separated list of usernames:
req_usernames = req_usernames.split()
def filter(app):
return AuthFilter(app, req_usernames)
return filter

class AuthFilter(object):
def __init__(self, app, req_usernames):
self.app = app
self.req_usernames = req_usernames

def __call__(self, environ, start_response):
if environ.get('REMOTE_USER') in self.req_usernames:
return self.app(environ, start_response)
start_response(
'403 Forbidden', [('Content-type', 'text/html')])
return ['You are forbidden to view this resource']
  • paste.filter_app_factory:类似于 paste.filter_factory,但是还接受 wsgi_app 作为参数,并返回一个 WSGI 应用程序

  • paste.server_factory:接受的参数和 app_factory 相同,但是返回一个 server。该 server 接受一个 WSGI application 作为参数,提供相应的服务

例如:

1
2
3
4
5
6
def server_factory(global_conf, host, port):
port = int(port)
def serve(app):
s = Server(app, host=host, port=port)
s.serve_forever()
return serve
  • paste.server_runner:类似于 paste.server_factory,除了第一个参数是 wsgi_app,同时 server 也会立即运行

Reference

PasteDeploy 官方文档
PasteDeploy github
详解Paste deploy