Saturday, March 31, 2012

Pyramid中的view(上)


本节资料整理改编自Pyramid官方网站,部分内容按理解进行了增删整理和归纳,以便结构上更清晰。
不知道官网为啥要将view、view configuration划分成独立的两个章节,不管它了。

一、如何定义view
在Pyramid中,view是一个可调用的python对象,可以是方法,也可以是类。它基本等同与原来Pylons中的controller。每个view至少需要接收一个request参数,并最终形成一个Response对象。
1. 函数方式定义的view
from pyramid.response import Response
def hello_world(request):
    return Response(’Hello world!’)

2. 类方式定义的view
from pyramid.response import Response
class MyView(object):
def __init__(self, request):
    self.request = request
def __call__(self):
    return Response(’hello’)
注意这里__init__、__call__两个函数的参数,其中__init__中的request即函数方式定义view的request。

另外需要注意的是,view的参数也有可能是context, request两个(在traversal中),如:
def view(context, request):
        return Response('OK')

class view(object):
        def __init__(self, context, request):
                self.context = context
                self.request = request

        def __call__(self):
                return Response('OK')

二、如何配置view
上面的例子只是定义了一个view,其需要生效可用还需要经过配置这一关。这些配置信息直接用于view的定位与判定。
正如在早先的章节中说明的,view配置有两种方式,一种是add_view方法,另外一种是view_config标注。两者的定义是可以完全等价的,其参数也基本一致,只是后者需要一个scan操作查找这些配置。例如:
config.add_view(’mypackage.views.my_view’, route_name=’ok’, request_method=’POST’, permission=’read’)

@view_config(route_name=’edit’)
def edit(request):
    return Response(’edited!’)

开发者通过配置使其开发的view能在Pyramid应用中使用。view配置参数大都来自context 和request。


跟route配置参数类似,view配置参数也可以分成普通参数、断言参数。在view的查找过程中,断言参数扮演了一个很重要的角色。一个view配置中断言参数的数量越多,调用该view所需要的环境就会越细化。一个申明了6个断言参数的view总是在申明了2个断言参数的view之前被查找、评估。必须所有的断言参数都匹配,该view才能被调用。(这跟route中的断言参数的作用是不一样的。)

1. 普通参数:
permission: 该view的访问权限,这个后续会具体介绍。
attr: Pyramid默认调用的是view类的__call__函数,如果需要指定调用其他方法,通过attr指定。如attr='index'。
renderer: 指定构建Response的渲染器。如json,模版等。后续章节详细介绍。
http_cache: 指定Response地expires和Cache-Control头属性。设置该值基本等同调用response.cache_expires。如
        http_cache=3600,表示通知浏览器缓存1小时、
        http_cache=datetime.timedelta(days=1),表示通知浏览器缓存1天、
        http_cache=0,表示无缓存
        http_cache=(3600, {'public':True}),表示缓存1小时,并且response.cache_control.public = True.
wrapper: 串联view以构造更复杂地Response。
decorator: view地装饰器,该装饰器需要返回一个接受context, request参数的view
mapper: 指定view mapper,用与转换view地参数和返回值。

2. 断言参数
name: view名字,在漫游时使用
context: 上下文,可以是对象或接口,也主要在漫游时使用
route_name: route名,主要用于URL分发。
request_type: 指定需要符合地request地接口,不常用。
request_method: GET、POST、DELETE、HEAD
request_param: 指定GET、POST必含的参数,如使用了request_param="foo=123"这种方式,则必须参数名、参数值都对应才算匹配。
match_param: match中必须包含地参数,如使用了request_param="foo=123"这种方式,则必须参数名、参数值都对应才算匹配。如果是一个字典,则必须里面每一项都匹配。
containment: resource树包含关系,必须是参数地子孙节点才匹配。
xhr: 匹配时是否处理HTTP_X_REQUESTED_WITH
header: 指定请求中必含的的HTTP header或header名值对。如‘User-Agent:Mozilla/.*’、'Host:localhost'
accept: 指定HTTP 请求头中客户端可以能够接受的内容类型,如'text/plain'、'text/*'、'*/*'
path_info: 匹配PATH_INFO的正则表达式。
custom_predicates: 定制的断言可执行对象。

Friday, March 30, 2012

使用Traversal来配置Pyramid项目(五)URL分发、漫游的混合配置



五、URL分发、漫游的混合配置

除了单独使用URL分发、漫游两种方式之外,Pyramid还提供了混合模式,可以将两者结合在一起使用。在混合模式下,漫游只发生在当某条route规则匹配之后才会发生。

跟纯粹的基于漫游地应用对比,两者地区别在于:
 * 纯粹漫游方式没有route定义;混合模式至少有一个。
 * 纯粹漫游方式root resource 是全局地,通过root factory在启动时创建;混合模式的root resource是基于每条route定义的。
 * 纯粹漫游方式漫游路径是整个PATH_INFO;混合模式是按route定义中的匹配字段匹配之后的一部分。
 * 纯粹漫游方式view配置不需要指明route_name,view定位时也不需要考虑它;混合模式则需先匹配route_name。


1. 混合模式

在add_route定义时:
 * pattern参数包含一个特殊的动态内容:*traverse 或 *subpath。
 * factory参数指向一个特定的root factory。
 * 如果没有factory参数,系统则采用全局的root factory(Configurator创建时传入)创建root对象。
 * 如果没有全局的root factory,则采用默认的root factory。


2. 使用*traverse定义混合模式

例如:
config.add_route('home', '{foo}/{bar}/*traverse')
config.add_route('home', '{foo}/{bar}/*traverse', factory='mypackage.routes.root_factory')

我们这里还是采用traversal案例中的root_factory,来看看下面的URL:
在这里例子中,漫游路径就是'a','b', 'c'

如果我们定义如下view配置:
config.add_view('mypackage.views.myview', route_name='home')
则mypackage.views.myview将在以下条件下被调用:
 * 匹配'home'为route名字
 * 漫游之后的view名字为空
 * context为任意对象

我们再来看下面的片段:
config.add_route('home', '{foo}/{bar}/*traverse',
                 factory='mypackage.routes.root_factory')
