Showing posts with label pyramid. Show all posts
Showing posts with label pyramid. Show all posts

Thursday, November 28, 2013

mako二三事 -- 与Babel的结合

为了支持国际化,在python程序中,经常需要使用Babel将语言相关的字符串抽取出来,形成pot文件。而在mako模板文件中,有大量的文本性内容需要国际化处理,因此,mako必须要支持Babel的抽取功能。

一、mako字符串的简单抽取

在mako中,使用了一个名为mako.ext.babelplugin的Babel抽取插件,正确安装Babel、mako后,就可以使用pybabel命令进行字符串抽取。

首先,我们创建一个a.html文件,内容为:


  Name:
  ${_('Francois Pinard')}



再创建一个babel.cfg文件,作为Babel 的配置文件:

[mako: **.html]
input_encoding = utf-8 

然后运行命令

pybabel extract -F babel.cfg .

即可在console输出:

extracting messages from a.html (input_encoding="utf-8")
# Translations template for PROJECT.
# Copyright (C) 2013 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR , 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2013-11-26 16:01+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"

#: a.html:3
msgid "Francois Pinard"
msgstr ""  

这个就是我们通常使用的pot文件,有了pot文件,我们就可以很方便的按照国际化命令生成po文件,mo文件。

在a.html中,我们可以看到的一个明显的变化是:Francois Pinard这段文本被改写成了${_('Francois Pinard’)},mako的Babel插件能够识别这种格式并抽取到pot文件中,而普通的文本name:则不会。

Babel进行抽取的时候,需要指定一个配置文件,这里我们做了一个简单的配置文件babel.cfg。在该文件中,**.html表示本目录以及子目录所有html文件,前面的mako表示这些文件均有mako的Babel插件来解析。input_encoding为模板的编码格式,这个跟模板载入时用的encoding是一致的。

二、对抽取字符串做注释

有时候,为了能够清楚分辨抽取字符串,可以对这些字符串做一个注释。

首先,我们修改a.html为:


  Name:
  ## TRANSLATORS: This is a proper name. See the gettext
  ## manual, section Names.
  ${_('Francois Pinard')}


这里要注意的是,这个注释必须直接放在抽取字符串之前,中间不能有其他多余字符串。

然后,运行

pybabel extract -F babel.cfg -c "TRANSLATORS:” . 

可以得到: 

#. TRANSLATORS: This is a proper name. See the gettext
#. manual, section Names.
#: a.html:5
msgid "Francois Pinard"
msgstr "" 

从上面结果可以看到,在pot文件中,将html文件里面对这个字符串的注释也抽取到了该文件中。


三、pyramid项目的支持

通常的,我们不会简单的对一个文件进行国际化,而是对一个使用了特定framework的项目进行国际化。比如使用了pyramid框架,通过pcreate生成的项目中,会有setup.py和setup.cfg两个文件,为了支持Babel抽取,需要在setup.py中加入mako配置(setup中加两个参数):

      package_data={'mypyramid': ['locale/*/LC_MESSAGES/*.mo']},
      message_extractors = { 'mypyramid': [
          ('**.py',   'lingua_python', None ),
          ('templates/**.pt',   'lingua_xml', None ),
          ('templates/**.html', 'mako', None),
          ('static/**', 'ignore', None),
          ]}, 

然后在mypyramid目录下建立locale文件夹即可。这里的'templates/**.html', 'mako', None含义跟上面的配置文件含义一致。

另外,在setup.cfg中,有国际化字符串抽取、编译、更新的系列配置,如果有需要,可以调整这些参数。
[compile_catalog]
directory = mypyramid/locale
domain = MyPyramid
statistics = true

[extract_messages]
add_comments = TRANSLATORS:
output_file = mypyramid/locale/MyPyramid.pot
width = 80

[init_catalog]
domain = MyPyramid
input_file = mypyramid/locale/MyPyramid.pot
output_dir = mypyramid/locale

[update_catalog]
domain = MyPyramid
input_file = mypyramid/locale/MyPyramid.pot
output_dir = mypyramid/locale
previous = true 

在pyramid项目中,为了支持国际化,还需要从python程序中将_这个函数压入到context中,如下:

tsf = TranslationStringFactory('MyPyramid’)
 
@subscriber(BeforeRender)
def add_renderer_globals(event):
    def auto_translate(string, domain=None, mapping=None):
        return localizer.translate(tsf(string), domain=domain, mapping=mapping) 
    request = event.get('request')
    if request:
        event['_'] = auto_translate

Monday, November 18, 2013

使用Pyramid和Cornice构建多格式支持的RESTful API

在Pyramid中利用Cornice可以很方便的基于HTTP Accept header不同构建不同格式的响应内容。

1. 安装Cornice
    pip install Cornice

2. 安装Cornice之后,会在系统中增加一个名为cornice的template,在学习时可以使用它创建一个简单的项目框架。
    pcreate -t cornice myapp
    创建好的项目相比pyramid的项目简单,只有一个__init__.py,一个views.py

3. 在views.py中定义了一个简单的api例子
hello = Service(name='hello', path='/', description="Simplest app")


@hello.get()
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World’} 

在该例子中,定义了一个url为/的api,访问这个api链接,将返回一个application/json响应。

4. 在__init__.py中使用include包含了cornice包
from pyramid.config import Configurator


def main(global_config, **settings):
    config = Configurator(settings=settings)
    config.include("cornice")
    config.scan("myapp.views")
    return config.make_wsgi_app() 

5. 运行该程序,查看结果
python setup.py develop 
pserve --reload myapp.ini 
我们通过curl访问该url:curl -D - http://0.0.0.0:6543,得到如下响应内容
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Mon, 18 Nov 2013 06:46:49 GMT
Server: waitress

{"Hello": "World"} 
从Content-Type中可以看到,取得的内容为application/json格式

6. 构建一个文本格式返回结果
为了取得文本结果,我们需要将view的结果进行格式化,因此在__init__.py中加入如下renderer
class TextRenderer(object):

    def __init__(self, info):
        pass

    def __call__(self, value, system):
        request = system.get('request')
        if request is not None:
            response = request.response
            response.content_type = 'text/plain'
        name = value['Hello']
        return u"Hello, {}!".format(name) 

注:如果不需要进行类似上面的数据转化,可以直接使用string renderer。

然后,在main函数中加入config.add_renderer('text', TextRenderer)

7. 修改views.py 以接收不同的HTTP Header
@hello.get(accept='text/plain', renderer='text')
@hello.get(accept='application/json', renderer='json')
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'} 

上面的两个@分别表示当get中的HTTP Header不同时的处理(使用不同的renderer对数据进行不同渲染)

8. 使用不同的HTTP Header查看结果
$ curl -D - -H 'Accept: text/plain' http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:41 GMT
Server: waitress

Hello, World!


$ curl -D - -H 'Accept: application/json' http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:48 GMT
Server: waitress

{"Hello": "World"} 


9. cornice与参数校验的整合
cornice可以很好的跟colander校验库结合使用。下面我们加入一个输入参数尝试一下。

9.1 安装colander
pip install colander

9.2 在views.py中增加一个校验规则
import colander

class NameSchema(colander.MappingSchema):
    name = colander.SchemaNode(colander.String(),
                               validator=colander.Length(2)) 

该校验规则表示系统接收一个名为name的参数,且该参数为字符串类型,最小长度为2(Length参数为min,max,这里省略了max)

9.3 在views.py中增加参数处理方法
@hello.post(accept='text/plain', renderer='text', schema=NameSchema)
@hello.post(accept='application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name} 

上面的方法为/这个url增加了post处理。

9.4 取得运行结果
curl -H 'Accept: application/json' http://0.0.0.0:6543 -d '{"name": "Alex"}' 
结果:{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}
这是因为cornice默认接收的Content-Type是application/x-www-form-urlencoded,因此不能解析-d '{"name": "Alex"}' 这种参数

$ curl -H 'Accept: application/json' http://0.0.0.0:6543 -d 'name=Alex'
{"Hello": "Alex"} 

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' http://0.0.0.0:6543 -d '{"name": "Alex"}'
{"Hello": "Alex"} 
该例子中指定了Content-Type,因此系统能识别-d '{"name": "Alex"}'

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' http://0.0.0.0:6543 -d '{"name": "A"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "Shorter than minimum length 2"}]} 


本文是http://makina-corpus.com/blog/metier/multi-format-restful-api-with-cornice的一个摘要翻译,需要查看原文请自行前往。 

Tuesday, October 15, 2013

使用PyCharm搭配Vagrant开发Pyramid程序


PyCharm是JetBrains出品的一个功能强大的Python IDE,PyCharm Pro 3中对Vagrant和Pyramid都提供了良好的支持,可以通过他们之间的搭配构建出一个干干净净的开发环境。

1. 软件安装
安装Virtual Box https://www.virtualbox.org/
安装Vagrant http://www.vagrantup.com/

确保PyCharm中的Vagrant插件已经激活。



2. 安装一个virtual box
在PyCharm Setting中,找到Vagrant项,点击左下角的+按钮,增加一个box。

下载完成后,Vagrant Boxes列表中即多一个box项。

3. 创建Vagrantfile
创建、打开Pyramid项目,在PyCharm中选择tools菜单 -> Vagrant -> Init in Project Root,即可在当前项目根目录创建一个Vagrantfile,打开该文件,加入:
  config.vm.network :forwarded_port, guest: 6543, host: 6543
因Pyramid一般使用6543这个端口,如果项目端口选择不一样,可以自行配置。
如果配置了多个box,在init的时候,会弹出box选择项。

4. 配置虚拟机 
运行Vagrant up启动虚拟机,登录到该虚拟机。
运行sudo apt-get install python-virtualenv 安装虚拟环境程序
运行virtualenv env创建一个虚拟环境
运行source env/bin/activate启动该虚拟环境
运行pip install pyramid==1.4.3 安装pyramid

根据项目不同,本步骤安装程序也可以不一样,这里只是演示一下在虚拟机里面也可以使用python虚拟环境。一般团队开发可以定制自己的专有box以便整个团队开发环境的一致。

5. 配置remote interpreter
在Settings中,找到Project Interpreter项


点击Configure Interpreters


选择Remote…

选择Fill from vagrant config




因上面使用了python虚拟环境,因此,上面的Python interpreter path需要变更为:
/home/vagrant/env/bin/python

到现在为止,就可以运行普通的python程序进行项目开发了。不过因为要配置Pyramid,还需要变更一个配置。

6. Pyramid运行配置变更
运行tools菜单中的Run setup.py Task,选择develop,将在虚拟机中运行python setup.py develop

打开项目的Run/Debug Configurations,变更Config file为如下类似内容
/home/vagrant/env/bin/pserve /Users/XXXX/Documents/PycharmProjects/test1/development.ini 
(即在development文件前加上pserve,远程interpreter默认启动的是python,而不会像本地开发一样启动pserve)

7. 运行程序
现在在Pycharm中运行/调试程序,就可以看到其实程序是在虚拟机中运行,而非本机。




并且仍然可以在本机浏览器使用http://0.0.0.0:6543查看运行结果。

将PyCharm跟Vagrant结合之后,就可以将所有项目需要的各种包、软件都丢到虚拟机中,甚至可以每个项目配置一个box。于此同时,仍然可以保持PyCharm各种开发调试习惯不变。


Monday, April 22, 2013

Simple-TODO Pyramid实现版

看到@python4cn 网站上有个专栏列了各个python框架的SimpleTODO实现,唯独没有Pyramid,就随手写了一个,略做补充。 具体的项目地址在 https://github.com/eryxlee/pyramid_koans/tree/master/Simple-TODO
import os
import datetime
import transaction

from pyramid.config import Configurator
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPFound

from sqlalchemy import engine_from_config

from sqlalchemy import (
    Column,
    Integer,
    DateTime,
    Unicode,
    )

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 Todo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True, autoincrement=True)
    title = Column(Unicode(255), default=u'')
    post_date = Column(DateTime, default=datetime.datetime.now)
    finished = Column(Integer, default=0)

    def __init__(self, title, post_date, finished):
        self.title = title
        self.post_date = post_date
        self.finished = finished


