Sunday, April 29, 2012

使用egg包方式在SAE上部署Pyramid应用


用源码在SinaAppEngine 上部署Pyramid应用经常会遇到文件过多的问题,因此尝试了一下egg包部署(只是喜欢egg格式而已~~,本文未涉及其他打包部署方式),主要步骤如下:

1. 打包

因为有的时候需要变更一些第三方包中的内容以适应平台的需要,我们需要将应用涉及到的第三方包重新打包(如果没有变更,不需要重新打包)。这个时候我们可以使用pip zip命令来完成这一工作。

不过不知何故,直接使用类似pip zip --no-pyc pyramid-1.3-py2.6.egg打出来的包里面包含了pyramid-1.3-py2.6.egg这个目录名,直接使用这些包会导致import失败。我们直接打开pip包中的zip.py文件,找到其中的zip_package方法,将其中涉及到的module_name的两行做修改,去掉往zip包写目录的功能即可。

对Pyramid涉及到的第三方包逐个运行pip zip命令即可完成打包工作。打包时注意带上--no-pyc参数,将pyc排除在外。

对于一些其中包含c模块的包,其中的包名可能会涉及到一些本地环境,如OS等。比如zope.interface-3.8.0-py2.6-macosx-10.7-intel.egg,可以直接将其名字改为zope.interface-3.8.0-py2.6.egg。

另外,目前SAE使用python 2.6版本,因此最好使用对应的python环境来打包。


2. 配置virtualenv.bundle目录

将所有涉及到的egg包拷贝到virtualenv.bundle中之后,在该目录建立一个easy-install.pth文件,里面包含如下内容:

import sys; sys.__plen = len(sys.path)
./Chameleon-2.8.4-py2.6.egg
./Mako-0.7.0-py2.6.egg
./MarkupSafe-0.15-py2.6.egg
./ordereddict-1.1-py2.6.egg
./PasteDeploy-1.5.0-py2.6.egg
./pyramid-1.3-py2.6.egg
./repoze.lru-0.5-py2.6.egg
./translationstring-1.1-py2.6.egg
./venusian-1.0a6-py2.6.egg
./WebOb-1.2b3-py2.6.egg
./zope.deprecation-3.5.1-py2.6.egg
./zope.interface-3.8.0-py2.6.egg
import sys; new=sys.path[sys.__plen:]; del sys.path[sys.__plen:]; p=getattr(sys,'__egginsert',0); sys.path[p:p]=new; sys.__egginsert = p+len(new)

3. 修改index.wsgi文件

用site.addsitedir将这个目录包含到系统环境中。

import sae

import os
import site

app_root = os.path.dirname(__file__)
site.addsitedir(os.path.join(app_root, 'virtualenv.bundle'))

from pyramid.config import Configurator

settings = {"pyramid.reload_templates":"false",
            "pyramid.debug_authorization":"false",
            "pyramid.debug_notfound":"false",
            "pyramid.debug_routematch":"false",
            "pyramid.default_locale_name":"en"
            }

config = Configurator(settings=settings)
config.add_static_view('static', 'mypyramid:static', cache_max_age=3600)
config.add_route('home', '/')
config.scan("mypyramid")

app = config.make_wsgi_app()

application = sae.create_wsgi_app(app)


4. 上传

使用svn ci -m "my first pyramid in egg package"


5. 浏览

使用SAE提供的链接访问刚才部署的应用吧。


6. example下载与使用


本次使用的例子已经上传到vdisk,下载

下载该例子包含了pyramid开发所需的文件支持,可以直接在本地使用pyramid环境完成日常开发。开发完毕再将涉及的egg包打包到vieturlenv.bundle目录,静态文件拷贝到static目录上传即可。



本例子只适合当前的SAE环境,今后SAE环境的变化可能会导致本例不能正常运行。


Friday, April 27, 2012

在SAE上部署Pyramid应用(Python应用)


SAE是国内做的比较出色的PaaS平台,而且很早就推出了Python支持。不过因为没有内置Pyramid框架的支持(一大堆杂七杂八的包,支持也不易啊~~),要将一个Pyramid应用部署到还是要花点时间的。

一、创建应用

1. 按照SAE手册,在“我的首页”创建新应用:pyramidkoans。


3. 创建目录1


二、加入Pyramid应用

1. 新建一个Pyramid应用:mypyramid

2. 将mypyramid/mypyramid目录拷贝到1目录

3. 将mypyramid/mypyramid/static 目录拷贝到1目录(SAE处理了/static开始的请求,不过不能删除原来目录下的static目录及内容,否则pyramid在生成static_url的时候会出错。)

4. 编辑index.wsgi文件为

import sae

import os
import sys

app_root = os.path.dirname(__file__)
sys.path.insert(0, os.path.join(app_root, 'virtualenv.bundle'))

from pyramid.config import Configurator

settings = {"pyramid.reload_templates":"false",
            "pyramid.debug_authorization":"false",
            "pyramid.debug_notfound":"false",
            "pyramid.debug_routematch":"false",
            "pyramid.default_locale_name":"en"
            }

config = Configurator(settings=settings)
config.add_static_view('static', 'mypyramid:static', cache_max_age=3600)
config.add_route('home', '/')
config.scan("mypyramid")

app = config.make_wsgi_app()