config.add_view('mypackage.views.myview', route_name='home')
config.add_view('mypackage.views.another_view', route_name='home',
                name='another')
则mypackage.views.another_view将在以下条件下被调用:
 * 匹配'home'为route名字
 * 漫游之后的view名字为another
 * context为任意对象
http://example.com/one/two/a/another URL可以访问another view


3. 使用traverse参数定义混合模式

使用*traverse定义的话,只能将URL最后的部分作为漫游序列。如果想要更灵活呢?Pyramid还引入了一个traverse参数。

例如:
config.add_route('abc', '/articles/{article}/edit', traverse='/{article}')

这里traverse参数的语法跟pattern是一样的,而且traverse中地内容必须完全包含在pattern中。

如上例,访问URL /articles/1/edit,则article匹配之后的值是1。因此,漫游路径就是‘/1’。即在root 对象上以‘1’为参数调用__getitem__,如果存在1这个对象,则将其作为context,传入view。

如果pattern中有*traverse定义,则忽略traverse参数。

4. 使用 *subath

如果想在route匹配时使用subpath,但又不想去执行漫游(漫游会产生subpath这个参数)。那么我们可以在pattern中使用subpath。
from pryamid.static import static_view

www = static_view('mypackage:static', use_subpath=True)

config.add_route('static', '/static/*subpath')
config.add_view(www, route_name='static')


Thursday, March 29, 2012

使用Traversal来配置Pyramid项目(四)复杂Traversal案例

四、一个复杂的案例

models.py
---------------------------
from sqlalchemy import (
    Column,
    Integer,
    Text,
    and_,
    )

from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    )

from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()

class MyCat(Base):
    __tablename__ = 'mycat'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    pid = Column(Integer)
    desc = Column(Text)

    def __init__(self, name, pid, desc):
        self.name = name
        self.pid = pid
        self.desc = desc
        
    def __getitem__(self, key):
        session= DBSession()
        
        is_file = False
        try:
            key.index(".")
            is_file = True
        except: pass
        
        if is_file:
            item = session.query(MyFile).filter(and_(MyFile.name==key, MyFile.cat==self.id)).first()
        else:
            item = session.query(MyCat).filter(and_(MyCat.name==key, MyCat.pid==self.id)).first()

        if item is None:
            raise KeyError(key)

        item.__parent__ = self
        item.__name__ = key
        return item

    def get(self, key, default=None):
        try:
            item = self.__getitem__(key)
        except KeyError:
            item = default
        return item

    def listall(self):
        session= DBSession()
        cats = session.query(MyCat).filter(MyCat.pid==self.id).all()
        files = session.query(MyFile).filter(MyFile.cat==self.id).all()
        return cats + files

class MyFile(Base):
    __tablename__ = 'myfile'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    cat = Column(Integer)
    save_path = Column(Text)
    desc = Column(Text)

    def __init__(self, name, cat, save_path, desc):
        self.name = name
        self.cat = cat
        self.save_path = save_path
        self.desc = desc


class MyRoot(object):
    __name__ = None
    __parent__ = None

    def __getitem__(self, key):
        session= DBSession()

        item = session.query(MyCat).filter(and_(MyCat.name==key, MyCat.pid==0)).first()
        if item is None:
            raise KeyError(key)

        item.__parent__ = self
        item.__name__ = key
        return item

    def get(self, key, default=None):
        try:
            item = self.__getitem__(key)
        except KeyError:
            item = default
        return item

    def __iter__(self):
        session= DBSession()
        query = session.query(MyCat).filter(MyCat.pid==0)
        return iter(query)

root = MyRoot()

def root_factory(request):
    return root



views.py
--------------------------
from .models import (
    DBSession,
    MyCat,
    MyFile,
    )

def view_root(context, request):
    print request.resource_url(context)
    return {'context':context, 'items':list(context), 'project':'MyTest'}

def view_cat(context, request):
    print request.resource_url(context)
    return {'context':context, 'items':context.listall(), 'project':'MyTest'}

def view_file(context, request):
    print request.resource_url(context)
    return {'item':context, 'project':'MyTest'}

def view_photo(context, request):
    return {'item':context, 'project':'MyTest'}


__init__.py
--------------------------
from pyramid.config import Configurator
from sqlalchemy import engine_from_config

from .models import (
    DBSession, 
    root_factory,
    )

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    config = Configurator(settings=settings, root_factory=root_factory)
    config.add_static_view('static', 'static', cache_max_age=3600)


    config.add_view('traverseonrdb.views.view_root', 
                    context='traverseonrdb.models.MyRoot', 
                    renderer="templates/root.pt")
    config.add_view('traverseonrdb.views.view_cat', 
                    context='traverseonrdb.models.MyCat', 
                    renderer="templates/cat.pt")

    config.add_view('traverseonrdb.views.view_photo',
                    name="photoview",
                    context='traverseonrdb.models.MyFile',
                    renderer="templates/photo.pt")    
    
    config.add_view('traverseonrdb.views.view_file',
                    context='traverseonrdb.models.MyFile',
                    renderer="templates/file.pt")

    return config.make_wsgi_app()


populate.py
--------------------------
# -*- coding: UTF-8 -*-

import os
import sys
import transaction

from sqlalchemy import engine_from_config

from pyramid.paster import (
    get_appsettings,
    setup_logging,
    )

from ..models import (
    DBSession,
    MyCat,
    MyFile,
    Base,
    )

def usage(argv):
    cmd = os.path.basename(argv[0])
    print('usage: %s <config_uri>\n'
          '(example: "%s development.ini")' % (cmd, cmd)) 
    sys.exit(1)