@view_config(route_name='index',  renderer='/index.mako')
def index_view(request):
    todos = DBSession.query(Todo).order_by(Todo.post_date.desc()).all()
    return {'todos': todos}


@view_config(route_name='add', request_method='POST')
def add_post_view(request):
    title = request.params.get('title', '')
    with transaction.manager:
        todo = Todo(title=title, post_date=datetime.datetime.now(), finished=0)
        DBSession.add(todo)

    raise HTTPFound(location = request.route_url('index'))


@view_config(route_name='edit', request_method='GET', renderer='/edit.mako')
def edit_view(request):
    id = int(request.matchdict.get('id'))
    todo = DBSession.query(Todo).filter(Todo.id==id).first()

    return {'todo': todo}


@view_config(route_name='edit', request_method='POST')
def edit_post_view(request):
    id = int(request.matchdict.get('id'))
    title = request.params.get('title')
    with transaction.manager:
        DBSession.query(Todo).filter(Todo.id==id).update({Todo.title:title})

    raise HTTPFound(location = request.route_url('index'))


@view_config(route_name='finish')
def finish_view(request):
    id = int(request.matchdict.get('id'))
    status = request.params.get('status', 'yes')
    finished = {'yes':1, 'no':0}.get(status)
    if finished != None:
        with transaction.manager:
            DBSession.query(Todo).filter(Todo.id==id).update({Todo.finished:finished})

    raise HTTPFound(location = request.route_url('index'))