application = sae.create_wsgi_app(app)


三、整理Pyramid支持包

1. 打包

SAE提供的bundle_local.py在处理

zope.deprecation-3.5.1-py2.7.egg
zope.interface-3.8.0-py2.7-macosx-10.7-intel.egg

这两个包的时候会出错(zope目录已经建立。。。,没判断。)。

因此自己一个一个拷贝到virtualenv.bundle吧。

2. 修改

因SAE不支持载入.so文件,因此需要对

zope.interface-3.8.0-py2.7-macosx-10.7-intel.egg
MarkupSafe-0.15-py2.7-macosx-10.7-intel.egg

这两个包进行修改,修改内容为:
删除 zope.interface-3.8.0-py2.7-macosx-10.7-intel.egg包中的 _zope_interface_coptimizations.c  _zope_interface_coptimizations.py
删除 MarkupSafe-0.15-py2.7-macosx-10.7-intel.egg包中的 _speedups.c _speedups.py

3. 下载ordereddict-1.1包,解开将其中的ordereddict.py放入virtualenv.bundle根目录。http://pypi.python.org/pypi/ordereddict

4. 最终的项目目录结构如下:



virtualenv.bundle中目录结构如下:




5. 因考虑到可能需要修改一些包中的文件,这次测试没有用egg、zip包(SAE提到了可以将全部第三方代码打成一个zip包,没提每个第三方能否分别用一个egg包的方式),全部解开上传的。


四、上传到SAE

在pyramidkoans运行svn add 1,然后运行svn ci -m "my first pyramid app"即可。

然后就可以通过SAE提供的URL进行访问了。





Thursday, April 26, 2012

Amazon EC2 AutoScaling配置备忘录


Amazon AWS EC2 提供了良好的AutoScaling能力,可以根据预先定义的几个系统性能参数的阈值提供自动地服务能力伸缩,并且同时提供了良好的使用文档、开发文档,基本看完其开发文档即可正常使用其提供的功能。本文仅记录一次AutoScaling过程的一些操作步骤备忘以及一些需要今后注意地地方。

一、简单实验

1. 下载AutoScaling工具包



2. 配置本地环境

如果电脑上没有安装Java,就安装一个,然后处理后续操作。

export JAVA_HOME=/Library/Java/Home

export AWS_AUTO_SCALING_HOME=/Users/eryxlee/amazon/tools/AutoScaling-1.0.49.1

export PATH=$PATH:$AWS_AUTO_SCALING_HOME/bin