#直接在数据库中构建如下初始数据以供演示。
#MySite ---- MyPhoto ---- SportPhoto ---- ski.jpg
#         |            |
#         |            -- PrivatePhoto ---- love.jpg
#         |
#         -- MyNote ---- 20120327.txt
#         |
#         -- MyMP3 ---- paradise.mp3
#         |
#         -- MyVideo ---- hot.avi
def main(argv=sys.argv):
    if len(argv) != 2:
        usage(argv)
    config_uri = argv[1]
    setup_logging(config_uri)
    settings = get_appsettings(config_uri)
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    Base.metadata.create_all(engine)
    with transaction.manager:
        my_photo_cat = MyCat(name='MyPhoto', pid=0, desc="photos taken by myself")
        DBSession.add(my_photo_cat)
        my_note_cat = MyCat(name='MyNote', pid=0, desc="take a note a day to save life")
        DBSession.add(my_note_cat)
        my_mp3_cat = MyCat(name='MyMP3', pid=0, desc="my favorite mp3")
        DBSession.add(my_mp3_cat)
        my_video_cat = MyCat(name='MyVideo', pid=0, desc="all my hot videos")
        DBSession.add(my_video_cat)
        DBSession.flush()
        
        my_sport_photo_cat = MyCat(name='SportPhoto', pid=my_photo_cat.id, desc="photos of sports")
        DBSession.add(my_sport_photo_cat)
        my_private_photo_cat = MyCat(name='PrivatePhoto', pid=my_photo_cat.id, desc="secret photos")
        DBSession.add(my_private_photo_cat)
        DBSession.flush()
        
        afile = MyFile(name='ski.jpg', cat=my_sport_photo_cat.id, save_path="", desc="20111112 by canon")
        DBSession.add(afile)
        afile = MyFile(name='love.jpg', cat=my_private_photo_cat.id, save_path="", desc="20111111 by canon")
        DBSession.add(afile)
        afile = MyFile(name='20120327.txt', cat=my_note_cat.id, save_path="", desc="diary 20120327")
        DBSession.add(afile)
        afile = MyFile(name='paradise.mp3', cat=my_mp3_cat.id, save_path="", desc="mp3 download")
        DBSession.add(afile)
        afile = MyFile(name='hot.avi', cat=my_video_cat.id, save_path="", desc="hot video from some place")
        DBSession.add(afile)


详细代码参见:
https://github.com/eryxlee/pyramid_koans/tree/master/traverseonrdb

Wednesday, March 28, 2012

使用Traversal来配置Pyramid项目(三)Traversal案例

三、案例
1. 漫游示例



假如我们需要访问一个链接http://example.com/foo/bar/baz/biz/buz.txt。那么这个访问请求的PATH_INFO即为/foo/bar/baz/biz/buz.txt。
同时假如我们有如下的resource tree

整个漫游过程如下:
- 找到root resource,查询foo
- 找到foo resource,查询bar
- 找到bar resource,查找baz,找不到,抛出KeyError
得到 context 为bar resource
view 名 为 baz
subpath 为 (u'biz', u'buz.txt')
随即,漫游结束,开始view定位。

下面我们将resource树改成如下形状:

访问同样的链接http://example.com/foo/bar/baz/biz/buz.txt
整个漫游过程如下:
- 找到root resource,查询foo
- 找到foo resource,查询bar
- 找到bar resource,查询baz
- 找到baz resource,查询biz
- 找到biz resource,查找bux.txt,找不到,抛出KeyError异常
得到context为biz resource
view名为buz.txt
subpath为()

2. 完整案例

上例中:
5-6行,创建了一个简单的resouce类
8-9行,创建了一个resource 树
11-13行,定义了一个view
16行,创建带root_factory的configurator
17行,将view加入注册配置,并指定上下文为Resource类

下面我们就可以启动这个程序,然后访问如下URL:
http://localhost:8080/,浏览器即显示Here's a resource and its children: {'a': {'b': {'c': {}}}}
http://localhost:8080/a/b,浏览器即显示Here's a resource and its children: {'c': {}}
http://localhost:8080/a/b/c,浏览器即显示Here's a resource and its children: {}

http://localhost:8080/xyz,浏览器即显示 404 Not Found
http://localhost:8080/a/b/c/d,浏览器即显示 404 Not Found

3. 进一步改变
我们删去resource树的构建,让系统使用默认的root_factory,则我们可以得到下面的例子:

现在我们再访问如下URL
http://localhost:8080/foobar,浏览器显示Here's a simple view without any route/resource configuration.
这是一个traversal地变种,只有root resource,而且foobar是view名字,这种情况比较适合做简单地小程序来测试熟悉系统。

Tuesday, March 27, 2012

使用Traversal来配置Pyramid项目(二)Traversal算法

二、Traversal

1. root factory配置

理解清楚resource树之后,我们就可以自己按照需要构建定制的resource树,并在Pyramid项目中的__init__模块的main函数中将这棵树的根节点传给Pyramid配置方法Configurator。如果调用该方法的时候,没有root_factory参数或root_factory参数为None,系统会使用一个默认的root_factory,它永远返回一个不带子节点的resource。

2. 算法参考案例
class Resource(dict):
pass

def get_root(request):
return Resource({'a': Resource({'b': Resource({'c': Resource()})})})

可能地访问URL:/a/b/c

3. traversal算法

当一个用户对一个使用了Traversal算法的应用发起一次请求时,系统采用了如下算法来找到一个上下文resource和view名字。

1) request请求通过WSGI服务器,形成标准的WSGI请求,传递给router程序。
2) router 根据WSGI环境变量构造一个request对象。
3) 调用root_factory(get_root),得到root resource。
4) router采用WSGI中的PATH_INFO变量作为traverse(漫游)的路径。首先,去掉前导的'/',并将后续的路径片段按'/'切分形成一个漫游序列,如[u'a', u'b', u'c']。 注意我们这里的内容都是经过url unquote以及unicode decode的,所有数据值都是以unicode方式存在的。
5) 现在我们有一个root resource,以及一个漫游序列。我们从漫游序列中取出一个数据:'a',并将它传给root resource地__getitem__方法,我们可以得到另外一个resource(我们就叫它resource ‘A')。然后再从漫游序列中取出一个数据:'b',然后传给resource 'A'的__getitem__方法,我们又得到了一个新地resource(resource 'B')。再从漫游序列取出一个数据:’c',传给resource 'B'的__getitem__方法,则得到resource 'C'。
6) 一旦整个漫游序列用完,或任何一次__getitem__调用抛出一个KeyError异常、AttributeError异常的时候,以及遇到任何一个以@@开头的路径片段时,漫游过程即中止。
7) 不论以上任何一种情况终止了漫游,最后一个发现的resource即为我们需要的上下文(context)。如果这个时候漫游序列耗尽了,则view名字为‘’。否则紧邻的一个路径片段将被当作view名字。
8) 在view名字之后的任何路径片段极为subpath。

