понедельник, 29 августа 2011 г.

Bottle application helper

Next is the helper class for creation of the Bottle Web Framework applications:
def expose(path, **kw):
    def decor(view):
        view._route_path_ = path
        view._kwargs_ = kw
        return view
    return decor

class BottleBaseApp(object):
    """
    - Autosave attributes with names from __session__ attribute
    - Session type is attr __session_type__ ('cookie'|'pickle'),
      'cookie' is default
    - Methods decorated with @expose will be auto-routed and
      sessions' attrs will be flushed at the end
    """
    def __init__(self, bottleapp):
        self.bottle = bottleapp
        # Prepare session manager as attr sessions
        if self.__session_type__ == "pickle":
            self.sessions = PickleSession()
        else:
            self.sessions = CookieSession()

        def router(meth):
            """Return wrapped view-method"""
            # meth is already bound to self
            def wrp(f=meth, *a, **kw):
                try:
                    ret = f(*a, **kw) # bound, no needs self,
                finally:
                    self.flush_session()
                return ret
            functools.update_wrapper(wrp, meth)
            return wrp

        # Route all decorated methods
        for attrname in dir(self):
            attr = getattr(self, attrname)
            path = getattr(attr, "_route_path_", None)
            kw = getattr(attr, "_kwargs_", {})
            if path:
                self.bottle.route(path, callback=router(attr), **kw)

    def flush_session(self):
        """Copy to session all attrs and save session"""
        try:
            ses = self.sessions.get_session()
            for a in self.__session__:
                try:
                    v = getattr(self, a)
                    ses[a] = v
                except:
                    continue
            self.sessions.save(ses)
        except Exception as x:
            pass
And now is the example how to use this:
class MyApp(BottleBaseApp):
    __session_type__ = "cookie"
    __session__ = ["tasks_ctl_res", "tasks_prc", "prc_conns",]

    def __init__(self, bottleapp):
        super(MyApp, self).__init__(bottleapp)
        self.tasks_ctl_res = {}
        self.tasks_prc = []
        self.prc_conns = {}


    @expose("/index")
    def index(self):
        return "Index page is empty"

    # ... etc ...
I use Session implementation by Sean Reifschneider - MANY THANKS! - (see this).

воскресенье, 21 августа 2011 г.

Internationalization in the Bottle

This approach is very limited! In serious sites you have to support Web-robots, links with language code... I use it for small (embedded Web console) projects only.

I18N in the Bottle Python framework is easy. I prefer to do i18n for 2 different tasks:
  1. i18n for messages (strings)
  2. i18n for static text big pies (in the templates)
I use gettext mechanism. In my application folder I have folders:
locale/
    en/
      TEMPLATES/
        xxx.tpl
    bg/
      LC_MESSAGES/
        messages.mo
      TEMPLATES/
        xxx.tpl
    xx/
      LC_MESSAGES/
        messages.mo
      TEMPLATES/
        xxx.tpl
bg_messages.po
xx_messages.po

"locale" is the folder for i18n/i10n. Folders with language codes ("bg", "xx") are placed here. Each of them contents two subfolders: LC_MESSAGES with compiled .po file (messages.mo) and TEMPLAES (with .tpl files), except en/ folder: no LC_MESSAGES here.

I use make-like mechanism for compilation .po-files, adding new locale - Python fabricate build tool. It's very lightweight and is written in single Python file!

.po-files are very simple. I changed next strings in the one:
"Content-Type: text/plain; charset=utf8\n"
"Content-Transfer-Encoding: utf8\n"
and encoded it into utf-8 (using vim, for example). Then added my msgid, msgstr pairs, as usual.

So, my Bottle application have to know current language (preferred by user) and how to: a) found corresponding template b) how to translate word/text.
# to test, change language in the browser (see priority of them!). Then
# refresh page - you should see text in different language
_LANGS = {}
def initi18n():
    """Init internationalization of UI
    """
    import __builtin__
    global _LANGS
    for l in ("bg","ru"): # list of supported language
        trans = gettext.translation(I18N_DOMAIN, LOCALEDIR, languages=[l])
        _LANGS[l] = trans
    # default translator (i.e. _())
    __builtin__.__dict__["_"] = unicode


def request_lang(req):
    """Determine preferred language of the user from WSGI environment
    variable HTTP_ACCEPT_LANGUAGE. Default is 'en'
    """
    # if cfg module overrides user language
    if cfg.LANG != "default":
        return cfg.LANG

    try:
        return req.environ.get("HTTP_ACCEPT_LANGUAGE", "").split(";")[0].split("-")[0]
    except:
        return "en"


def switch_lang(lang):
    """Select language for output VIA GETTEXT
    """
    import __builtin__
    trans = _LANGS.get(lang, None)
    if trans:
        trans.install(unicode=True)
    else:
        __builtin__.__dict__["_"] = unicode


def templatei18n(name, lang=None, **kw):
    """Like bottle template() but render for current language
    from os.environ["LANGUAGE"]. And not like bottle's template,
    doesn't support source, only name (not path!) as 1st arg!
    If no lang, it will be selected from request
    """
    if not lang:
        lang = request_lang(request)
    tplpath = "%s/%s/TEMPLATES/%s" % (LOCALEDIR, lang, name)
    tplpath = os.path.abspath(tplpath) + ".tpl"
    if not os.path.exists(tplpath):
        tplpath = name
    return template(tplpath, **kw)


def i18n(templ):
    """Decorator for template rendering using current
    language (i18n support)
    """
    def decor(view):
        @functools.wraps(view)
        def w(*a, **kw):
            lang = request_lang(request)
            switch_lang(lang)
            d = view(*a, **kw)
            return templatei18n(templ, lang=lang, **d)
        return w
    return decor

I use initi18n() before application starting and use @i18n decorator for localized templates. Nothing else:
@i18n("tasks") # name of the template
def tasks_list(self):
    """Show table of tasks
    """
    ps = tasks_info()
    for taskname in ps:
        msg = self.runres.get(taskname, "") or ps[taskname]["msg"]
        ps[taskname]["msg"] = msg
        self.runres[taskname] = ""
    return dict(ps=ps)
User should select preferred language in the Web browser options. You - create template "tasks.tpl" in all locale/*/TEMPLATES and translated messages in .mo-file to use _(..) function everywhere you need (Python code, templates).