编辑认证文件credential.txt,里面包含如下内容:(具体keyed,key到AWS Security Credentials中寻找。
AWSAccessKeyId=XXXXXXXXXXXXXXXXX
AWSSecretKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

编辑完之后,变更权限
chmod 600 credential.txt

export AWS_CREDENTIAL_FILE=/Users/eryxlee/amazon/tools/AutoScaling-1.0.49.1/credential.txt


3. 简单命令操作

配置完环境变量之后,运行as-cmd命令即可得到一个AutoScaling的命令列表。

运行as-describe-auto-scaling-groups得到No AutoScalingGroups found,如果你之前没有建过的话


4. 建立简单的一个AutoScaling配置,并最终销毁它

4.1. 建立节点启动配置

as-create-launch-config MyTestLC --image-id ami-7530ee1c --instance-type t1.micro

这里:
MyTestLC是配置名
image-id是AMI的id
instance-type是节点类型

运行完之后可以运行as-describe-launch-configs查阅已经创建的配置。

4.2. 建立AutoScaling组

as-create-auto-scaling-group MyTestGroup --launch-configuration MyTestLC --availability-zones us-east-1a --min-size 1 --max-size 1

这里:
MyTestGroup是组名
launch-configuration是上一步创建的配置名
availability-zones指定节点创建所在的可用区域
min-size指最小节点数量
max-size指最大节点数量

运行完之后可以运行验证命令:

as-describe-auto-scaling-groups --headers

可以的大如下类似信息:
AUTO-SCALING-GROUP  GROUP-NAME   LAUNCH-CONFIG  AVAILABILITY-ZONES  MIN-SIZE  MAX-SIZE  DESIRED-CAPACITY
AUTO-SCALING-GROUP  MyTestGroup  MyTestLC       us-east-1a          1         1         1              
INSTANCE  INSTANCE-ID  AVAILABILITY-ZONE  STATE      STATUS   LAUNCH-CONFIG
INSTANCE  i-cf3291a8   us-east-1a         InService  Healthy  MyTestLC

4.3. 查验结果

现在登录AWS console,可以看到已经有一个节点在启动了。不过我们也可看到,这个节点没有配置security group,也没有配置keypair,基本上属于不可用状态。(除非已经在AMI中配置好了所有自启动并且计算完将结果传输到其他服务器上。)

4.4. 销毁节点

在清理AutoScaling组之前,推荐先销毁所有的节点,运行一下命令,指定最大最小数量均为0:

as-update-auto-scaling-group MyTestGroup --min-size 0 --max-size 0

整个节点关闭过程需要几分钟,耐心等待。

4.5. 销毁AutoScaling组

as-delete-auto-scaling-group MyTestGroup

4.6. 销毁启动配置

as-delete-launch-config MyTestLC

这样一个AutoScaling的轮回就基本结束了。不过也可以看到,除非AMI配置很好,这个AutoScaling组做不了啥工作。


二、建立一个真实可用的AutoScaling组

1. 准备工作

要做好一个运行良好的AutoScaling组,需要实现做好规划,准备在节点上面完成什么功能,结果如何输出,是否需要人工登录,是否需要安全配置等等。

下面我们就看一个简单的WEB应用服务器的简单扩容配置方式,假定我们需要提供一个Pyramid应用,它提供80端口访问,使用独立的MySQL数据库,并且管理人员偶尔需要登录上去进行简单管理。

1.1. AMI制作

先从已有AMI启动一个节点,假定这个节点已经设置好OS配置,可以正常运行。下面开始应用的部署:

* 在该节点上配置好python环境
* 安装好Pyramid
* 部署应用
* 配置应用Log集中输出到特定服务器
* 配置supervisor服务

因为AutoScaling的自身特性,不推荐在其上配置MySQL之类的数据库服务,也不建议在上面存储任何数据,如果要存储文件,建议直接存储到S3。这样该节点随时打开关闭都不会影响应用数据完整性。

也可以配置puppet等自动安装服务,不过如果应用系统稳定,建议直接做出AMI,免去启动之后安装配置动作,直接启动即可用。

如果节点启动之后需要一个比较长的暖场时间,则需要小心配置之后涉及的冷却时间和ELB存活自动检测以免出现多启动节点或ELB认为节点已正常但实际还未提供服务的情况。

1.2. keypair生成

如果今后希望可以登录AutoScaling的节点,则需要在创建的时候提供keypair,建议在开始配置前准备好一个可用的keypair

1.3. security group配置

如果需要配置对外访问安全,还需要配置好security group。我们这里因只需要提供80端口(Pyramid 6543端口)、22端口服务,则配置22端口对所有IP可访问。6543端口对ELB可访问(amazon-elb/amazon-elb-sg),ELB对外提供80端口,外网无法直接访问节点6543端口。

1.4. ELB配置

配置ELB HTTP 80端口转向6543端口


2. 环境配置

2.1. AutoScaling的配置

同第一节内容

2.2. CloudWatch配置

下载CloudWatch工具包,http://aws.amazon.com/developertools/2534

export AWS_CLOUDWATCH_HOME=/Users/eryxlee/amazon/tools/CloudWatch-1.0.12.1

export PATH=$PATH:$AWS_CLOUDWATCH_HOME/bin


3. 创建启动配置

as-create-launch-config MyTestLC --image-id ami-7530ee1c --instance-type t1.micro --key testenv-keypair --group test-autoscale

这里多了两个参数:

key指key pair的名字
group指security group的名字


4. 创建AutoScaling组

as-create-auto-scaling-group MyAutoScalingTestGroup --launch-configuration MyTestLC --availability-zones us-east-1a us-east-1b --min-size 1 --max-size 10 --load-balancers autoscale

这里可用区域指定了2个,之后创建的节点可以分布在这两个区域中。
load-balancers指定了ELB名字

同第一节,创建完之后,即可看到一个节点已经启动,现在就可以测试直接通过ELB的DNS名字可以直接访问Pyramid应用。如果有任何问题,检查之前的配置。

注意:新建的ELB很有可能会出现节点加入了之后Health Check过不了没法提供服务的情况(已经遇到几次了。),这时候只要手工将节点从ELB中移除,然后再加入,就会正常。正式使用前,最好几个配置到Auto Scaling中的可用区域中的节点都试试看,直到正常自动加入为止。


5. 配置ScaleUp策略

as-put-scaling-policy MyScaleUpTestPolicy --auto-scaling-group MyAutoScalingTestGroup --adjustment=1 --type ChangeInCapacity --cooldown 90

这个命令指定了一直ScaleUp的策略,策略内容为每次增加一个节点,之后90秒为冷却时间。冷却时间是为了确保刚才启动的节点已经真正提供服务之后再进行系统负载评估。在冷却时间内,其他Scale操作命令都无效,安静的等待刚才的服务节点提供服务能力。良好的冷却时间选择能够有效的防止系统规模暴涨暴跌。

该命令执行之后,会返回类似arn:aws:autoscaling:us-east-1:538202822254:XXXXXXXXXXX的一长串ARN名字,记得保留这个名字后面有用。


定义了ScaleUp策略之后,怎么触发这个策略呢?那就需要用到刚才配置的CloudWatch工具了。下面的命令定义了一个CPU负载过高的告警,一旦CPU一段时间内达到定义的负载阈值,就会触发这个告警:

mon-put-metric-alarm MyHighCPUAlarm --comparison-operator GreaterThanThreshold --evaluation-periods 1 --metric-name CPUUtilization --namespace "AWS/EC2" --period 60 --statistic Average --threshold 60 - -alarm-actions POLICY-ARN_from_previous_step --dimensions "AutoScalingGroupName=MyAutoScalingTestGroup"

这是一个很复杂的命令。它的主要目的就是评估现有系统性能,一旦发现超出告警设定,就使用AWS SNS发出消息。其参数的含义是:

comparison-operator:比较操作符,主要有GreaterThanOrEqualToThreshold、GreaterThanThreshold、LessThanOrEqualToThreshold、LessThanThreshold 这几种。
evaluation-periods:评估时间周期
metric-name:度量对象,这里选择CPU
namespace:命名空间,如AWS/EC2、AWS/ELB
period:告警时间周期
statistic:统计方式,可以是Minimum、Maximum、Sum、Average、SampleCount
threshold:阈值,CPU达到60%
alarm-actions:这里是SNS话题名,往这个话题发送消息
Dimensions:是一个key/value对,用于标识这个度量对象的一些特定属性。

注意:
a. 为了演示需要(尽快看到效果),在上面几个命令中,各参数的值都被人为缩小了,实际环境所用的值大多都会大于上面的设定。具体的配置值需要根据实际场景反复实验推敲。
b. 注意各配置参数之间的关系,以及它们之间的配合。
c. AWS还提供了定时ScaleUp等很多策略配置,具体查看AWS文档。

6. 定义ScaleDown策略

定义了ScaleUp策略之后,最好还要定义ScaleDown策略,这样就会在负载低的时候减少节点,从而减少资源占有,降低成本。

as-put-scaling-policy MyScaleDownTestPolicy --auto-scaling-group MyAutoScalingTestGroup --adjustment=-1 --type ChangeInCapacity --cooldown 90

mon-put-metric-alarm MyLowCPUAlarm --comparison-operator LessThanThreshold --evaluation-periods 1 --metric-name CPUUtilization --namespace "AWS/EC2" --period 60 --statistic Average --threshold 15 --alarm-actions POLICY-ARN_from_previous_step --dimensions "AutoScalingGroupName=MyAutoScalingTestGroup"

命令含义基本跟ScaleUp类似。


7. 加压吧!

完成上面的配置之后,马上可以再开个节点安装个siege进行压力测试了,也应该可以在Console中看到instance的增加减少,不过需要点耐心。

通过压力测试也可以验证各参数之间的配合是否协调,是否能满足设计需要。


8. 清理现场

如果不要这个AutoScaling配置了,还需要完成下面动作清理现场。
mon-delete-alarms MyLowCPUAlarm
mon-delete-alarms MyHighCPUAlarm
as-update-auto-scaling-group MyAutoScalingTestGroup --min-size 0 --max-size 0
as-delete-auto-scaling-group MyAutoScalingTestGroup
as-delete-launch-config MyTestLC


9. 计费事项

实现了AutoScaling是件很爽的事情,除了收到账单的时候。AWS的整体费用真正用起来可不算低哦。在整个AutoScaling配置中,涉及到收费的地方有:

a. Amazon SNS,每月10w免费额度,之后每10w收6美分
b. Instance,按节点量、大小计算
c. ELB,按时长+流量计算
d. CloudWatch,按节点+告警计算
e. 流量费用
f. EBS、EBS IO费用,如果用了的话



Monday, April 23, 2012

Pyramid 1.3 使用说明文档


这是一个Pyramid 1.3使用文档的集合,其主要内容编译自官方文档,部分片段、案例按照自己的理解和测试做了改写,去掉了其中繁琐重复以及不常用的内容、并将一些个人感觉比较凌乱的章节进行了整理。



Pyramid 1.3基础


因Pyramid1.3相比以前的版本从使用命令方面变更比较多,因此补个简单使用的章节。

一、虚拟环境

在安装Pyramid之前,推荐首先利用virtualenv建立一个专用的虚拟环境。

virtualenv --no-site-packages pyramid-1.3

这里使用了--no-site-packages参数,避免了跟系统安装包的冲突,可以保证一个比较干净的环境。

使用source bin/activate激活这个虚拟环境,以便后续操作。


二、安装Pyramid

安装Pyramid 1.3版本。

easy_install pyramid==1.3

系统会自动下载安装所依赖的包。


三、建立工程

使用命令:

pcreate -s alchemy simpleCRUD

即可建立一个项目框架,这里可以选择 alchemy、starter、zodb三种类型的项目框架(scaffold)。


四、进入开发模式

项目建立好之后,在开始开发前,还需要在该项目目录下运行如下命令:

python setup.py develop

该命令会在我们刚才建立的虚拟环境中的lib/python/site-packages目录下建立一个指向本目录的指针 -- 1). simpleCRUD.egg-link文件(该文件内容包含了本项目的绝对路径),2). 在easy_install.pth中加入本项目的绝对路径。如果setup.py中包含了未安装得python包,也会在此时安装这些包。