一旦得到了context,view名,subpath,漫游的所有任务即结束了,这些参数将传递给router,以便进行下一步的view定位工作。

使用Traversal来配置Pyramid项目(一)Resources

本节资料整理改编自Pyramid官方网站,部分内容按理解进行了增删整理和归纳,以便结构上更清晰。
Pyramid 1.3官方文档对于Traversal、resource、view等的说明比较分散凌乱,结构上不是很清晰(个人感觉),因此这里重新编排这部分内容,如果有人不能认同,请参阅Pyramid官方网站(http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/index.html)。

在当前网站建设中,我们经常可以看到网站地图这样一个功能,通过层级关系,将网站功能逐次列出来,其实我们仔细看的话,我们可以发现,严格来说,网站地图(特别是静态网站)也是一个树状结构的图,通过一层一层的目录结构形成一个URL,如http://127.0.0.1/photos/sports/1123.jpg,http://127.0.0.1/photos/sports/1120.jpg,http://127.0.0.1/photos/tour/1212.jpg,我们将这些URL按'/'分解,就可以看到,每一个URL其实就是在这颗树中从根到叶子节点的一次查询过程。

这就是Traversal基本思想的来源。这棵树中的所有节点就是Traverse所用到的resource。

一、Resources

1. 什么是resource

在Pyramid中,resource就是指一个app对应的结构树中的一个节点。在Pyramid中,即便从不使用,也会默认生成一个root resource。这个root节点就是这个resource树的根节点。一棵resource树就是用于表达网站结构一个类似递归字典结构。

如果使用Traversal方式,Pyramid将会根据PATH_INFO由根开始查找这棵resource树,直到找到一个resource(PATH用完或找不到子节点),Pyramid即肯定该resource为上下文(context),并用这个context和request中其他数据来定位一个合适的view。

如果使用URL分发方式,通常开发者将不会直接接触到resource树。在这种模式下,resouce树通常只有一个根节点,用来保存一些安全认证信息。

在很多情况下,参与resource树构建的不仅仅只有网站结构信息,还会有网站本身数据模型的数据。

根据树的定义,我们可以将resource分成容器节点、叶子节点两种。

针对Pyramid这种类似递归字典的数据结构,也可以表示为容器节点必须带__getitem__方法,通过调用这个方法传入名字可以得到对应的子节点,如果传入的名字查找不到相应的子节点,必须抛出KeyError异常。而叶子节点则没有__getitem__方法,即便有,也必须总是抛出KeyError异常。

按照上面的定义,我们可以简单构造如下resource树:
class Resource(dict):
pass

def get_root(request):
return Resource({'a': Resource({'b': Resource({'c': Resource()})})})


在这个例子中,get_root就是一个root_factory,它返回一个Resurce,我们可以命名它为root,那么root['a']就是一个容器节点,里面包含一个key为'b'的子节点root['a']['b'],这个root['a']['b']也是一个容器节点,里面包含一个key为'c'的子节点root['a']['b']['c'],而root['a']['b']['c']则是一个叶子节点。

如果我们将这个get_root作为Pyramid Configurator的root_factory参数,那么当一个URL访问如/a/b/c来临时,Traversal将找到这个key为'c'的对象,并用它与request来定位view。

在这里例子中,为了简化程序,我们将resource树中的所有节点都定义成了同一种类型,在真实环境中,每一个节点都可以是任意类型。


2. 节点的位置感知

为了URL生成,定位,安全以及traversal API等因素的需要,Pyramid规定resource树中的所有resource必须是可感知位置的,即每一个节点必须带__parent__、__name__两个属性。

__parent__属性永远指向该节点的父节点。__name__则是一个名字以供其父节点通过__getitem__函数来查找。

root resource中的__name__必须为空字串,__parent__为None。

如一个resource从root resource中通过__getitem__返回,则该resource的__parent__必须指向root resource,__name__必须跟调用__getitem__是的参数一致。即从任何一个节点都可以通过递归__parent__得到root resource。


3. 通过resource生成URL

一旦每一个resource都可以感知位置之后,我们就可以通过pyramid.request.Request.resource_url()来生成访问该resource的URL。
如root resource有一个名字为a的子节点resource_a,那么调用request.resource_url(resource_a)即可生成http://example.com/a/这样一个URL。注意,这里产生的URL最后带一个'/'。这是因为resource指代的是resource 树这个层级结构中的一个位置。

resource_url也可以带参数,如request.resource_url(resource_a, 'foo', 'bar')可以生成http://example.com/a/foo/bar这样的URL。注意,这里最后是不带'/'的。

或者传入query参数,如request.resource_url(resource_a, query={'a':'1'})可以生成http://example.com/a/?a=1


4. 跟resouce相关的一些api

pyramid.traversal.resource_path(resource) 返回不带域名的URL

pyramid.traversal.find_resource(resource, '/path') 通过path找到resource,可以使用绝对路径(前带'/'),或相对路径

pyramid.location.lineage() 产生一个包含其自身及依次父节点的生成器

pyramid.location.inside(b, a) 检查b是否是a的子孙。

pyramid.traversal.find_root() 找到根节点

pyramid.traversal.find_interface() 通过接口查找resource

Sunday, March 25, 2012

使用URL分发方式配置Pyramid项目(下)

五、匹配规则案例
下面通过几种可能的案例及其匹配之后的matchdict内容来解释一下这个匹配规则。
1. 规则 foo/{baz}/{bar}

foo/1/2 -> {’baz’:u’1’, ’bar’:u’2’} 最普通的匹配方式foo/abc/def -> {’baz’:u’abc’, ’bar’:u’def’} 注意,这里的值是unicode的
foo/1/2/ -> 不匹配,因为最后有一个'/'
bar/abc/def -> 第一段就无法匹配