@view_config(route_name='delete')
def delete_view(request):
    id = int(request.matchdict.get('id'))
    with transaction.manager:
        DBSession.query(Todo).filter(Todo.id==id).delete()

    raise HTTPFound(location = request.route_url('index'))


def my_static_path(self, path, **kw):
    if not os.path.isabs(path):
        if not ':' in path:
            path = '%s:%s/%s' % ("simpletodo", 'static', path)
    kw['_app_url'] = self.script_name

    return self.static_url(path, **kw)


def main(global_config, **settings):
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    Base.metadata.bind = engine
    config = Configurator(settings=settings)
    config.add_request_method(my_static_path, 'static_path')
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('index', '/')
    config.add_route('add', '/todo/new')
    config.add_route('edit', '/todo/{id:\d+}/edit')
    config.add_route('finish', '/todo/{id:\d+}/finish')
    config.add_route('delete', '/todo/{id:\d+}/delete')
    config.scan()

    return config.make_wsgi_app()

Thursday, March 28, 2013

Pyramid Route方式中减少一点add_route的方法

用了Pyramid Route方式之后,经常会面对一大堆add_route的定义,灵活利用Pyramid提供的一些便利技巧,可以大大减少这些route的定义。下面介绍一个简单的技巧:


@view_defaults(route_name='myroute' )
class MyController(object):
    def __init__(self, request):
        self.request = request
        print 'do something before every action.'

    @view_config(match_param=('ctrl=my', 'action=action1'))
    def action1(self):
        print self.request.matchdict['ctrl'], self.request.matchdict['action'], self.request.matchdict['pa']
        return Response('in my controller action 1')

    @view_config(match_param=('ctrl=my', 'action=action2'))
    def action2(self):
        print self.request.matchdict['ctrl'], self.request.matchdict['action'], self.request.matchdict['pa']
        return Response('in my controller action 2')

    @view_config(match_param=('ctrl=my', 'action=action3'), custom_predicates=(lambda context, request: request.matchdict['pa'][0]=='3',))
    def action3(self):
        print self.request.matchdict['ctrl'], self.request.matchdict['action'], self.request.matchdict['pa']
        return Response('in my controller action 3')