五、运行单元测试

生成的项目框架默认有一个单元测试的例子,在项目配置好之后,即可运行单元测试命令:

python setup.py test -q

如果安装了nose、coverage,还可以得到单元测试覆盖率:

nosetests --cover-package=simplecrud--cover-erase --with-coverage


六、安装数据库

如果是使用了sqlalchemy的项目,还需要额外的一步:建立数据库。

initialize_simpleCRUD_db development.ini

运行该命令将会执行simplecrud/script目录下的initializedb.py文件,在该文件中,可以建立数据库、插入初始数据。


七、启动应用

做完上面所有步骤之后,即可使用:

pserve development.ini --reload

启动服务器,开始提供服务了。

用Pyramid建立一个简单的CRUD程序


熟悉Pyramid之后,可以很方便的建立一个简单的CRUD程序。下面我们展示一下一个最简单的书籍管理应用的构建过程。

一、建立项目框架

pcreate -s alchemy simpleCRUD

cd simpleCRUD

python setup.py develop

二、建立model

model.py 加入代码:

class Book(Base):
    __tablename__ = 'book'
    id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
    name = Column(Unicode(64), unique=True, nullable=False)
    author = Column(Unicode(32), nullable=True)
    desc = Column(Unicode, nullable=True)
    ISBN = Column(Unicode(20), nullable=True)
    price = Column(Float, nullable=True)

    def __init__(self, name, author, desc, ISBN, price):
        self.name = name
        self.author = author
        self.desc = desc
        self.ISBN = ISBN
        self.price = price