2. 规则 foo/{name}.{ext}
foo/biz.html -> {’name’: u’biz’, ’ext’: u’html’}
/foo/biz -> 不匹配,没有'.'

3. 规则 /Foo Bar/{baz}
/Foo%20Bar/abc -> {'baz': u'abc'}

4. 规则 foo/{baz}/{bar}*fizzle
foo/1/2/ -> {’baz’:u’1’, ’bar’:u’2’, ’fizzle’:()}
foo/abc/def/a/b/c -> {’baz’:u’abc’, ’bar’:u’def’, ’fizzle’:(u’a’, u’b’, u’c’)}

5. 规则 foo/*fizzle
/foo/La%20Pe%C3%B1a/a/b/c -> {’fizzle’:(u’La Pe\xf1a’, u’a’, u’b’, u’c’)}

6. 规则 foo/{baz}/{bar}{fizzle:.*}
foo/1/2/ -> {’baz’:u’1’, ’bar’:u’2’, ’fizzle’:()}
foo/abc/def/a/b/c -> {’baz’:u’abc’, ’bar’:u’def’, ’fizzle’: u’a/b/c’)}

这里的.*是正则表达式,表示通配所有字符,跟例子4是不一样的,而且这里需要{}将占位符包含起来。



六、生成URL
在程序开发中,经常需要跳转到一个route对应的URL,这个时候就需要有一种方式来将route的定义转成一个实际的url了。Pyramid提供了url、path两种声称方式,url是指包含了当前protocol与hostname在内完整url。如果要生成不包含protocol与hostname的url则需要使用path方式。
如,定义了一个规则'{a}/{b}/{c}',那么
request.route_url(’foo’, a=’1’, b=’2’, c=’3’)可以得到http://example.com/1/2/3
request.route_path(’foo’, a=’1’, b=’2’, c=’3’)可以得到/1/2/3

在生成url的时候也要注意unicode,例如加了一条规则config.add_route(’la’, u’/La Peña/{city}’)
那么request.route_path(’la’, city=u’Québec’) 可以得到/La%20Pe%C3%B1a/Qu%C3%A9bec

使用了*通配符的规则,例如加了一条规则config.add_route(’abc’, ’a/b/c/*foo’)
request.route_path(’abc’, foo=u’Québec/biz’) 可以得到/a/b/c/Qu%C3%A9bec/biz
request.route_path(’abc’, foo=(u’Québec’, u’biz’)) 也可以得到 /a/b/c/Qu%C3%A9bec/biz


与route_url类似的还有static_url,经常需要在模版中使用它。

七、定制断言
有的时候,为了更精确的定位route,除了系统自带的断言之外,我们还会使用一些自定义的断言。这个时候就需要使用custom_predicates这个参数了。custom_predicates参数是一个包含了一个或多个断言的tuple。每一个断言接受两个参数,第一个参数是一个字典,一般叫info,其中有包含了一个叫match的字典,其内容为URL分解后的占位符名值对;还包含一个名为route的对象,指向当前匹配的Route。断言的第二个参数就是当前处理的request。

例如,我们需要某一个url片段是在'one','two','three'三者选其一。我们可以写如下的程序段:


从这个程序片段,我们可以看出:
1. 因需要动态传入'one','two','three'这些参数,所以定义了一个any_of函数。
2. any_of返回的是一个predicte可执行对象,该对象接受info,request两个参数。就是我们需要的断言。
3. 该断言查找到match中有否num这个字段,并且其值在'one','two','three'之中,则断言为真
4. custom_predicates接受一个tuple,可以一次传入多个断言,断言之间是 并且 的关系。任何一个断言不匹配,则不匹配该route。

另外,定制的断言还可以变更match这个字典中的值,从而可以完成一些更有趣的事情。例如


该程序判断match中的名值对,如果其名字是year、month、day中之一,就将其值转为整型。注意,这里必须返回True,以确保断言的要求。

进一步,我们可以将上面的程序做如下改写:


这段程序中,直接在匹配规则中定义了正则表达式以确保符合的URL中的year、month、day一定是数字,因此在断言里面就不需要用try/catch了。不过因为这里变更了match字典中的值,因此要严格注意断言的顺序,以免几个断言之间产生冲突,建议变更内容的断言放在断言tuple的最后。

我们再来看看info中另外一个属性: route,它指向了当前操作中的route,因此,它有name,pattern等route特有的属性,因此我们可以得到下面的程序


该程序就是判定如果route名是'y'、'ym'、'ymd'三者之一,就断言其year值必须是2010。

使用URL分发方式配置Pyramid项目(上)

本节资料整理改编自Pyramid官方网站,部分内容按理解进行了增删整理和归纳,以便结构上更清晰。


Pyramid中可以使用URL分发作为其URL映射机制,这种方式来源于其前身Pylons中引用的Routes项目,通过定义一系列有序的URL匹配规则来将每一个request请求关联到合适的view上。如果找不到任何一条相匹配的规则,Pyramid将转而采用漫游(traversal)方式来定位view。在URL规划过程时,可以在ini文件中设置pyramid.debug_routematch为true来打开调试模式以便观察规则匹配结果。


一、Route配置

通常地,在开发过程中,route跟view是相匹配出现地。Pyramid允许使用如下两种定义方式来完成配置:

1. 同时add_route、add_view。


config.add_route(’myroute’, ’/prefix/{one}/{two}’)
config.add_view(myview, route_name=’myroute’)


在这里,add_route的第一个参数是这个route的名字,第二个参数是用于匹配的规则。add_view的第一个参数是view对象,也可以是一个类似'mypackage.views.myview'这样的带包结构的view名。第二个参数是一个断言,表明只有route_name为‘myroute’时才能调用这个view。这两个函数均可以带一系列断言参数来缩小匹配范围。



2. 使用scan扫描view定义



在main函数中定义route,然后使用scan方法扫描整个包,载入标注的view。

config.add_route(’myroute’, ’/prefix/{one}/{two}’)

config.scan(’mypackage’)


在view中,则用view_config来标注对应的view

@view_config(route_name=’myroute’)

def myview(request):

return Response(’OK’)



这种方式其配置比较靠近实现代码,因此从阅读上更清晰,比较受到开发人员的欢迎。





二、规则语法
这里的匹配规则主要是指上面例子中add_route的第二个参数,即’/prefix/{one}/{two}’。在Pyramid中,定义了一个比较直观的匹配算法灵活地实现了各种URL的匹配。
规则1. 前导‘/’可有可无,‘{foo}/bar/baz’跟‘/{foo}/bar/baz’两则是等价的。
规则2. 每一个匹配段(在'/'之间的字符串)均可以是一个既定字符串,也可以是一个占位符,甚至可以是两者的组合。如‘foo/{name}.html’
规则3. 匹配段必须至少包含一个字符。如‘/abc/’这个url跟‘/abc/{foo}’无法匹配,因foo不能是空地,但跟‘/{foo}/’可以匹配。
规则4. 占位符可以附带一个正则表达式以进一步缩小匹配范围。如{foo:\d+}表示只匹配数字内容。其实单独的占位符都隐含了一个通配地正则表达式,即{foo}就是{foo:[^/]+},匹配不包含'/'的一个或多个任意字符。
规则5. 可以使用*号加占位符来通配URL剩余部分。*只能放在匹配规则的最后,而且无需‘/’前导。如foo/{baz}/{bar}*fizzle
规则6. 规则严格按照申明顺序进行匹配,一旦发现匹配即中止匹配过程,返回匹配的route。


三、匹配算法
编写route配置的最终目的是为了将匹配规则与WSGI中的PATH_INFO相匹配(或不匹配)。这个处理过程很简单,当一个请求来临的时候,Pyramid会按照定义的顺序逐条检查所有匹配规则。这里要注意的是,除了上面我们看到的名字,匹配规则这两个参数之外,add_route还可以带一些所谓的断言参数,必须所有的断言参数都为真才能判定一条route配置匹配,否则即为不匹配,跳过这条规则检查下一条,直到检查完所有定义的规则。
一旦找到匹配的规则,Pyramid即启动view寻找机制定位该匹配route最合理地view。如果遍历完所有规则还是找不到匹配,Pyramid则启动漫游机制进行resouce定位和view定位。
作为匹配过程地一个自然结果,匹配算法将会在request中添加两个跟其相关的属性:matchdict、matched_route。matchdict是一个包含了占位符名与其匹配值对的字典。这里要注意一下的是,占位符名的类型是string,对应值的类型是Unicode,URL中对应部分将会通过url decode进行转换,并将utf-8转成Unicode之后才进入matchdict。




四、函数参数
add_route除了name和pattern之外,还可以带一系列的参数,这些参数大致可以分为普通和断言两种类型,其中断言参数参与匹配算法,如果断言参数不匹配,该规则即不匹配。


1. 普通参数
name: route名,必填,单一应用唯一。
factory: 当该route匹配时用于生成资源根对象(root resource object)的可执行对象(函数或类),不指定系统将采用默认的root factory。
traverse: 指定特定的漫游路径
pregenerator: 通过route产生url时的前置操作,用于变更route_url传入的参数,不常用。
use_global_views: 当view定位时找不到一个route_name相同的view时,是否需要看看context,request,view名匹配而route_name不匹配的view
static: 是否是静态route,静态意味着该route只能用于生成url,不能用于request匹配,也就是不用通过url访问到它。


2. 断言参数
pattern: 匹配规则
xhr: 匹配时是否处理HTTP_X_REQUESTED_WITH,处理AJAX请求时常用。


request_method: ‘GET’、‘POST’、‘HEAD’、‘DELETE’、‘PUT’之一或是一个它们的tuple组合。不指定通配所有请求。


path_info: 匹配PATH_INFO的正则表达式。


request_param: 指定GET、POST必含的参数,如使用了request_param="foo=123"这种方式,则必须参数名、参数值都对应才算匹配。


header: 指定请求中必含的的HTTP header或header名值对。如‘User-Agent:Mozilla/.*’、'Host:localhost'


accept: 指定HTTP 请求头中客户端可以能够接受的内容类型,如'text/plain'、'text/*'、'*/*'