@view_defaults(route_name='myroute' )
class MyController2(object):
    def __init__(self, request):
        self.request = request
        print 'do something before every action.'

    @view_config(match_param=('ctrl=you','action=action1'))
    def action1(self):
        print self.request.matchdict['ctrl'], self.request.matchdict['action'], self.request.matchdict['pa']
        return Response('in you controller action 1')

    @view_config(match_param=('ctrl=you', 'action=action2'))
    def action2(self):
        print self.request.matchdict['ctrl'], self.request.matchdict['action'], self.request.matchdict['pa']
        return Response('in you controller action 2')

    @view_config(match_param=('ctrl=you', 'action=action3'), custom_predicates=(lambda context, request: request.matchdict['pa'][0]=='3',))
    def action3(self):
        print self.request.matchdict['ctrl'], self.request.matchdict['action'], self.request.matchdict['pa']
        return Response('in you controller action 3')

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('myroute', '/{ctrl}/{action}*pa')
    config.add_route('home', '/')
    config.scan()
    return config.make_wsgi_app()

Monday, July 23, 2012

Pyramid 中SQLAlchemy 对象的JSON序列化


pyramid github中master 版本增加了custom objects的JSON支持,而在之前(1.3及以前)的版本中SQLAlchemy model对象的序列化需要编写JSONEncoder的子类,然后在dumps的时候指定。因此,想在程序中直接使用json这个renderer输出view结果是一件麻烦的事。

