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()

Thursday, September 6, 2012

FormEncode在SAE上的一点小改动


虽然FormEncode现在貌似用的人不多了,不过它的validate在数据校验、转换上还是很犀利的。如果用的好的话,还是可以大大减轻冗杂的代码量的。不过因为其中部分代码涉及到了一些IO操作,而SAE显然在这方面做了很多改动。因此要在SAE上使用它,就需要对它做点小小改动。主要变更代码如下:
api.py文件中有一个get_localedir函数,在系统载入时就会运行。该函数调用了resource_filename,而这个方法会去查找pwd模块(该模块SAE取消掉了)。
def get_localedir():
    """
    Retrieve the location of locales.

    If we're built as an egg, we need to find the resource within the egg.
    Otherwise, we need to look for the locales on the filesystem or in the
    system message catalog.
    """
    locale_dir = ''
    # Check the egg first
    if resource_filename is not None:
        return os.path.join(os.path.dirname(__file__), 'i18n')
        #try:
        #    locale_dir = resource_filename(__name__, "/i18n")
        #except NotImplementedError:
        #   # resource_filename doesn't work with non-egg zip files
        #    pass
    if not hasattr(os, 'access'):
        # This happens on Google App Engine
        return os.path.join(os.path.dirname(__file__), 'i18n')
    if os.access(locale_dir, os.R_OK | os.X_OK):
        # If the resource is present in the egg, use it
        return locale_dir

    # Otherwise, search the filesystem
    locale_dir = os.path.join(os.path.dirname(__file__), 'i18n')
    if not os.access(locale_dir, os.R_OK | os.X_OK):
        # Fallback on the system catalog
        locale_dir = os.path.normpath('/usr/share/locale')

    return locale_dir

大家注意一下就可以发现,这段代码里面还有个Google App Engine的代码实现。不过现在还没想到有什么好的方法来判断是否运行于SAE之上,因此,最简单的方式就是直接在行数开始处return os.path.join(os.path.dirname(__file__), 'i18n'),先当作SAE上的权宜之计,以后想到了合适的判断方式再来变更吧。

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)

Thursday, July 5, 2012

python setuptools test command 的一点小问题


为了测试一点小程序,用pcreate -t starter sampleutils 命令生成了一个项目框架。然后在sampleutils package中建了一个tests package,然后在这个package中放置了一个test_basic.py,程序如下:

import unittest

from pyramid import testing

class View2Tests(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()

    def tearDown(self):
        testing.tearDown()

    def test_my_view(self):
        pass

很简单,里面仅有一个testcase。现在就可以用python setup.py test来有运行单元测试了。

运行结果如下



很奇怪吧,竟然运行了两次同一个testcase。

再换nosetests运行看看:


这里明确显示是一个testcase。究竟哪里发生了问题呢?

再来看一下项目结构:


很快,我们就可以发现,pcreate生成的tests.py文件我们并没有删掉,而且这个module的名字跟tests package的名字重复了。这就是造成这次问题的原因。

但一般的,就算多了这个文件,也应该仅仅是忽略它啊,就像nosetests运行结果一样,不会去tests.py中查找任何test case。

为什么在python setup.py test 命令中会出现载入两次test_basic.py中的test case呢?

我们再来看python setup.py test这个命令的运行机制,该命令是setuptools中的一个命令之一,它通过读取setup.py文件中的配置信息,在指定目录中查找所有的testcase,然后运行。

找到setuptools的源码,打开command中的test.py程序,这个程序定义了setuptools中test command运行流程。



上面这段程序就是test case的查找过程。

从 if file.endswith('.py') and file!='__init__.py' 这一段就可以看出,它将tests.py 跟tests 混为一谈了,并且没有检查该模块是否已经载入,从而导致了在扫描tests的时候,载入了一次sampleutils.tests包,在扫描tests.py的时候,再次载入了sampleutils.tests这个包。这样也就很好的解释了在运行python setup.py test 命令的时候运行了两次相同的test case.

虽然是一个错误造成的问题,但也可以看出 setuptools中的代码还是值得推敲的。


Thursday, June 21, 2012

Mac OS Launchpad 上的重复图标


刚刚更新了一下App Store,发现Launchpad上出现了两个重复的图标iMovie,点击启动之后均打开同一个程序,版本什么的都一样,删也删不掉,好讨厌。在一番google之后,终于看到有人提出简单的修改方法,记录如下:

1. 打开终端
2. cd ~/Library/Application\ Support/Dock/
3. 这里面有一个db文件
4. 做个备份
5. 用sqlite3打开它
6. 查找其中的apps表,select * from apps where title='iMovie';
7. 发现果然有重复的,删掉id大的条目
8. killall Dock
9. 正常了

貌似还有个叫launchpad-control的工具可以完成Launchpad的管理,通过http://chaosspace.de/launchpad-control/下载(墙外)。

环境:Mac OS X 10.7.4

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环境的变化可能会导致本例不能正常运行。