custom_predicates: 定制的断言可执行对象。

Friday, March 23, 2012

Pyramid是怎么处理每个Request请求的?

本节内容基本译自Pyramid官方网站,添加了一些自己的标注。

本节基本就是router.py这个模块的一一对应,需要加深理解最好打开源代码一边看代码一边看说明(本文针对Pyramid 1.3b2代码)。

上一篇说了Pyramid项目里main函数使用make_wsgi_app生成了一个名字叫Router的app,并将这个app传给了WSGI服务器。那么,显而易见,Pyramid处理Request请求就是跟这个Router类息息相关了。

1. WSGI服务器一旦接受到用户请求,即按照规范要求构造WSGI环境变量,然后将这些变量传递给router对象(app)的__call__方法。(参见paste deploy)

2. router使用request_factory创建一个request对象,并将WSGI环境变量传递给request对象。从这种处理方式可以看出,我们甚至可以自定义一个定制化的request_factory,以便生成特有的request。(参加router.py 179行)

3. 将request、registry对象放入到thread local栈中,以使每次请求之间的数据不会有冲突。今后可以用get_current_request()、get_current_registry()来得到这两个对象,不过在view中建议采用直接采用request、request.registry。(参加router.py 180-183行)

4. 触发一个NewRequest事件。(参加router.py 75行)

5. 如果在之前的应用配置处(main函数)配置了route配置项,Pyramid会调用routes_mapper函数进行URL分发。该函数检查预定义的route中(main函数)是否有与request包含的当前WSGI变量相匹配的项。(参加router.py 79行)

6. 如果找到匹配项,route_mapper函数会在request中加入两个属性:matchdict、matched_route。matchdict是一个针对具体PATH_INFO与预定义route项匹配之后形成的动态参数。比如定义了add_route(’idea’, ’site/{id}’),那么/site/1请求就会形成一个值为{’id’:’1’}的matchdict。matched_route则是指对应的那条route对象(参加router.py 94-95行)。随后,生成该route对应的root对象(参加router.py 120行)。如果这条route中配置了factory,则采用该factory生成root对象,否则采用默认的root_factory。(参见48、77、118行)