为了在pyramid程序中增加JSON支持,只需要在model类里面增加__json__方法即可。如

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()
def sqlalchemy_json(self, request):
    obj_dict = self.__dict__
    return dict((key, obj_dict[key]) for key in obj_dict if not key.startswith("_"))
Base.__json__ = sqlalchemy_json

之后继承自Base的model类即可直接json renderer中输出了。(本例中暂没测试relation)如

@view_config(route_name='home', renderer='json')
def home(request):
    one = DBSession.query(MyModel).filter(MyModel.name=='one').first()
    return {'one':one, 'project':'MyProject'}

为了支持更多第三方类的序列化,pyramid还提供了adapter的功能,如在SQLAlchemy中常用datetime数据类型,这个数据类型在序列化时也会报错,则需要增加一个adapter,如:

def datetime_adapter(obj, request):
    return obj.strftime('%Y-%m-%d %H:%M:%S')

custom_json_renderer_factory.add_adapter(datetime.datetime, datetime_adapter)

并调用config.add_renderer将 custom_json_renderer_factory注册即可。

随便附上单独提取出来的代码,直接放入项目即可在1.3版本的pyramid上使用。


import json
import datetime

from zope.interface import providedBy, Interface
from zope.interface.registry import Components

class IJSONAdapter(Interface):
    """
    Marker interface for objects that can convert an arbitrary object
    into a JSON-serializable primitive.
    """
_marker = object()

class JSON(object):
    """ Renderer that returns a JSON-encoded string.

    Configure a custom JSON renderer using the
    :meth:`~pyramid.config.Configurator.add_renderer` API at application
    startup time:

    .. code-block:: python

       from pyramid.config import Configurator

       config = Configurator()
       config.add_renderer('myjson', JSON(indent=4))

    Once this renderer is registered as above, you can use
    ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or
    :meth:`~pyramid.config.Configurator.add_view``:

    .. code-block:: python

       from pyramid.view import view_config

       @view_config(renderer='myjson')
       def myview(request):
           return {'greeting':'Hello world'}

    Custom objects can be serialized using the renderer by either
    implementing the ``__json__`` magic method, or by registering
    adapters with the renderer.  See
    :ref:`json_serializing_custom_objects` for more information.

    The default serializer uses ``json.JSONEncoder``. A different
    serializer can be specified via the ``serializer`` argument.
    Custom serializers should accept the object, a callback
    ``default``, and any extra ``kw`` keyword argments passed during
    renderer construction.

    .. note::

       This feature is new in Pyramid 1.4. Prior to 1.4 there was
       no public API for supplying options to the underlying
       serializer without defining a custom renderer.
    """

    def __init__(self, serializer=json.dumps, adapters=(), **kw):
        """ Any keyword arguments will be passed to the ``serializer``
        function."""
        self.serializer = serializer
        self.kw = kw
        self.components = Components()
        for type, adapter in adapters:
            self.add_adapter(type, adapter)

    def add_adapter(self, type_or_iface, adapter):
        """ When an object of the type (or interface) ``type_or_iface`` fails
        to automatically encode using the serializer, the renderer will use
        the adapter ``adapter`` to convert it into a JSON-serializable
        object.  The adapter must accept two arguments: the object and the
        currently active request.

        .. code-block:: python

           class Foo(object):
               x = 5

           def foo_adapter(obj, request):
               return obj.x

           renderer = JSON(indent=4)
           renderer.add_adapter(Foo, foo_adapter)

        When you've done this, the JSON renderer will be able to serialize
        instances of the ``Foo`` class when they're encountered in your view
        results."""

        self.components.registerAdapter(adapter, (type_or_iface,),
            IJSONAdapter)

    def __call__(self, info):
        """ Returns a plain JSON-encoded string with content-type
        ``application/json``. The content-type may be overridden by
        setting ``request.response.content_type``."""
        def _render(value, system):
            request = system.get('request')
            if request is not None:
                response = request.response
                ct = response.content_type
                if ct == response.default_content_type:
                    response.content_type = 'application/json'
            default = self._make_default(request)
            return self.serializer(value, default=default, **self.kw)

        return _render

    def _make_default(self, request):
        def default(obj):
            if hasattr(obj, '__json__'):
                return obj.__json__(request)
            obj_iface = providedBy(obj)
            adapters = self.components.adapters
            result = adapters.lookup((obj_iface,), IJSONAdapter,
                default=_marker)
            if result is _marker:
                raise TypeError('%r is not JSON serializable' % (obj,))
            return result(obj, request)
        return default