initializedb.py 中main函数变更为:

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:
        book = Book(name='Python', author='chen', desc='desc', ISBN='978-7-121-06874-4', price=69.80)
        DBSession.add(book)


然后运行

initialize_simpleCRUD_db development.ini

建立数据库,并在数据库中插入第一条记录。


三、建立route结构

在__init__.py中的main函数加入代码:

    config.add_route('book_list', '/book/list')
    config.add_route('book_detail', '/book/detail/{id}')
    config.add_route('book_add', '/book/add')
    config.add_route('book_edit', '/book/edit/{id}')
    config.add_route('book_delete', '/book/delete/{id}')

通过上述URL结构即可构造一个简单的CRUD应用。


四、第一个功能list

首先实现list功能,确保list能正确读取数据库数据。因此,构建单元测试:

    def test_book_list(self):
        from .views import book_list
        request = testing.DummyRequest()
        info = book_list(request)
        self.assertEqual(len(info['books']), 1)
        self.assertEqual(info['books'][0].name, 'Python')

为了满足这个单元测试运行的条件,修改setUp函数为:

    def setUp(self):
        self.config = testing.setUp()
        from sqlalchemy import create_engine
        engine = create_engine('sqlite://')
        from .models import (
            Base,
            Book,
            )
        DBSession.configure(bind=engine)
        Base.metadata.create_all(engine)
        with transaction.manager:
            book = Book(name='Python', author='chen', desc='desc', ISBN='978-7-121-06874-4', price=69.80)
            DBSession.add(book)

该setUp函数建立一个专门用于单元测试的内存数据库,然后建立所有model定义的表结构,并插入第一条记录。在完成上面的初始化之后,我们的第一个单元测试的逻辑是正确的了。

现在再来构建对应的view:

@view_config(route_name='home', renderer='simplecrud:templates/book/book_list.pt')
@view_config(route_name='book_list', renderer='simplecrud:templates/book/book_list.pt')
def book_list(request):
    books = DBSession.query(Book).all()
    return dict(books = books)

这里就不分页了,全部一页取出显示。并且首页就是list页。

这时候运行 python setup.py test 即可运行单元测试查看是否满足。

但单元测试满足之后,现在使用pserver development.ini还是无法看到页面内容的,这需要建立对应的渲染模版,我们这里先使用chameleon模版来完成这个工作。



该模版的主要工作就是显示一个table,里面包含所有数据记录。在模版中的tal属性是chameleon定义的特殊属性。

1. tal:repeat="item books" 循环books列表,逐项取出赋值给item

2. repeat.item.number 循环计数器

3. tal:content="repeat.item.number" 替换该标签的内容部分,这里就是内容“1”

4. request.route_path('book_detail', id=item.id) 生成book_detail链接

5. tal:attributes="href string:${request.route_path('book_edit', id=item.id)}" 将标签的href属性替换


五、显示详细信息页面

详细信息页面基本构成跟list页面基本类似,最主要的区别是,当book id不存在时,详细信息页面需要返回HTTPForbidden,我们需要在单元测试中覆盖这种情况。

    def test_book_detail_unauthorized(self):
        from pyramid.httpexceptions import HTTPForbidden
        from .views import book_detail
        request = testing.DummyRequest()
        request.matchdict['id'] = 555
        info = book_detail(request)
        self.assertIsInstance(info, HTTPForbidden)

注意,如果我们想要在视图中使用raise的化,要使用self.assertRaises(HTTPForbidden, view_fn, request)这种方式来捕获。

六、使用simpleform来添加记录页面

添加记录程序一般会涉及到显示Form、存储、出错这样三种情况,因此,构建三个测试用例:

在request中什么都没有的情况下,显示form
    def test_book_add_form(self):
        from pyramid_simpleform.renderers import FormRenderer
        from .views import book_add
        request = testing.DummyRequest()
        info = book_add(request)
        self.assertIsInstance(info['renderer'], FormRenderer)

在request中指定了是POST时,将取到的数据存入数据库,成功后页面转向list页面,最后验证存入正确
    def test_book_add_save(self):
        from pyramid.httpexceptions import HTTPFound
        from .views import book_add
        from .models import Book

        _registerRoutes(self.config)

        request = testing.DummyRequest({'name':'a new book',
                                        'author':'a new author',
                                        'ISBN':'a new ISBN',
                                        'desc':'a new desc',
                                        'price':'6.0',
                                        'submit':'save'})
        request.method = 'POST'
        info = book_add(request)
        self.assertIsInstance(info, HTTPFound)

        added = DBSession.query(Book).filter(Book.id==2).first()

        self.assertEqual(added.name, 'a new book')
        self.assertEqual(added.author, 'a new author')