7. 如果没有找到相匹配的route,而且在创建Configurator对象时指定了root_factory参数,则使用该root_factory创建root对象。如果没有指定root_factory参数,则使用DefaultRootFactory来创建root对象。(参见48、77、118行)

8. Pyramid通过root、request参数漫游(traverser),traverser从root对象开始漫游(__getitem__方法)以寻找合适的context,如果root对象没有__getitem__方法,则将root对象赋给context。traverser返回一个带context、view名字等信息的字典。(参加router.py124-136行)

9. 将上一步取得的参数添加到request对象中,因此在view代码中,可以用request.context这种方式来访问他们。(参见router.py 138行)

10. 触发contextFound事件(参见router.py 139行)

11. 使用context、request、view名字等信息查找view,如果找不到,则抛出HTTPNotFound 异常(参见router.py 141-162行)

12. 如果找到了合适的view,Pyramid查看是否已经定义了认证策略,且这个view配置了访问权限。如是,则Pyramid将request中的访问者凭证与context附带的安全信息进行匹配,如果匹配通过,Pyramid则调用该view并且获得response对象。否则抛出HTTPForbidden异常。

13. 如果在上述过程中(root factory、traversal、view)抛出了异常,如HTTPNotFound、 HTTPForbidden,router将捕获这些异常,并赋给request.exception属性。然后寻找一个合适该异常的view,如果有这样的view,就调用它,产生response对象。如果找不到,就抛出该异常。

14. 触发NewResponse事件(参见router.py 188行)

15. 当一个response对象通过view或exception view生成之后,Pyramid将会遍历执行所有通过add_response_callback方法加进来的方法、对象。(参加 router.py 190-192 行)

16. 遍历执行通过add_finished_callback加入进来的方法、对象。(参见router.py 196-197 行)

17. 将threadlocal从栈中弹出。(参见 router.py 200 行)

Thursday, March 22, 2012

Pyramid启动时都能干些啥?

Pyramid找到我们项目的main函数之后,一切就相对简单了。

一、项目参数的传递

我们打开项目中的__init__.py文件,找到自动生成的main函数如下:




我们可以看到,main函数带了两个参数,global_config和setting。这是由Paste deploy传递过来的解析自development.ini的参数项。

其中global_config是development.ini中[DEFAULT]这一节中的参数定义,在我们自动生成的例子中,我们没有使用default节,所以也就没有传入自定义的参数,系统默认在global_config这个字典对象中加了两个参数项定义:
here: 表示development.ini文件所在目录的绝对路径
__file__: 表示development.ini文件的绝对路径

setting字典则包含了所有定义在[app:main]中的参数项(除了use),在我们的例子中,就包含了pyramid.reload_templates、pyramid.debug_authorization、sqlalchemy.url等的参数项。这些参数项都可以直接在我们的程序中使用。我们也可以在[app:main]中定义自定义的参数项,如mytest=testconfig。那么我们在view中就可以用如下程序取得定义的值。
settings = request.registry.settings
mytest = settings[’mytest’]

这里注意一下,放在其他section中的参数是传不进来的,如果一定要这样用,需要在程序中取到ini文件路径(global_config中的__file__),然后自行使用ConfigParser读取解析。


二、Configurator的配置

就如上面的程序,main函数做的最主要的工作就是做整合项目的配置,形成注册表。在Pyramid中可以配置的东西很多,我们可以简单看下几个常用的配置:

1. 静态资源
通过config.add_static_view('static', 'static', cache_max_age=3600)加入一个静态资源目录,一般写成config.add_static_view(name=’static’, path=’/var/www/static’) 或 config.add_static_view(name=’static’, path=’mypackage:a/b/c/static’) 这样的方式更清晰。
在add_static_view中,name代表了静态文件URL的前缀,如果我们使用了上面前一个例子,那么URL /static/foo.css 就会取指向文件/var/www/static/foo.css。后一个例子是相对与项目包结构的相对路径。
不过现在很多程序都是用外部资源来做静态资源的,也可以用config.add_static_view(name=’http://example.com/images’, path=’mypackage:static’)这种方式将静态文件指向外部服务器。
在程序中,就可以用static_url('pyramidkoans:static/favicon.ico') 或 static_path('pyramidkoans:static/favicon.ico') 这样的方式来引用静态资源。按照之前不同的定义,这里会解析出不同的结果。

2. add_route、add_view、view_config、scan
这几个函数定义了从URL到view的映射关系,add_route只在Route模式才会使用,traversal模式下不用。
view的配置可以使用add_view手工加入,也可以使用view_config标注+scan函数合作完成。


3. 权限
pyramid整合了一套ACL权限管理体系,也需要在configurtor中进行配置才能生效。pyramid权限管理划分了认证、授权两个部分,需要分别配置。
authn_policy = AuthTktAuthenticationPolicy('sosecret', callback=groupfinder)
authz_policy = ACLAuthorizationPolicy()

config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)


4. 用include包含子模块
项目大了之后,可以划分成多个子模块,然后用include包含子模块。

5.subscriber
Pyramid定义了一套简单的事件系统,开发者可以通过订阅一些特定的事件来改变框架的一些默认行为。


三、app的生成
完成了configurator的配置之后,pyramid就调用make_wsgi_app来生成一个符合WSGI规范的app,以便提供服务。
不过这个app是一个叫Router的类的实例,很奇怪吧。


好了,接下来就等待访问链接的来临吧

Wednesday, March 21, 2012

利用Amazon SES为应用构建简单的邮件服务

Amazon SES是AWS推出的一项邮件发送服务,类似于sendgrid等专业的邮件发送服务商。不过因AWS众多其他服务的捆绑支持,对于AWS平台上的应用还是具有不小的吸引力的。


一. 为啥使用SES呢

1. 对于发送量不稳定的应用,价格还是便宜的,发多少,算多少($0.10/1000条),没有套餐限制,附带流量也会产生费用。
2. 对于AWS EC2上的应用,有每天2000封的免费额度,基本足够大部分人使用了。