custom_json_renderer_factory = JSON()

def datetime_adapter(obj, request):
    return obj.strftime('%Y-%m-%d %H:%M:%S')

def date_adapter(obj, request):
    return obj.strftime('%Y-%m-%d')

custom_json_renderer_factory.add_adapter(datetime.datetime, datetime_adapter)
custom_json_renderer_factory.add_adapter(datetime.date, date_adapter)

Saturday, May 5, 2012

Pyramid 与 Beaker


Beaker为Python程序提供了良好的缓存和session支持。在Pyramid中,也有一个叫pyramid_beaker的小程序提供了Beaker接入支持,只需要在Pyramid应用中include pyramid_beaker即可在程序中使用Beaker。

pyramid_beaker主要提供了一个session factory,可以读取ini配置文件中的配置信息提供session factory服务。同时,pyramid_beaker也能够将配置的cache信息读取到beaker.cache.cache_regions中,以便在程序中使用@cache_region标注。不过这个插件对cache的支持不是很给力,个人感觉应该直接将给request对象植入一个CacheManager或Cache比较好,这样在应用中就可以不必关注是否使用Beaker了。

一、启用Beaker

将development.ini中的pyramid.includes中增加一项pyramid_beaker即可在应用中启用Beaker支持

pyramid.includes =
    pyramid_debugtoolbar
    pyramid_tm
    pyramid_beaker


三、beaker session

1. session的配置

在development.ini中加入如下配置即可在程序中使用Beaker Session。

session.type = file
session.data_dir = %(here)s/data/sessions/data
session.lock_dir = %(here)s/data/sessions/lock
session.key = mykey
session.secret = mysecret
session.cookie_on_exception = true

其中session.type支持cookie, file, dbm, memory, ext:memcached, ext:database, ext:google这几种类型。(注意:cookie有大小限制)
如果使用了ext:memcached, ext:database这两种类型,还会增加一个session.url的配置,如:

beaker.session.key = sso
beaker.session.secret = somesecret
beaker.session.type = ext:memcached
beaker.session.url = 10.72.249.39:11211;10.90.133.122:11211;10.242.117.122:11211
beaker.session.timeout = 10800


2. session的使用

Pyramid通过session factory封装了session的使用,因此配置了pyramid_beaker之后,引用request.session就是在使用Beaker提供的session支持。


四、beaker cache

1. cache的配置

beaker.cache.regions = default_term, second, short_term, long_term
beaker.cache.type = memory
beaker.cache.second.expire = 1
beaker.cache.short_term.expire = 60
beaker.cache.default_term.expire = 300
beaker.cache.long_term.expire = 3600

其中session.type支持file, dbm, memory, ext:memcached, ext:database, ext:google这几种类型。

cache的配置引入了一个region的概念,可以支持多种类、不同层次的cache支持。如:

beaker.cache.data_dir = %(here)s/data/cache/data
beaker.cache.lock_dir = %(here)s/data/cache/lock
beaker.cache.regions = short_term, long_term
beaker.cache.short_term.type = ext:memcached
beaker.cache.short_term.url' = 127.0.0.1.11211
beaker.cache.short_term.expire = 3600
beaker.cache.long_term.type = file
beaker.cache.long_term.expire = 86400


2. cache的使用

在ini文件配置好之后,在程序中,就可以使用@cache_region这个标注来使用cache功能了。如:

from beaker.cache import cache_region

@cache_region('long_term')
def get_photos():
    pass

注意,不要直接在视图上加这个标注,最好将数据存取的地方抽取成函数,如果一定要在视图上做,view_config本身可以提供简单的自带cache功能。

    def view_callable(request):

        @cache_region('long_term')
        def func_to_cache():
            ...
            return something
        return func_to_cache()


3. cache的另类使用

标注虽然很简单,不过有的时候可能会懒得将数据存取独立成函数,这时候,也可以直接使用Beaker的功能直接往cache中put、get数据,如:

import time
from beaker.cache import cache_regions, CacheManager

cm = CacheManager(cache_regions=cache_regions)
cache = cm.get_cache_region("mypyramid", "short_term")

try:
    atime = cache.get("atime")
except Exception, exp:
    atime = str(time.time())
    cache.put("atime", atime, expiretime=60)


注意,这里的put可以单独指定一个expiretime,这会直接覆盖region中定义的过期时间。

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进行访问了。





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

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