在存入数据有错误的情况下,返回提交界面,并确保不会变更数据库已有数据
    def test_book_add_conflict(self):
        from pyramid_simpleform.renderers import FormRenderer
        from .views import book_add
        from .models import Book

        _registerRoutes(self.config)

        request = testing.DummyRequest({'name':'Python',
                                        'author':'a new author',
                                        'ISBN':'a new ISBN',
                                        'desc':'a new desc',
                                        'price':'6.0',
                                        'submit':'save'})
        request.method = 'POST'
        info = book_add(request)
        self.assertIsInstance(info['renderer'], FormRenderer)

        added = DBSession.query(Book).filter(Book.id==1).first()

        self.assertEqual(added.name, 'Python')
        self.assertEqual(added.author, 'chen')
        self.assertEqual(added.ISBN, '978-7-121-06874-4')


下面开始构造视图方法。一般情况下,从form读取数据都有有个验证、校验、转换过程,为了程序简单,我们采用simpleform,以及formencode用于数据校验,form构成。

class BookSchema(Schema):

    filter_extra_fields = True  # 过滤掉其他字段
    allow_extra_fields = True  # 允许form中有其他字段

    name = validators.String(min=2, max=64, not_empty=True)  # name最小2字符,最大64
    author = validators.String(max=32)
    desc = validators.String()
    ISBN = validators.String(max=20)
    price = validators.Number(max=1000)    # price为数字,浮点型

@view_config(route_name='book_add', renderer='simplecrud:templates/book/book_add.pt')
def book_add(request):
    form = Form(request, schema=BookSchema)
    if form.validate():      # 如果是正确提交

        book = Book(form.data.get("name"),
                    form.data.get("author"),
                    form.data.get("desc"),
                    form.data.get("ISBN"),
                    form.data.get("price"))

        try:
            DBSession.add(book)
            DBSession.flush()
            transaction.commit()

            return HTTPFound(location=route_path("book_list", request))  # 返回到list页面
        except IntegrityError:  # name重复,设置form出错信息
            transaction.abort()
            form.errors["global_error"] = 'database insert error, maybe book name conflict.'
        except Exception, e:
            transaction.abort()
            form.errors["global_error"] = 'database error.' + str(e)
            log.error("database error!")

    return dict(renderer=FormRenderer(form))  返回form页面

这里需要注意的是,单元测试中,并没有去构造route表,但我们程序的重定向中使用了route_path方法,因此需要在单元测试中手工构造route表:

def _registerRoutes(config):
    config.add_route('book_list', '/book/list')
    config.add_route('book_detail', '/book/detail/{id}')
    config.add_route('book_add', '/book/add')
    config.add_route('book_edit', '/book/edit/{id}')
    config.add_route('book_delete', '/book/delete/{id}')


七、edit,delete

edit、delete基本类似new与detail,值得注意的是,edit时尽可能只访问一次数据库、减轻数据库访问量。

八、完整程序


Sunday, April 22, 2012

Pyramid的tween

在Pyramid中,tween是一些运行在Pyramid router处理逻辑和WSGI服务器直接的程序,经常用于Pyramid框架扩展功能。tween比较类似于WSGI中间件,但它能访问Pyramid应用注册表。


一、tween factory

tween是一个接收request对象并且返回response对象的可调用程序。

要使用tween,必须首先构建tween factory。tween factory 必须是一个全局范围可调用的程序,它带两个参数:handler、registry。handler可以是一个Pyramid request处理程序或另外一个tween。registry则是Pyramid的应用注册表。tween factory必须返回一个tween。如:

import time
from pyramid.settings import asbool
import logging

log = logging.getLogger(__name__)

def timing_tween_factory(handler, registry):
    # 如果.ini配置文件中定义了do_timing = true,则启动计时
    if asbool(registry.settings.get('do_timing')):
        def timing_tween(request):
            #定义一个tween,接受request,返回response
            start = time.time()
            try:
                response = handler(request)  # 调用原始的处理程序,可能是Pyramid处理程序,或一个tween
            finally:
                end = time.time()
                log.debug('the request took %s seconds' % (end-start))
            return response
        return timing_tween    # 如果开启了do_timing,则返回tween
    return handler   # 如果没有开启do_timing,则返回原始的handler


二、注册一个tween factory

可以通过add_tween方法直接注册一个tween factory。如:

config = Configurator()
config.add_tween('myapp.tweens.timing_tween_factory')

注意这里只能是一个包结构定义的字符串,不能直接传一个对象。(还不清楚问什么要这么限定。)

tween也可以直接定义在ini文件中,如

pyramid.tweens = myapp..tweens.timing_tween_factory

如果在ini文件中定义了tweens,那么程序中的add_tween将被忽略。

如果在一个应用中注册了多个tween,他们将在程序启动时按照定义的顺序(如果没有特别指定的话)形成一个链式结构,类似于原来pylons的洋葱体结构。如定义了如下tween:

from pyramid.config import Configurator
config = Configurator()

config.add_tween('myapp.tween_factory1')
config.add_tween('myapp.tween_factory2')

将形成如下调用顺序

INGRESS (implicit)     WSGI入口
myapp.tween_factory2
myapp.tween_factory1
pyramid.tweens.excview_tween_factory (implicit)   Pyramid提供
MAIN (implicit)  应用