当然,也要牢记,amazon不是专业的mail发送公司,涉及到更专业的送达服务、垃圾邮件等方面还不那么健全,普通用用是完全没问题的,毕竟大家都知道邮件是怎么回事,有点小问题也都能谅解,顶多勤快点,多泡泡amazon服务论坛。


二. SES接入模式

amazon提供了多种接入支持以满足各种不同的需要:

1. smtp发送服务,提供了STARTTLS、TLS Wrapper两种不同方式,端口不同,可以用于编程或安装应用软件配置
2. api发送服务,可以通过amazon的api发送邮件,这里提供了一个封装。https://github.com/pankratiev/python-amazon-ses-api/blob/master/amazon_ses.py3. 与 Postfix 整合,配置其relayhost参数,使已有的应用能方便的使用SES服务能力。

三. smtp接入案例

1. 发件人认证

SES需要对每一个发件人邮箱进行认证,不存在的发件人不能够发送邮件。只需要再verify a new sender里面输入邮件地址,SES会往这个邮箱发送一个验证URL,在浏览器打开该URL即可完成验证。
SES 工作在sanbox状态下,需要同时将收信人也进行同样的认证。如果需要在生产环境使用,需要事先申请,24小时开通。

2. 生成SMTP 用户密码对

在发信之前,还需要生成一个smtp用户密码对,只需要点击SMTP Settings 中的Create My SMTP Credentials按钮即可在IAM中生成一个账号,同时显示其名字、密码,记录下来保存好需要在发送邮件时使用。

3. 小程序


mailServer = smtplib.SMTP_SSL('email-smtp.us-east-1.amazonaws.com', 465)
mailServer.set_debuglevel(1)
mailServer.login(IAM用户名, IAM用户密码)
mailServer.sendmail('发件人', ['收件人'], msg.as_string())
mailServer.close()

Tuesday, March 20, 2012

Pyramid项目是怎么启动起来的?

Pyramid 官方文档上专门有一个章节解释了Pyramid项目的启动过程,不过因为整个启动过程还涉及到一些非pyramid模块,光看那个说明未必能很好理解清楚整个启动过程。下面,我们通过一个小例子简单看看一个Pyramid项目是怎么一步一步启动起来的。


一、创建好项目,配置好开发环境。

按照pyramid的介绍,我们用如下命令很快就可以创建一个演示项目。

pcreate -s alchemy PyramidKoans
python setup.py develop

Ok,现在就可以用
pserve development.ini来启动这个项目了。


二、pserve是啥?

问题来了,为什么是用pserve来启动?pserve又是怎么启动载入的呢?

我们知道,pyramid安装的时候会在/bin目录下(如果你用virtualenv的话)生成一堆的执行文件,pserve就是其中一个,我们打开看看先:


这堆文件基本都是这个样子,只是里面参数有差异而已,其实这些都是easy_install根据egg包中的egg_info自动生成的,所以当然都差不多啦。

下面我们再打开pyramid EGG_INFO中的entry_points.txt文件对比一下:

可以看到在/bin下生成的执行文件跟上面console_scripts是一一对应的。

所以,我们就可以很好理解 load_entry_point('pyramid==1.3b2', 'console_scripts', 'pserve')() 这语句了,它就是来pyramid 1.3b2这个egg包中来找'console_scripts', 'pserve'这个入口,然后载入其对应的程序,这里就是pyramid.scripts.pserve:main。

再打开pyramid包script目录下,找到pserve.py找到main函数,刚才的pserve命令就是启动了这个main函数而已。(其实这个script就是原来的paste script内容,基本原封不动的转到这里来了。)


二、paste deploy来了

既然用了paste script,那当然就要用到paste deploy啦。

找到PServeCommand类的run方法,很容易就可以找到

这就是用了paste deploy中的loadserver,loadapp方法。

paste deploy是一个很常用定位、配置WSGI应用/服务器的工具包,它可以从一个定制的配置文件里面载入你所定义的app和server。这里这个配置文件就是上面启动命令里面提到的development.ini。加载了app和server之后,马上可以看到

因为这里的server跟app都是符合WSGI标准的,所有将app丢给server即可启动啦。


三、development.ini

上面已经找到了server和app的载入,那么又是怎么找到你所需要的那个server跟app呢?要理解这点,就需要看development.ini这个配置文件了。

这是一个基本上算标准的ini配置文件,不过paste deploy还是在上面做了一些定制。比如server:main、app:main、use这些定义。


server:main
这里的server是一个固定用法,表示下面的配置用于启动一个WSGI server。

server:main这一配置块定义了WSGI server启动方式以及它的启动参数,如host、port。
这里的第一行use = egg:waitress#main就是定义了如何启动WSGI server。这里use也是个固定用法,egg:waitress#main表示找到waitress这个egg包,找到里面的entry_points中main定义项:

然后查看waitress这个包中的serve_paste这个方法即可,这个方法就是将waitress这个WSGI server启动起来。具体的waitress内容这里就不详细讨论了。

在定义waitressa参数的时候要注意下,我们经常还需要定义一个threads参数,它表示需要启动多少线程来提供服务,默认是4个,如果需要在正式环境用的话是不够的。

当然,我们也可以通过修改配置重新使用paste来替换waitress提供服务。


app:main
这里app也是固定用法,表示app_factory,表示由这里的信息来生成WSGI app。main是配置块名字,在使用pipeline的时候用得到。

app:main块定义了app中所能使用的参数(除了use),如pyramid.reload_templates、pyramid.debug_templates、sqlalchemy.url…这些都是pyramid本身或app程序中需要使用的配置,如果app需要定义自己的配置,也可以放在这里。

这里我们要着重看一下的是use = egg:PyramidKoans这句。egg:PyramidKoans表示从PyramidKoans这个egg包找到app的入口,这里省略了#main(默认就是它啦)。所以,又要找entry point啦,打开PyramidKoans项目中的setup.py文件(没打包发行之前,entry_points存在这里面。),

我们在这里也找到了entry_points,看[paste.app_factory]这里的定义,paste deploy就是来找这一段配置,再看main = pyramidkoans:main,这里第一个main就是我们刚才说的省略了的那个main,然后它指向了 pyramidkoans:main,表示需要到paramidkoans这个包里去找main这个方法。


终于走到我的程序了!