三、指定tween的位置

默认的,tween的顺序就是申明的顺序,不过开发者也可以通过特定参数来改变这个顺序。如:

config.add_tween('myapp.tween_factory', over=pyramid.tweens.MAIN)

add_tween 中可以通过under或over来特别指定tween的顺序。它们的值可以是:

* None:默认
* 一个tween factory的名字
* pyramid.tweens.MAIN, pyramid.tweens.INGRESS, or pyramid.tweens.EXCVIEW其中之一
* 以上的组合形式

其中under表示相比更靠近MAIN,over表示相比更靠近INGRESS。

如上例,将形成如下顺序:

INGRESS (implicit)
pyramid.tweens.excview_tween_factory (implicit)
myapp.tween_factory
MAIN (implicit)

再如:

config.add_tween(’myapp.tween_factory1’, over=pyramid.tweens.MAIN)
config.add_tween(’myapp.tween_factory2’, over=pyramid.tweens.MAIN, under=’myapp.tween_factory1’)

将形成如下顺序:

INGRESS (implicit)
pyramid.tweens.excview_tween_factory (implicit)
myapp.tween_factory1
myapp.tween_factory2
MAIN (implicit)

什么over、under都不定义,相当于使用了under=INGRESS。

在under、over中也可使用类似under=('someothertween', 'someothertween2', INGRESS)的方式,则系统只会检查存在的tween,如果tween不存在,则忽略。

开发者可以通过ptweens命令来查看tweens的顺序。

四、冲突与环形顺序

Pyramid不允许定义多次定义同名的tween factory。

Pyramid如果检测到over、under构成了一个环,则抛出异常。






Saturday, April 21, 2012

Pyramid的Hook机制


在Pyramid应用中,可以使用各种Hook 来影响影响框架的行为。

一、变更Not Found视图

当Pyramid找不到URL对应的视图时,将会调用not found 视图。Pyramid提供了一个默认的not fund视图,但开发者也可以自定义它。如下:

from hello world.views import notfound
config.add_notfound_view(notfound)

通过这种方式就可以将默认的not found 视图替换成自己所需的特殊视图了。

除了上面的指令方式指定之外,也可以用标注方式来指定not found 视图。如:

from pyramid.view import notfound_view_config

@notfound_view_config()
def notfound(request):
    return Response('Not Found!!', status='404 Not Found')

在Pyramid中,甚至可以定义多个not found视图,如:

from pyramid.view import notfound_view_config

@notfound_view_config(request_method='GET')
def notfound_get(request):
    return Response('Not Found during GET!!', status='404 Not Found')

@notfound_view_config(request_method='POST')
def notfound_post(request):
    return Response('Not Found during POST!!', status='404 Not Found')

not found 视图可以带request参数,或context、request参数。request就是调用当时的request,context就是HTTPNotFound异常的一个实例。

二、变更Forbidden视图

同样的,Forbidden视图也可视需要变更。如:

from hello world.views import forbidden_view
config.add_forbidden_view(forbidden_view)


from pyramid.view import forbidden_view_config

@forbidden_view_config()
def forbidden(request):
    return Response('forbidden')


三、变更Request Factory

在Pyramid处理WSGI传过来的请求时,它会基于WSGI环境变量创建一个request对象。系统系统的默认对象就是pyramid.request.Reqiest的一个实例,但开发者也可以通过程序来改变它。如:

from pyramid.request import Reqest

class MyRequest(Request):
    pass

config = Configurator(request_factory=MyRequest)


config = Configurator()
config.set_request_factory(MyRequest)


四、Response 回调

Pyramid运行在视图返回了一个response对象之后调用一个回调函数来变更response里的内容。如:

def cache_callback(request, response):
    if request.exception is not None:
        response.cache_control.max_age = 360

request.add_response_callback(cache_callback)

当视图返回非Response对象或抛出未处理异常时。不会调用response 回调。但当异常视图(这也是一个视图)处理返回后,会调用它,但此时request.exception就不是None,而是异常对象。因此上例判断了该值。

Response回调按添加顺序被调用,所有回调都在NewResponse事件发生后才会被调用。回调中抛出的异常将不会马上得到处理,而是需要传递到Pyramid route程序才能处理。

注意:Response回调的生命周期只是一次请求之内。如果需要每个请求都调用某个回调,则需要在每个请求中注册这个回调(如订阅NewRequest事件)。


五、Finished回调

Finished回调将在Pyramid router处理完一个请求之后被无条件调用。Finished回调带一个request参数,如:

def log_callback(request):
    log.debug('request finished.')

request.add_finished_callback(log_callback)

Finished回调按添加顺序被调用。即便是程序发生异常而无法产生response,也会调用Finished回调。回调中抛出的异常将不会马上得到处理,而是需要传递到Pyramid route程序才能处理。

注意:Finished回调的生命周期只是一次请求之内。如果需要每个请求都调用某个回调,则需要在每个请求中注册这个回调(如订阅NewRequest事件)。


六、改变Traverser程序

如果有特殊需要,Pyramid甚至允许自定义Traversal算法。如:

config = Configurator()
config.add_traverser(MyTraverser)

其中,MyTraverser必须实现如下的方法:

class MyTraverser(object):
    def __init__(self, root):
        “”“ root参数为root factory返回的root对象”“”
    def __call__(self, request):
        """ 返回一个至少包含root、context、view_name、subpath、traversed、virtual_root、virtual_root_path等值的字典"""

各参数的具体含义可参考traverser算法。

add_traverser方法还可以带一个root类参数,如config.add_traverser(MyTraverser, MyRoot)。这时如果root factory返回的root对象是MyRoot的实例,就使用MyTraverser算法,否则使用默认算法。


七、改变resource_url方法的URL生成方式

一旦改变了Traverser之后,经常也需要一同改变resource_url方法。如:

config.add_resource_url_adapter(MyResourceURLAdapter, Myroot)

因Traverser变更的可能性比较低,这里不展开细说。


八、修改Pyramid视图返回结果

视图一般情况下都是返回一个Response类或其子类的实例(未用renderer时),否则需要一个适配器来转换,如:

from Pyramid.response import Response

def string_response_adapter(s):
    response = Response(s)
    return response

config.add_response_adapter(string_response_adapter, str)

在上例中,如果视图返回str,将使用该适配器转换成Response。

类似情况在应用移植时可能会用到,一般不常见。


九、定制view mapper

如果不希望使用Pyramid视图结构,也可以通过定制view mapper来改变它,具体参见akhet项目,它将Pyramid改造成了原来Pylons的结构。


十、定制配置标注

Pyramid允许自己定制类似view_config的标注,以便只有经过scan才使其生效,详细细节参见Venusian文档。





Friday, April 20, 2012

在Pyramid中做unit testing


Pyramid支持Python unittest模块。Pyramid为单元测试、集成测试、功能测试提供了一系列便利功能。

一、setUp与tearDown

Pyramid为了支持get_current_reqeust、get_current_registry等功能,在框架中使用了thread local 机制,因此在单元测试中,推荐在setUp方法中调用pyramid.testing.setUp,在tearDown调用pyramid.testing.tearDown以进行thread local的设置和清除。如果在应用中使用了get_current_request功能,还需要在setUp中创建一个request对象。如:

import unittest
from pyramid import testing

class MyTest(unittest.TestCase):
    def setUp(self):
        request = testing.DummyRequest()
        self.config = testing.setUp(request=request)

    def tearDown(self): 
        testing.tearDown()

通过这样配置之后,在单元测试中直接引用get_current_request即可返回一个request对象,而非None。

二、利用Pyramid提供的API编写单元测试

在Pyramid Configurator API和pyramid.testing模块中为单元测试提供了许多便利功能。如在应用注册表中注入一些测试桩,而非实际的功能。

下面我们用一个带权限的视图的例子来简单说明其用法。



上例中,定义了一个视图,该视图中的程序首先判断是否具有权限,如果没有权限,则抛出HTTPForbidden异常。

现在我们构造一个简单的单元测试,如:

        from my.package.views import view_fn
        request = testing.DummyRequest()
        info = view_fn(request)

运行之后我们发现,这程序始终没法模拟没有权限的过程。因为我们在__init__.py中的main中配置了权限策略,但在单元测试中,我们没有做任何配置。因此Pyramid提供了这方面的一些桩功能来专门为单元测试服务。如下面的程序:



在这个例子中,test_view_fn_forbidden单元测试直接指定了当前用户无权限的策略,从而引发HTTPForbidden异常。在这种情况下,在视图中用pyramid.security.authenticated_userid(request)得到当前用户名即为hank。

在上例中,我们在setUp中通过testing.setUp()构建了一个config,它是一个Configurator对象,可以在测试程序中往这个对象中设置需要的测试桩。Pyramid在Configurator对象上提供了如下几种测试桩:

testing_add_renderer(path, renderer=None)  注册渲染器

testing_add_subscripber(event_iface=None) 注册订阅者

testing_resource(resource)  提供一个资源字典供pyramid.traversal.find_resource使用。

testing_securitypolicy(usesrid=None, groupids=(), permissive=True) 提供权限控制策略

同时在pyramid.testing模块里面提供了:

DummyResource(__name__=None, __parent__=None, __provides__=None, **kw)

DummyRequest(params=None, environ=None, headers=None, path=’/’, cookies=None, post=None, **kw)

DummyTemplateRenderer(string_response='')


三、如何写集成测试用例

在Pyramid中,单元测试基本上都是基于mock或dummy来完成测试的。而集成测试更多的是针对程序段及其需要的Pyramid架构支持整合在一起的测试。

我们经常用includeme方法来进行集成测试,在这种方式下,运行测试用例时会通过setUp建立整个Pyramid环境,并在tearDown时清除。

下面我们再用个例子说明。假设我们有一个应用,名字为myapp,其中有个叫my_view的视图,其输出‘Welcome to this application’。那么它的集成测试用例如下:



除非必要,建议更多采用Configurator API和注册mock对象的方法来做单元测试,单元测试的速度远快于集成测试。


四、如何写功能测试

功能测试主要是为了测试应用是否完成了其应该的功能。如下例:



在这个例子中,我们用了webtest模块来直接启动app,从而发起类似实际的request请求,并查看其结果。

Pyramid官方文档里面单元测试部分的内容还是写的比较简单的,感觉很多东西不够深入,以后有机会多写点实际的例子看看。