First commit
authorThierry Florac <tflorac@ulthar.net>
Thu, 19 Feb 2015 00:46:48 +0100
changeset 1 3f89629b9e54
parent 0 16d47bd81d84
child 2 a4db31e40fe1
First commit
src/pyams_utils.egg-info/PKG-INFO
src/pyams_utils.egg-info/SOURCES.txt
src/pyams_utils.egg-info/dependency_links.txt
src/pyams_utils.egg-info/entry_points.txt
src/pyams_utils.egg-info/namespace_packages.txt
src/pyams_utils.egg-info/not-zip-safe
src/pyams_utils.egg-info/requires.txt
src/pyams_utils.egg-info/top_level.txt
src/pyams_utils/__init__.py
src/pyams_utils/adapter.py
src/pyams_utils/attr.py
src/pyams_utils/configure.zcml
src/pyams_utils/context.py
src/pyams_utils/data.py
src/pyams_utils/date.py
src/pyams_utils/decorator.py
src/pyams_utils/doctests/README.txt
src/pyams_utils/encoding.py
src/pyams_utils/html.py
src/pyams_utils/i18n.py
src/pyams_utils/interfaces/__init__.py
src/pyams_utils/interfaces/data.py
src/pyams_utils/interfaces/site.py
src/pyams_utils/interfaces/size.py
src/pyams_utils/interfaces/tales.py
src/pyams_utils/interfaces/text.py
src/pyams_utils/interfaces/timezone.py
src/pyams_utils/list.py
src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.mo
src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.po
src/pyams_utils/locales/pyams_utils.pot
src/pyams_utils/property.py
src/pyams_utils/protocol/__init__.py
src/pyams_utils/protocol/http.py
src/pyams_utils/protocol/xmlrpc.py
src/pyams_utils/registry.py
src/pyams_utils/request.py
src/pyams_utils/schema.py
src/pyams_utils/scripts/__init__.py
src/pyams_utils/scripts/zodb.py
src/pyams_utils/session.py
src/pyams_utils/site.py
src/pyams_utils/size.py
src/pyams_utils/tales.py
src/pyams_utils/tests/__init__.py
src/pyams_utils/tests/test_utilsdocs.py
src/pyams_utils/tests/test_utilsdocstrings.py
src/pyams_utils/text.py
src/pyams_utils/timezone/__init__.py
src/pyams_utils/timezone/utility.py
src/pyams_utils/timezone/vocabulary.py
src/pyams_utils/traversing.py
src/pyams_utils/unicode.py
src/pyams_utils/url.py
src/pyams_utils/views/__init__.py
src/pyams_utils/views/decimal.py
src/pyams_utils/wsgi.py
src/pyams_utils/zmi/__init__.py
src/pyams_utils/zmi/configure.zcml
src/pyams_utils/zmi/timezone.py
src/pyams_utils/zodb.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/PKG-INFO	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,194 @@
+Metadata-Version: 1.1
+Name: pyams-utils
+Version: 0.1.0
+Summary: Utility functions and classes for PyAMS
+Home-page: http://www.ztfy.org
+Author: Thierry Florac
+Author-email: tflorac@ulthar.net
+License: ZPL
+Description: ===================
+        pyams_utils package
+        ===================
+        
+        .. contents::
+        
+        What is pyams_utils ?
+        =====================
+        
+        pyams_utils is a set of classes and functions which can be used to provide many small services and
+        handle common operations in the context of a Pyramid application.
+        
+        Internal sub-packages include :
+         - date : convert dates to unicode ISO format, parse ISO datetime, convert date to datetime
+         - request : get current request, get request annotations, get and set request data via annotations
+         - timezone : convert datetime to a given timezone ; provides a server default timezone utility
+         - traversing : get object parents until a given interface is implemented
+         - unicode : convert any text to unicode for easy storage
+         - protocol : utility functions and modules for several nerwork protocols
+         - catalog : TextIndexNG index for Zope catalog and hurry.query "Text" query item
+         - text : simple text operations and text to HTML conversion
+         - html : HTML parser and HTML to text converter
+         - file : file upload data converter
+         - tal : text and HTML conversions for use from within TAL
+        
+        
+        How to use pyams_utils ?
+        ========================
+        
+        A set of pyams_utils usages are given as doctests in pyams_utils/doctests/README.txt
+        
+        
+        Changelog
+        =========
+        
+        0.4.11
+        ------
+         - added "encode" and "decode" functions in "ztfy.utils.unicode" module (with updated doctests)
+        
+        0.4.10
+        ------
+         - added configuration directives to remove static dependencies with "ztfy.skin" and "zopyx.txng3.core"
+         - updated Buildout's bootstrap
+        
+        0.4.9
+        -----
+         - added "ztfy.utils.decorator" module with "@deprecated" decorator
+        
+        0.4.8
+        -----
+         - remove security proxy in ITransactionManager adapter
+        
+        0.4.7
+        -----
+         - added new "component" registry utility module
+         - added date and datetime display formats
+         - added "keep_chars" argument in translateString function
+        
+        0.4.6
+        -----
+         - added ISet interface with permissions on zc.set.Set class
+        
+        0.4.5
+        -----
+         - make current participation request optional in "indexObject()" function
+        
+        0.4.4
+        -----
+         - added "allow_none" and "headers" arguments in XML-RPC "getClient()" methods
+        
+        0.4.3
+        -----
+         - change test for date types in tztime/gmtime functions (because datetime
+           inherits from date!)
+        
+        0.4.2
+        -----
+         - small correction in getHumanSize() function
+         - added dates formatting functions
+         - added check between date and datetime types in timezone module
+        
+        0.4.1
+        -----
+         - use request locale formatter in getHumanSize function
+        
+        0.4.0
+        -----
+         - move custom schema fields widgets to ZTFY.skin package
+        
+        0.3.14
+        ------
+         - added legend on ZEO connection properties edit form
+         - force usage of "escapeSlashes" argument when checking new content name
+        
+        0.3.13
+        ------
+         - added ZEO connection interface, utility and tools
+         - added "ztfy.utils.container" utility module
+         - added a persistent utility to store ZEO connection settings
+         - added "TextLine list" schema field and widget
+         - added request and session cached properties
+         - added Python 2.7 compatibility code and timeout parameter to XML-RPC
+           protocol helper
+         - changed request "data:" TAL namespace to basic HTTP request so it can be used
+           in views called via JSON-RPC or XML-RPC
+        
+        0.3.12
+        ------
+         - updated package source layout
+        
+        0.3.11
+        ------
+         - added dotted decimal schema field, not handling locales :-/
+        
+        0.3.10
+        ------
+         - upgraded for ztfy.jqueryui 0.6.0
+         - added Color schema field and widget
+         - added StringLine schema field
+         - added "text:translate" TAL adapter
+         - moved ITransactionManager adapter from ztfy.scheduler package
+        
+        0.3.9
+        -----
+         - added HTTP client based on httplib2, handling authentication and proxies
+        
+        0.3.8
+        -----
+         - corrected encodings vocabulary
+        
+        0.3.7
+        -----
+         - added encodings vocabulary
+        
+        0.3.6
+        -----
+         - corrected code and translations in MissingPrincipal class
+         - added permissions on TextIndexNG index
+        
+        0.3.5
+        -----
+         - re-add IList and IDict interfaces forgotten from bad merge :-(
+        
+        0.3.4
+        -----
+         - better check for missing requests
+        
+        0.3.3
+        -----
+         - Added "fanstatic:" TALES expression
+        
+        0.3.2
+        -----
+         - Mark ztfy.utils.security functions and classes as deprecated
+        
+        0.3.1
+        -----
+         - Updated signature in ztfy.utils.catalog.index to match last hurry.query release
+        
+        0.3
+        ---
+         - Switched to ZTK-1.1.2 and Python 2.6
+         - Added "getAge" function in date module 
+         - Added session module and TALES adapter to get/set session values
+         - Check None value in catalog.getObjectId(...) and catalog.getObject(...) methods
+        
+        0.2.1
+        -----
+         - Added 'site.locateAndRegister' facility function
+         - Update ServerTimezoneUtility parent classes
+        
+        0.2
+        ---
+         - Added 'data' namespace to access request data
+        
+        0.1
+        ---
+         - Initial release
+        
+Keywords: Pyramid PyAMS utilities
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Zope Public License
+Classifier: Development Status :: 4 - Beta
+Classifier: Programming Language :: Python
+Classifier: Framework :: Zope3
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/SOURCES.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,67 @@
+MANIFEST.in
+setup.py
+docs/HISTORY.txt
+docs/README.txt
+src/pyams_utils/__init__.py
+src/pyams_utils/adapter.py
+src/pyams_utils/attr.py
+src/pyams_utils/configure.zcml
+src/pyams_utils/context.py
+src/pyams_utils/data.py
+src/pyams_utils/date.py
+src/pyams_utils/decorator.py
+src/pyams_utils/encoding.py
+src/pyams_utils/html.py
+src/pyams_utils/i18n.py
+src/pyams_utils/list.py
+src/pyams_utils/property.py
+src/pyams_utils/registry.py
+src/pyams_utils/request.py
+src/pyams_utils/schema.py
+src/pyams_utils/session.py
+src/pyams_utils/site.py
+src/pyams_utils/size.py
+src/pyams_utils/tales.py
+src/pyams_utils/text.py
+src/pyams_utils/traversing.py
+src/pyams_utils/unicode.py
+src/pyams_utils/url.py
+src/pyams_utils/wsgi.py
+src/pyams_utils/zodb.py
+src/pyams_utils.egg-info/PKG-INFO
+src/pyams_utils.egg-info/SOURCES.txt
+src/pyams_utils.egg-info/dependency_links.txt
+src/pyams_utils.egg-info/entry_points.txt
+src/pyams_utils.egg-info/namespace_packages.txt
+src/pyams_utils.egg-info/not-zip-safe
+src/pyams_utils.egg-info/requires.txt
+src/pyams_utils.egg-info/top_level.txt
+src/pyams_utils/browser/__init__.py
+src/pyams_utils/browser/configure.zcml
+src/pyams_utils/browser/decimal.py
+src/pyams_utils/doctests/README.txt
+src/pyams_utils/interfaces/__init__.py
+src/pyams_utils/interfaces/data.py
+src/pyams_utils/interfaces/site.py
+src/pyams_utils/interfaces/size.py
+src/pyams_utils/interfaces/tales.py
+src/pyams_utils/interfaces/text.py
+src/pyams_utils/interfaces/timezone.py
+src/pyams_utils/locales/pyams_utils.pot
+src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.mo
+src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.po
+src/pyams_utils/protocol/__init__.py
+src/pyams_utils/protocol/http.py
+src/pyams_utils/protocol/xmlrpc.py
+src/pyams_utils/scripts/__init__.py
+src/pyams_utils/scripts/zodb.py
+src/pyams_utils/tests/__init__.py
+src/pyams_utils/tests/test_utilsdocs.py
+src/pyams_utils/tests/test_utilsdocstrings.py
+src/pyams_utils/timezone/__init__.py
+src/pyams_utils/timezone/configure.zcml
+src/pyams_utils/timezone/utility.py
+src/pyams_utils/timezone/vocabulary.py
+src/pyams_utils/zmi/__init__.py
+src/pyams_utils/zmi/configure.zcml
+src/pyams_utils/zmi/timezone.py
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/dependency_links.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/entry_points.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,5 @@
+
+      # -*- Entry points: -*-
+      [console_scripts]
+      pyams_upgrade = pyams_utils.scripts.zodb:upgrade_site
+      
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/namespace_packages.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/not-zip-safe	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/requires.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,27 @@
+setuptools
+babel
+chameleon
+docutils
+httplib2
+persistent
+pyramid
+pyramid_zodbconn
+pysocks
+pytz
+transaction
+z3c.form
+z3c.pt
+z3c.ptcompat
+ZODB
+zope.annotation
+zope.component
+zope.container
+zope.datetime
+zope.interface
+zope.schema
+zope.security
+zope.site
+zope.traversing
+
+[test]
+pyramid_zcml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/top_level.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+pyams_utils
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,64 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from chameleon import PageTemplateFile
+from pyams_utils.context import ContextSelector
+from pyams_utils.request import get_annotations, get_debug
+from pyams_utils.site import site_factory
+from pyams_utils.tales import ExtensionExpr
+from pyams_utils.traversing import NamespaceTraverser
+from z3c.pt.pagetemplate import PageTemplateFile as Z3cPageTemplateFile
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_utils')
+
+
+def includeme(config):
+    """pyams_utils features include"""
+
+    # add translations
+    config.add_translation_dirs('pyams_utils:locales')
+
+    # define root factory
+    config.set_root_factory(site_factory)
+
+    # add request annotations
+    config.add_request_method(get_annotations, 'annotations', reify=True)
+    config.add_request_method(get_debug, 'debug', reify=True)
+
+    # add traverser handling namespaces via "++ns++(options)" URLs
+    config.add_traverser(NamespaceTraverser)
+
+    # add custom subscriber predicate to filter events via supported interface(s)
+    config.add_subscriber_predicate('context_selector', ContextSelector)
+
+    # load registry components
+    try:
+        import pyams_zmi
+    except ImportError:
+        config.scan(ignore='pyams_utils.zmi')
+    else:
+        config.scan()
+
+    if hasattr(config, 'load_zcml'):
+        config.load_zcml('configure.zcml')
+
+    PageTemplateFile.expression_types['extension'] = ExtensionExpr
+    Z3cPageTemplateFile.expression_types['extension'] = ExtensionExpr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/adapter.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,96 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import venusian
+
+# import interfaces
+
+# import packages
+from zope.interface import implementedBy
+
+
+class ContextAdapter(object):
+    """Context adapter"""
+
+    def __init__(self, context):
+        self.context = context
+
+
+class ContextRequestAdapter(object):
+    """Context + request adapter"""
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+
+class ContextRequestViewAdapter(object):
+    """Context + request + view adapter"""
+
+    def __init__(self, context, request, view):
+        self.context = context
+        self.request = request
+        self.view = view
+
+
+class adapter_config(object):
+    """Function or class decorator to declare an adapter"""
+
+    venusian = venusian
+
+    def __init__(self, **settings):
+        if 'for_' in settings:
+            if settings.get('context') is None:
+                settings['context'] = settings.pop('for_')
+        self.__dict__.update(settings)
+
+    def __call__(self, wrapped):
+        settings = self.__dict__.copy()
+        depth = settings.pop('_depth', 0)
+
+        def callback(context, name, ob):
+            adapts = settings.get('context')
+            if adapts is None:
+                adapts = getattr(ob, '__component_adapts__', None)
+                if adapts is None:
+                    raise TypeError("No for argument was provided for %r and "
+                                    "can't determine what the factory adapts." % ob)
+            if not isinstance(adapts, tuple):
+                adapts = (adapts,)
+
+            provides = settings.get('provides')
+            if provides is None:
+                intfs = list(implementedBy(ob))
+                if len(intfs) == 1:
+                    provides = intfs[0]
+                if provides is None:
+                    raise TypeError("Missing 'provided' argument")
+
+            config = context.config.with_package(info.module)
+            config.registry.registerAdapter(ob, adapts, provides, settings.get('name', ''))
+
+        info = self.venusian.attach(wrapped, callback, category='pyams_adapter',
+                                    depth=depth + 1)
+
+        if info.scope == 'class':
+            # if the decorator was attached to a method in a class, or
+            # otherwise executed at class scope, we need to set an
+            # 'attr' into the settings if one isn't already in there
+            if settings.get('attr') is None:
+                settings['attr'] = wrapped.__name__
+
+        settings['_info'] = info.codeinfo  # fbo "action_method"
+        return wrapped
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/attr.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from pyams_utils.adapter import ContextAdapter, adapter_config
+from pyramid.exceptions import NotFound
+from zope.interface import Interface
+
+
+@adapter_config(name='attr', context=Interface, provides=ITraversable)
+class AttributeTraverser(ContextAdapter):
+    """++attr++ namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        try:
+            return getattr(self.context, name)
+        except AttributeError:
+            raise NotFound
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/configure.zcml	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,29 @@
+<configure
+	xmlns="http://pylonshq.com/pyramid"
+	xmlns:zcml="http://namespaces.zope.org/zcml">
+
+	<include package="pyramid_zcml" />
+
+	<!-- Registration of external components -->
+	<include package="zope.component" file="meta.zcml" />
+	<include package="zope.browserpage" file="meta.zcml" />
+	<include package="zope.i18n" file="meta.zcml" />
+
+	<include package="z3c.form" file="meta.zcml" />
+
+	<include package="zope.component" />
+	<include package="zope.annotation" />
+	<include package="zope.dublincore" />
+	<include package="zope.site" />
+	<include package="zope.traversing" />
+
+	<include package="z3c.form" />
+	<include package="z3c.pt" />
+	<include package="z3c.ptcompat" />
+
+
+	<configure zcml:condition="installed pyams_zmi">
+		<include package=".zmi" />
+	</configure>
+
+</configure>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/context.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,49 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+class ContextSelector(object):
+    """Interface based context selector
+
+    This selector can be used on any subscriber to define
+    interfaces that the context must support for the event
+    to be applied
+    """
+
+    def __init__(self, ifaces, config):
+        if not isinstance(ifaces, (list, tuple)):
+            ifaces = (ifaces,)
+        self.interfaces = ifaces
+
+    def text(self):
+        return 'context_selector = %s' % str(self.interfaces)
+
+    phash = text
+
+    def __call__(self, event):
+        for intf in self.interfaces:
+            try:
+                if intf.providedBy(event.object):
+                    return True
+            except (AttributeError, TypeError):
+                if isinstance(event.object, intf):
+                    return True
+        return False
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/data.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,64 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+
+# import interfaces
+from pyams_utils.interfaces.data import IObjectData, IObjectDataRenderer
+from pyams_utils.interfaces.tales import ITALESExtension
+from pyramid.interfaces import IRequest
+from zope.publisher.interfaces.browser import IBrowserRequest
+
+# import packages
+from pyams_utils.adapter import ContextAdapter, ContextRequestViewAdapter, adapter_config
+from zope.interface import Interface
+
+
+@adapter_config(context=IObjectData, provides=IObjectDataRenderer)
+class ObjectDataRenderer(ContextAdapter):
+    """Object data JSON renderer"""
+
+    def get_object_data(self):
+        data = IObjectData(self.context)
+        return json.dumps(data.object_data) if data is not None else None
+
+
+@adapter_config(name='object_data', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class ObjectDataExtension(ContextRequestViewAdapter):
+    """extension:object_data TALES extension"""
+
+    def render(self, context=None):
+        if context is None:
+            context = self.context
+        renderer = IObjectDataRenderer(context, None)
+        if renderer is not None:
+            return renderer.get_object_data()
+
+
+@adapter_config(name='request_data', context=(Interface, IRequest, Interface), provides=ITALESExtension)
+class PyramidRequestDataExtension(ContextRequestViewAdapter):
+    """extension:request_data TALES extension for Pyramid request"""
+
+    def render(self, params=None):
+        return self.request.annotations.get(params)
+
+
+@adapter_config(name='request_data', context=(Interface, IBrowserRequest, Interface), provides=ITALESExtension)
+class BrowserRequestDataExtension(ContextRequestViewAdapter):
+    """extension:request_data TALES extension for Zope browser request"""
+
+    def render(self, params=None):
+        return self.request.annotations.get(params)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/date.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,194 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard packages
+from datetime import datetime
+
+# import interfaces
+
+# import packages
+from pyams_utils.request import check_request
+from pyams_utils.timezone import gmtime, tztime
+from zope.datetime import parseDatetimetz
+
+from pyams_utils import _
+
+
+def unidate(value):
+    """Get specified date converted to unicode ISO format
+    
+    Dates are always assumed to be stored in GMT timezone
+    
+    @param value: input date to convert to unicode
+    @type value: date or datetime
+    @return: input date converted to unicode
+    @rtype: unicode
+    """
+    if value is not None:
+        value = gmtime(value)
+        return value.isoformat('T')
+    return None
+
+
+def parse_date(value):
+    """Get date specified in unicode ISO format to Python datetime object
+    
+    Dates are always assumed to be stored in GMT timezone
+    
+    @param value: unicode date to be parsed
+    @type value: unicode
+    @return: the specified value, converted to datetime
+    @rtype: datetime
+    """
+    if value is not None:
+        return gmtime(parseDatetimetz(value))
+    return None
+
+
+def date_to_datetime(value):
+    """Get datetime value converted from a date or datetime object
+    
+    @param value: a date or datetime value to convert
+    @type value: date or datetime
+    @return: input value converted to datetime
+    @rtype: datetime
+    """
+    if type(value) is datetime:
+        return value
+    return datetime(value.year, value.month, value.day)
+
+
+SH_DATE_FORMAT = _("%d/%m/%Y")
+SH_DATETIME_FORMAT = _("%d/%m/%Y - %H:%M")
+
+EXT_DATE_FORMAT = _("on %d/%m/%Y")
+EXT_DATETIME_FORMAT = _("on %d/%m/%Y at %H:%M")
+
+
+def format_date(value, format=EXT_DATE_FORMAT, request=None):
+    """Format given date with the given format"""
+    if request is None:
+        request = check_request()
+    localizer = request.localizer
+    return datetime.strftime(tztime(value), localizer.translate(format))
+
+
+def format_datetime(value, format=EXT_DATETIME_FORMAT, request=None):
+    """Format given datetime with the given format"""
+    return format_date(value, format, request)
+
+
+def get_age(value):
+    """Get age of a given datetime (including timezone) compared to current datetime (in UTC)
+    
+    @param value: a datetime value, including timezone
+    @type value: datetime
+    @return: string representing value age
+    @rtype: gettext translated string
+    """
+    request = check_request()
+    translate = request.localizer.translate
+    now = gmtime(datetime.utcnow())
+    delta = now - value
+    if delta.days > 60:
+        return translate(_("%d months ago")) % int(round(delta.days * 1.0 / 30))
+    elif delta.days > 10:
+        return translate(_("%d weeks ago")) % int(round(delta.days * 1.0 / 7))
+    elif delta.days > 2:
+        return translate(_("%d days ago")) % delta.days
+    elif delta.days == 2:
+        return translate(_("the day before yesterday"))
+    elif delta.days == 1:
+        return translate(_("yesterday"))
+    else:
+        hours = int(round(delta.seconds * 1.0 / 3600))
+        if hours > 1:
+            return translate(_("%d hours ago")) % hours
+        elif delta.seconds > 300:
+            return translate(_("%d minutes ago")) % int(round(delta.seconds * 1.0 / 60))
+        else:
+            return translate(_("less than 5 minutes ago"))
+
+
+def get_duration(v1, v2=None, request=None):
+    """Get delta as string between two dates
+
+    >>> from datetime import datetime
+    >>> from pyams_utils.date import get_duration
+    >>> from pyramid.testing import DummyRequest
+    >>> request = DummyRequest()
+    >>> date1 = datetime(2015, 1, 1)
+    >>> date2 = datetime(2014, 3, 1)
+    >>> get_duration(date1, date2, request)
+    '10 months'
+
+    Dates order is not important:
+    >>> get_duration(date2, date1, request)
+    '10 months'
+    >>> date2 = datetime(2014, 11, 10)
+    >>> get_duration(date1, date2, request)
+    '7 weeks'
+    >>> date2 = datetime(2014, 12, 26)
+    >>> get_duration(date1, date2, request)
+    '6 days'
+
+    For durations lower than 2 days, duration also display hours:
+    >>> date1 = datetime(2015, 1, 1)
+    >>> date2 = datetime(2015, 1, 2, 15, 10, 0)
+    >>> get_duration(date1, date2, request)
+    '1 day and 15 hours'
+    >>> date2 = datetime(2015, 1, 2)
+    >>> get_duration(date1, date2, request)
+    '24 hours'
+    >>> date2 = datetime(2015, 1, 1, 13, 12)
+    >>> get_duration(date1, date2, request)
+    '13 hours'
+    >>> date2 = datetime(2015, 1, 1, 1, 15)
+    >>> get_duration(date1, date2, request)
+    '75 minutes'
+    >>> date2 = datetime(2015, 1, 1, 0, 0, 15)
+    >>> get_duration(date1, date2, request)
+    '15 seconds'
+    """
+    if v2 is None:
+        v2 = datetime.utcnow()
+    assert isinstance(v1, datetime) and isinstance(v2, datetime)
+    if request is None:
+        request = check_request()
+    translate = request.localizer.translate
+    v1, v2 = min(v1, v2), max(v1, v2)
+    delta = v2 - v1
+    if delta.days > 60:
+        return translate(_("%d months")) % int(round(delta.days * 1.0 / 30))
+    elif delta.days > 10:
+        return translate(_("%d weeks")) % int(round(delta.days * 1.0 / 7))
+    elif delta.days >= 2:
+        return translate(_("%d days")) % delta.days
+    else:
+        hours = int(round(delta.seconds * 1.0 / 3600))
+        if delta.days == 1:
+            if hours == 0:
+                return translate(_("24 hours"))
+            else:
+                return translate(_("%d day and %d hours")) % (delta.days, hours)
+        else:
+            if hours > 2:
+                return translate(_("%d hours")) % hours
+            else:
+                minutes = int(round(delta.seconds * 1.0 / 60))
+                if minutes > 2:
+                    return translate(_("%d minutes")) % minutes
+                else:
+                    return translate(_("%d seconds")) % delta.seconds
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/decorator.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,47 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import functools
+import warnings
+
+# import interfaces
+
+# import packages
+
+
+def deprecated(*msg):
+    """This is a decorator which can be used to mark functions
+    as deprecated. It will result in a warning being emitted
+    when the function is used.
+    """
+
+    def decorator(func):
+
+        @functools.wraps(func)
+        def new_func(*args, **kwargs):
+            warnings.warn_explicit("Function %s is deprecated. %s" % (func.__name__, message),
+                                   category=DeprecationWarning,
+                                   filename=func.func_code.co_filename,
+                                   lineno=func.func_code.co_firstlineno + 1)
+            return func(*args, **kwargs)
+        return new_func
+
+    if len(msg) == 1 and callable(msg[0]):
+        message = u''
+        return decorator(msg[0])
+    else:
+        message = msg[0]
+        return decorator
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/doctests/README.txt	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,135 @@
+==================
+ZTFY.utils package
+==================
+
+Introduction
+------------
+
+This package is composed of a set of utility functions, which in complement with zope.app.zapi
+package can make Zope management easier.
+
+
+Unicode functions
+-----------------
+
+While working with extended characters sets containing accentuated characters, it's necessary to
+conversing strings to UTF8 so that they can be used without any conversion problem.
+
+    >>> from pyams_utils import unicode
+
+'translate_string' is a utility function which can be used, for example, to generate an object's id
+without space and with accentuated characters converted to their unaccentuated version:
+
+    >>> sample = 'Mon titre accentué'
+    >>> unicode.translate_string(sample)
+    'mon titre accentue'
+
+Results are lower-cased by default ; this can be avoided be setting the 'force_lower' argument
+to False:
+
+    >>> unicode.translate_string(sample, force_lower=False)
+    'Mon titre accentue'
+    >>> unicode.translate_string(sample, force_lower=True, spaces='-')
+    'mon-titre-accentue'
+
+If input string can contain 'slashes' (/) or 'backslashes' (\), they are normally removed ; 
+by using the 'escape_slashes' parameter, the input string is splitted and only the last element is
+returned ; this is handy to handle filenames on Windows platform:
+
+    >>> sample = 'Autre / chaîne / accentuée'
+    >>> unicode.translate_string(sample)
+    'autre chaine accentuee'
+    >>> unicode.translate_string(sample, escape_slashes=True)
+    'accentuee'
+    >>> sample = 'C:\\Program Files\\My Application\\test.txt'
+    >>> unicode.translate_string(sample)
+    'cprogram filesmy applicationtest.txt'
+    >>> unicode.translate_string(sample, escape_slashes=True)
+    'test.txt'
+
+To remove remaining spaces or convert them to another character, you can use the "spaces" parameter
+which can contain any string to be used instead of initial spaces:
+
+    >>> sample = 'C:\\Program Files\\My Application\\test.txt'
+    >>> unicode.translate_string(sample, spaces=' ')
+    'cprogram filesmy applicationtest.txt'
+    >>> unicode.translate_string(sample, spaces='-')
+    'cprogram-filesmy-applicationtest.txt'
+
+Spaces replacement is made in the last step, so using it with "escape_slashes" parameter only affects
+the final result:
+
+    >>> unicode.translate_string(sample, escape_slashes=True, spaces='-')
+    'test.txt'
+
+Unicode module also provides encoding and decoding functions:
+
+    >>> var = b'Cha\xeene accentu\xe9e'
+    >>> unicode.decode(var, 'latin1')
+    'Chaîne accentuée'
+    >>> unicode.encode(unicode.decode(var, 'latin1'), 'latin1') == var
+    True
+
+    >>> utf = 'Chaîne accentuée'
+    >>> unicode.encode(utf, 'latin1')
+    b'Cha\xeene accentu\xe9e'
+    >>> unicode.decode(unicode.encode(utf, 'latin1'), 'latin1') == utf
+    True
+
+
+Dates functions
+---------------
+
+Dates functions are used to convert dates from/to string representation:
+
+    >>> from datetime import datetime
+    >>> from pyams_utils import date
+    >>> now = datetime.fromtimestamp(1205000000)
+    >>> now
+    datetime.datetime(2008, 3, 8, 19, 13, 20)
+
+You can get an unicode representation of a date in ASCII format using 'unidate' fonction ; date is
+converted to GMT:
+
+    >>> udate = date.unidate(now)
+    >>> udate
+    '2008-03-08T19:13:20+00:00'
+
+'parse_date' can be used to convert ASCII format into datetime:
+
+    >>> ddate = date.parse_date(udate)
+    >>> ddate
+    datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=<StaticTzInfo 'GMT'>)
+
+'date_to_datetime' can be used to convert a 'date' type to a 'datetime' value ; if a 'datetime' value
+is used as argument, it is returned 'as is':
+
+    >>> ddate.date()
+    datetime.date(2008, 3, 8)
+    >>> date.date_to_datetime(ddate)
+    datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=<StaticTzInfo 'GMT'>)
+    >>> date.date_to_datetime(ddate.date())
+    datetime.datetime(2008, 3, 8, 0, 0)
+
+
+Timezones handling
+------------------
+
+Timezones handling game me headaches at first. I finally concluded that the best way (for me !) to handle
+TZ data was to store every datetime value in GMT timezone.
+As far as I know, there is no easy way to know the user's timezone from his request settings. So you can:
+- store this timezone in user's profile,
+- define a static server's timezone
+- create and register a ServerTimezoneUtility to handle server default timezone.
+
+My current default user's timezone is set to 'Europe/Paris' ; you should probably update this setting in
+'timezone.py' if you are located elsewhere.
+
+    >>> from pyams_utils import timezone
+    >>> timezone.tztime(ddate)
+    datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=<StaticTzInfo 'GMT'>)
+
+'gmtime' function can be used to convert a datetime to GMT:
+
+    >>> timezone.gmtime(now)
+    datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=<StaticTzInfo 'GMT'>)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/encoding.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,153 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.schema.interfaces import IVocabularyFactory, IChoice
+
+# import packages
+from pyams_utils.request import check_request
+from zope.interface import provider, implementer
+from zope.schema import Choice
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary, getVocabularyRegistry
+
+from pyams_utils import _
+
+
+ENCODINGS = {
+    'ascii': _('English (ASCII)'),
+    'big5': _('Traditional Chinese (big5)'),
+    'big5hkscs': _('Traditional Chinese (big5hkscs)'),
+    'cp037': _('English (cp037)'),
+    'cp424': _('Hebrew (cp424)'),
+    'cp437': _('English (cp437)'),
+    'cp500': _('Western Europe (cp500)'),
+    'cp720': _('Arabic (cp720)'),
+    'cp737': _('Greek (cp737)'),
+    'cp775': _('Baltic languages (cp775)'),
+    'cp850': _('Western Europe (cp850)'),
+    'cp852': _('Central and Eastern Europe (cp852)'),
+    'cp855': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)'),
+    'cp856': _('Hebrew (cp856)'),
+    'cp857': _('Turkish (cp857)'),
+    'cp858': _('Western Europe (cp858)'),
+    'cp860': _('Portuguese (cp860)'),
+    'cp861': _('Icelandic (cp861)'),
+    'cp862': _('Hebrew (cp862)'),
+    'cp863': _('Canadian (cp863)'),
+    'cp864': _('Arabic (cp864)'),
+    'cp865': _('Danish, Norwegian (cp865)'),
+    'cp866': _('Russian (cp866)'),
+    'cp869': _('Greek (cp869)'),
+    'cp874': _('Thai (cp874)'),
+    'cp875': _('Greek (cp875)'),
+    'cp932': _('Japanese (cp932)'),
+    'cp949': _('Korean (cp949)'),
+    'cp950': _('Traditional Chinese (cp950)'),
+    'cp1006': _('Urdu (cp1006)'),
+    'cp1026': _('Turkish (cp1026)'),
+    'cp1140': _('Western Europe (cp1140)'),
+    'cp1250': _('Central and Eastern Europe (cp1250)'),
+    'cp1251': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)'),
+    'cp1252': _('Western Europe (cp1252)'),
+    'cp1253': _('Greek (cp1253)'),
+    'cp1254': _('Turkish (cp1254)'),
+    'cp1255': _('Hebrew (cp1255)'),
+    'cp1256': _('Arabic (cp1256)'),
+    'cp1257': _('Baltic languages (cp1257)'),
+    'cp1258': _('Vietnamese (cp1258)'),
+    'euc_jp': _('Japanese (euc_jp)'),
+    'euc_jis_2004': _('Japanese (euc_jis_2004)'),
+    'euc_jisx0213': _('Japanese (euc_jisx0213)'),
+    'euc_kr': _('Korean (euc_kr)'),
+    'gb2312': _('Simplified Chinese (gb2312)'),
+    'gbk': _('Unified Chinese (gbk)'),
+    'gb18030': _('Unified Chinese (gb18030)'),
+    'hz': _('Simplified Chinese (hz)'),
+    'iso2022_jp': _('Japanese (iso2022_jp)'),
+    'iso2022_jp_1': _('Japanese (iso2022_jp_1)'),
+    'iso2022_jp_2': _('Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)'),
+    'iso2022_jp_2004': _('Japanese (iso2022_jp_2004)'),
+    'iso2022_jp_3': _('Japanese (iso2022_jp_3)'),
+    'iso2022_jp_ext': _('Japanese (iso2022_jp_ext)'),
+    'iso2022_kr': _('Korean (iso2022_kr)'),
+    'latin_1': _('West Europe (latin_1)'),
+    'iso8859_2': _('Central and Eastern Europe (iso8859_2)'),
+    'iso8859_3': _('Esperanto, Maltese (iso8859_3)'),
+    'iso8859_4': _('Baltic languages (iso8859_4)'),
+    'iso8859_5': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)'),
+    'iso8859_6': _('Arabic (iso8859_6)'),
+    'iso8859_7': _('Greek (iso8859_7)'),
+    'iso8859_8': _('Hebrew (iso8859_8)'),
+    'iso8859_9': _('Turkish (iso8859_9)'),
+    'iso8859_10': _('Nordic languages (iso8859_10)'),
+    'iso8859_13': _('Baltic languages (iso8859_13)'),
+    'iso8859_14': _('Celtic languages (iso8859_14)'),
+    'iso8859_15': _('Western Europe (iso8859_15)'),
+    'iso8859_16': _('South-Eastern Europe (iso8859_16)'),
+    'johab': _('Korean (johab)'),
+    'koi8_r': _('Russian (koi8_r)'),
+    'koi8_u': _('Ukrainian (koi8_u)'),
+    'mac_cyrillic': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)'),
+    'mac_greek': _('Greek (mac_greek)'),
+    'mac_iceland': _('Icelandic (mac_iceland)'),
+    'mac_latin2': _('Central and Eastern Europe (mac_latin2)'),
+    'mac_roman': _('Western Europe (mac_roman)'),
+    'mac_turkish': _('Turkish (mac_turkish)'),
+    'ptcp154': _('Kazakh (ptcp154)'),
+    'shift_jis': _('Japanese (shift_jis)'),
+    'shift_jis_2004': _('Japanese (shift_jis_2004)'),
+    'shift_jisx0213': _('Japanese (shift_jisx0213)'),
+    'utf_32': _('all languages (utf_32)'),
+    'utf_32_be': _('all languages (utf_32_be)'),
+    'utf_32_le': _('all languages (utf_32_le)'),
+    'utf_16': _('all languages (utf_16)'),
+    'utf_16_be': _('all languages (BMP only - utf_16_be)'),
+    'utf_16_le': _('all languages (BMP only - utf_16_le)'),
+    'utf_7': _('all languages (utf_7)'),
+    'utf_8': _('all languages (utf_8)'),
+    'utf_8_sig': _('all languages (utf_8_sig)'),
+}
+
+
+@provider(IVocabularyFactory)
+class EncodingsVocabulary(SimpleVocabulary):
+
+    def __init__(self, terms, *interfaces):
+        request = check_request()
+        translate = request.localizer.translate
+        terms = [SimpleTerm(v, title=translate(t)) for v, t in ENCODINGS.items()]
+        terms.sort(key=lambda x: x.title)
+        super(EncodingsVocabulary, self).__init__(terms, *interfaces)
+
+getVocabularyRegistry().register('PyAMS encodings', EncodingsVocabulary)
+
+
+class IEncodingField(IChoice):
+    """Encoding field interface"""
+
+
+@implementer(IEncodingField)
+class EncodingField(Choice):
+    """Encoding field"""
+
+    def __init__(self, vocabulary='PyAMS encodings', **kw):
+        if 'values' in kw:
+            del kw['values']
+        if 'source' in kw:
+            del kw['source']
+        kw['vocabulary'] = vocabulary
+        super(EncodingField, self).__init__(**kw)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/html.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,124 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from html.parser import HTMLParser
+
+# import interfaces
+
+# import packages
+
+
+class MyHTMLParser(HTMLParser):
+    """HTML parser"""
+    data = ''
+    entitydefs = {'amp': '&', 'lt': '<', 'gt': '>',
+                  'nbsp': ' ',
+                  'apos': "'", 'quot': '"',
+                  'Agrave': 'À', 'Aacute': 'A', 'Acirc': 'Â', 'Atilde': 'A', 'Auml': 'Ä', 'Aring': 'A',
+                  'AElig': 'AE',
+                  'Ccedil': 'Ç',
+                  'Egrave': 'É', 'Eacute': 'È', 'Ecirc': 'Ê', 'Euml': 'Ë',
+                  'Igrave': 'I', 'Iacute': 'I', 'Icirc': 'I', 'Iuml': 'I',
+                  'Ntilde': 'N',
+                  'Ograve': 'O', 'Oacute': 'O', 'Ocirc': 'Ô', 'Otilde': 'O', 'Ouml': 'Ö', 'Oslash': 'O',
+                  'Ugrave': 'Ù', 'Uacute': 'U', 'Ucirc': 'Û', 'Uuml': 'Ü',
+                  'Yacute': 'Y',
+                  'THORN': 'T',
+                  'agrave': 'à', 'aacute': 'a', 'acirc': 'â', 'atilde': 'a', 'auml': 'ä', 'aring': 'a', 'aelig': 'ae',
+                  'ccedil': 'ç',
+                  'egrave': 'è', 'eacute': 'é', 'ecirc': 'ê', 'euml': 'ë',
+                  'igrave': 'i', 'iacute': 'i', 'icirc': 'î', 'iuml': 'ï',
+                  'ntilde': 'n',
+                  'ograve': 'o', 'oacute': 'o', 'ocirc': 'ô', 'otilde': 'o', 'ouml': 'ö', 'oslash': 'o',
+                  'ugrave': 'ù', 'uacute': 'u', 'ucirc': 'û', 'uuml': 'ü',
+                  'yacute': 'y',
+                  'thorn': 't',
+                  'yuml': 'ÿ'}
+
+    charrefs = {34: '"', 38: '&', 39: "'",
+                60: '<', 62: '>',
+                192: 'À', 193: 'A', 194: 'Â', 195: 'A', 196: 'Ä', 197: 'A',
+                198: 'AE',
+                199: 'Ç',
+                200: 'È', 201: 'É', 202: 'Ê', 203: 'Ë',
+                204: 'I', 205: 'I', 206: 'Î', 207: 'Ï',
+                208: 'D',
+                209: 'N',
+                210: 'O', 211: 'O', 212: 'Ô', 213: 'O', 214: 'Ö', 216: 'O',
+                215: 'x',
+                217: 'Ù', 218: 'U', 219: 'Û', 220: 'Ü',
+                221: 'Y', 222: 'T',
+                223: 'sz',
+                224: 'à', 225: 'a', 226: 'â', 227: 'a', 228: 'ä', 229: 'a',
+                230: 'ae',
+                231: 'ç',
+                232: 'è', 233: 'é', 234: 'ê', 235: 'ë',
+                236: 'i', 237: 'i', 238: 'î', 239: 'ï',
+                240: 'e',
+                241: 'n',
+                242: 'o', 243: 'o', 244: 'ô', 245: 'o', 246: 'ö', 248: 'o',
+                249: 'ù', 250: 'u', 251: 'û', 252: 'ü',
+                253: 'y', 255: 'ÿ'}
+
+    def handle_data(self, data):
+        try:
+            self.data += data
+        except:
+            self.data += data.decode('utf8')
+
+    def handle_entityref(self, name):
+        self.data += self.entitydefs.get(name, '')
+
+    def handle_charref(self, name):
+        try:
+            n = int(name)
+        except ValueError:
+            return
+        if not 0 <= n <= 255:
+            return
+        self.handle_data(self.charrefs.get(n))
+
+    def handle_starttag(self, tag, attrs):
+        if tag == 'td':
+            self.data += ' '
+        elif tag == 'br':
+            self.data += '\n'
+
+    def handle_endtag(self, tag):
+        if tag == 'p':
+            self.data += '\n'
+
+
+def html_to_text(value):
+    """Utility function to extract text content from HTML
+
+    >>> from pyams_utils.html import html_to_text
+    >>> html = '''<p>This is a HTML text part.</p>'''
+    >>> html_to_text(html)
+    'This is a HTML text part.\\n'
+
+    HTML parser should handle entities correctly:
+    >>> html = '''<div><p>Header</p><p>This is an &lt;&nbsp;&#242;&nbsp;&gt; entity.<br /></p></div>'''
+    >>> html_to_text(html)
+    'Header\\nThis is an < o > entity.\\n\\n'
+
+    """
+    if value is None:
+        return ''
+    parser = MyHTMLParser()
+    parser.feed(value)
+    parser.close()
+    return parser.data
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/i18n.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,76 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def normalize_lang(lang):
+    lang = lang.strip().lower()
+    lang = lang.replace('_', '-')
+    lang = lang.replace(' ', '')
+    return lang
+
+
+def get_browser_language(request):
+    """Custom locale negotiator
+
+    Copied from zope.publisher code
+    """
+    accept_langs = request.headers.get('Accept-Language', '').split(',')
+
+    # Normalize lang strings
+    accept_langs = [normalize_lang(l) for l in accept_langs]
+    # Then filter out empty ones
+    accept_langs = [l for l in accept_langs if l]
+
+    accepts = []
+    for index, lang in enumerate(accept_langs):
+        l = lang.split(';', 2)
+
+        # If not supplied, quality defaults to 1...
+        quality = 1.0
+
+        if len(l) == 2:
+            q = l[1]
+            if q.startswith('q='):
+                q = q.split('=', 2)[1]
+                try:
+                    quality = float(q)
+                except ValueError:
+                    # malformed quality value, skip it.
+                    continue
+
+        if quality == 1.0:
+            # ... but we use 1.9 - 0.001 * position to
+            # keep the ordering between all items with
+            # 1.0 quality, which may include items with no quality
+            # defined, and items with quality defined as 1.
+            quality = 1.9 - (0.001 * index)
+
+        accepts.append((quality, l[0]))
+
+    # Filter langs with q=0, which means
+    # unwanted lang according to the spec
+    # See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
+    accepts = [acc for acc in accepts if acc[0]]
+
+    accepts.sort()
+    accepts.reverse()
+
+    return [lang for qual, lang in accepts][0] if accepts else None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,30 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+# ZODB application name settings key
+PYAMS_APPLICATION_SETTINGS_KEY = 'pyams.application_name'
+
+# ZODB default application name
+PYAMS_APPLICATION_DEFAULT_NAME = 'application'
+
+# Settings key to define site root factory
+PYAMS_APPLICATION_FACTORY_KEY = 'pyams.application_factory'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/data.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+from zope.schema import Dict
+
+
+class IObjectData(Interface):
+    """Object data generic interface"""
+
+    object_data = Dict(title="Data associated with this object",
+                       required=False)
+
+
+class IObjectDataRenderer(Interface):
+    """Object data rendering interface"""
+
+    def get_object_data(self):
+        """Get object data as JSON string"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/site.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,58 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.component.interfaces import IObjectEvent
+
+# import packages
+from zope.interface import Interface, Attribute
+
+
+class ISiteRoot(IAttributeAnnotatable):
+    """Marker interface for site root"""
+
+
+class INewLocalSiteCreatedEvent(IObjectEvent):
+    """Event interface when a new site root has been created"""
+
+
+class ISiteUpgradeEvent(IObjectEvent):
+    """Event interface when a site upgrade is requested"""
+
+
+SITE_GENERATIONS_KEY = 'pyams.generations'
+
+
+class ISiteGenerations(Interface):
+    """Site generations interface"""
+
+    generation = Attribute("Current schema generation")
+
+    def evolve(self, site, current=None):
+        """Evolve from current generation to last one"""
+
+
+class IConfigurationManager(IAttributeAnnotatable):
+    """Configuration manager marker interface"""
+
+
+class IConfigurationFactory(Interface):
+    """Configuration factory interface
+
+    This factory may be loaded through an adapter
+    """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/size.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,28 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+from zope.schema import Int
+
+
+class ILength(Interface):
+    """Length interface"""
+
+    length = Int(title="Object length")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/tales.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,31 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+
+
+class ITALESExtension(Interface):
+    """Custom TALES extension
+
+    These extensions will be registered throught adapters for
+    (context, request, view) or (context, request)
+    """
+
+    def render(self, context=None):
+        """Render extension"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/text.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface, Attribute
+
+from pyams_utils import _
+
+
+class IHTMLRenderer(Interface):
+    """Text renderer interface
+
+    HTML renderers are implemented as adapters for a source object (which can
+    be a string) and a request, so that you can easily implement custom renderers
+    for any object and/or for any request layer.
+    """
+
+    title = Attribute(_("Renderer name"))
+
+    def render(self, **kwargs):
+        """Render adapted text"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/timezone.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,49 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.schema.interfaces import IChoice
+
+# import packages
+from zope.interface import implementer, Interface
+from zope.schema import Choice
+
+from pyams_utils import _
+
+
+class ITimezone(IChoice):
+    """Marker interface for timezone field"""
+
+
+@implementer(ITimezone)
+class Timezone(Choice):
+    """Timezone choice field"""
+
+    def __init__(self, **kw):
+        if 'vocabulary' in kw:
+            kw.pop('vocabulary')
+        if 'default' not in kw:
+            kw['default'] = u'GMT'
+        super(Timezone, self).__init__(vocabulary='PyAMS timezones', **kw)
+
+
+class IServerTimezone(Interface):
+    """Server timezone interface"""
+
+    timezone = Timezone(title=_("Server timezone"),
+                        description=_("Default server timezone"),
+                        required=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/list.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,41 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def unique(seq, idfun=None):
+    """Extract unique values from list, preserving order
+
+    >>> from pyams_utils.list import unique
+    >>> mylist = [1, 2, 3, 2, 1]
+    >>> unique(mylist)
+    [1, 2, 3]
+    """
+    if idfun is None:
+        def idfun(x): return x
+    seen = {}
+    result = []
+    for item in seq:
+        marker = idfun(item)
+        if marker in seen:
+            continue
+        seen[marker] = 1
+        result.append(item)
+    return result
Binary file src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.po	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,521 @@
+# French translations for PACKAGE package
+# This file is distributed under the same license as the PACKAGE package.
+# Thierry Florac <tflorac@ulthar.net>, 2015.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-01-24 16:53+0100\n"
+"PO-Revision-Date: 2015-01-18 01:01+0100\n"
+"Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"Generated-By: Lingua 3.7\n"
+
+#: src/pyams_utils/encoding.py:33
+msgid "English (ASCII)"
+msgstr "Anglais (ASCII)"
+
+#: src/pyams_utils/encoding.py:34
+msgid "Traditional Chinese (big5)"
+msgstr "Chinois traditionnel (big5)"
+
+#: src/pyams_utils/encoding.py:35
+msgid "Traditional Chinese (big5hkscs)"
+msgstr "Chinois traditionnel (big5hkscs)"
+
+#: src/pyams_utils/encoding.py:36
+msgid "English (cp037)"
+msgstr "Anglais (cp037)"
+
+#: src/pyams_utils/encoding.py:37
+msgid "Hebrew (cp424)"
+msgstr "Hébreu (cp424)"
+
+#: src/pyams_utils/encoding.py:38
+msgid "English (cp437)"
+msgstr "Anglais (cp437)"
+
+#: src/pyams_utils/encoding.py:39
+msgid "Western Europe (cp500)"
+msgstr "Europe de l'ouest (cp500)"
+
+#: src/pyams_utils/encoding.py:40
+msgid "Arabic (cp720)"
+msgstr "Arabe (cp720)"
+
+#: src/pyams_utils/encoding.py:41
+msgid "Greek (cp737)"
+msgstr "Grec (cp737)"
+
+#: src/pyams_utils/encoding.py:42
+msgid "Baltic languages (cp775)"
+msgstr "Langues baltes (cp775)"
+
+#: src/pyams_utils/encoding.py:43
+msgid "Western Europe (cp850)"
+msgstr "Europe de l'ouest (cp850)"
+
+#: src/pyams_utils/encoding.py:44
+msgid "Central and Eastern Europe (cp852)"
+msgstr "Europe centrale et de l'est (cp852)"
+
+#: src/pyams_utils/encoding.py:45
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)"
+msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (cp855)"
+
+#: src/pyams_utils/encoding.py:46
+msgid "Hebrew (cp856)"
+msgstr "Hébreu (cp856)"
+
+#: src/pyams_utils/encoding.py:47
+msgid "Turkish (cp857)"
+msgstr "Turc (cp857)"
+
+#: src/pyams_utils/encoding.py:48
+msgid "Western Europe (cp858)"
+msgstr "Europe de l'ouest (cp858)"
+
+#: src/pyams_utils/encoding.py:49
+msgid "Portuguese (cp860)"
+msgstr "Portugais (cp860)"
+
+#: src/pyams_utils/encoding.py:50
+msgid "Icelandic (cp861)"
+msgstr "Islandais (cp861)"
+
+#: src/pyams_utils/encoding.py:51
+msgid "Hebrew (cp862)"
+msgstr "Hébreu (cp862)"
+
+#: src/pyams_utils/encoding.py:52
+msgid "Canadian (cp863)"
+msgstr "Canadien (cp863)"
+
+#: src/pyams_utils/encoding.py:53
+msgid "Arabic (cp864)"
+msgstr "Arabe (cp864)"
+
+#: src/pyams_utils/encoding.py:54
+msgid "Danish, Norwegian (cp865)"
+msgstr "Danois, Norvégien (cp865)"
+
+#: src/pyams_utils/encoding.py:55
+msgid "Russian (cp866)"
+msgstr "Russe (cp866)"
+
+#: src/pyams_utils/encoding.py:56
+msgid "Greek (cp869)"
+msgstr "Grec (cp869)"
+
+#: src/pyams_utils/encoding.py:57
+msgid "Thai (cp874)"
+msgstr "Thaï (cp874)"
+
+#: src/pyams_utils/encoding.py:58
+msgid "Greek (cp875)"
+msgstr "Grec (cp875)"
+
+#: src/pyams_utils/encoding.py:59
+msgid "Japanese (cp932)"
+msgstr "Japonais (cp932)"
+
+#: src/pyams_utils/encoding.py:60
+msgid "Korean (cp949)"
+msgstr "Coréen (cp949)"
+
+#: src/pyams_utils/encoding.py:61
+msgid "Traditional Chinese (cp950)"
+msgstr "Chinois traditionnel (cp950)"
+
+#: src/pyams_utils/encoding.py:62
+msgid "Urdu (cp1006)"
+msgstr "Ourdou (cp1006)"
+
+#: src/pyams_utils/encoding.py:63
+msgid "Turkish (cp1026)"
+msgstr "Turc (cp1026)"
+
+#: src/pyams_utils/encoding.py:64
+msgid "Western Europe (cp1140)"
+msgstr "Europe de l'ouest (cp1140)"
+
+#: src/pyams_utils/encoding.py:65
+msgid "Central and Eastern Europe (cp1250)"
+msgstr "Europe centrale et de l'est (cp1250)"
+
+#: src/pyams_utils/encoding.py:66
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)"
+msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (cp1251)"
+
+#: src/pyams_utils/encoding.py:67
+msgid "Western Europe (cp1252)"
+msgstr "Europe de l'ouest (cp1252)"
+
+#: src/pyams_utils/encoding.py:68
+msgid "Greek (cp1253)"
+msgstr "Grec (cp1253)"
+
+#: src/pyams_utils/encoding.py:69
+msgid "Turkish (cp1254)"
+msgstr "Turc (cp1254)"
+
+#: src/pyams_utils/encoding.py:70
+msgid "Hebrew (cp1255)"
+msgstr "Hébreu (cp1255)"
+
+#: src/pyams_utils/encoding.py:71
+msgid "Arabic (cp1256)"
+msgstr "Arabe (cp1256)"
+
+#: src/pyams_utils/encoding.py:72
+msgid "Baltic languages (cp1257)"
+msgstr "Langues baltes (cp1257)"
+
+#: src/pyams_utils/encoding.py:73
+msgid "Vietnamese (cp1258)"
+msgstr "Viernamien (cp1258)"
+
+#: src/pyams_utils/encoding.py:74
+msgid "Japanese (euc_jp)"
+msgstr "Japonais (euc-jp)"
+
+#: src/pyams_utils/encoding.py:75
+msgid "Japanese (euc_jis_2004)"
+msgstr "Japonais (euc-jis-2004)"
+
+#: src/pyams_utils/encoding.py:76
+msgid "Japanese (euc_jisx0213)"
+msgstr "Japonais (euc-jisx0213)"
+
+#: src/pyams_utils/encoding.py:77
+msgid "Korean (euc_kr)"
+msgstr "Coréen (euc-kr)"
+
+#: src/pyams_utils/encoding.py:78
+msgid "Simplified Chinese (gb2312)"
+msgstr "Chinois simplifié (gb2312)"
+
+#: src/pyams_utils/encoding.py:79
+msgid "Unified Chinese (gbk)"
+msgstr "Chinois unifié (gbk)"
+
+#: src/pyams_utils/encoding.py:80
+msgid "Unified Chinese (gb18030)"
+msgstr "Chinois unifié (gb18030)"
+
+#: src/pyams_utils/encoding.py:81
+msgid "Simplified Chinese (hz)"
+msgstr "Chinois simplifié (hz)"
+
+#: src/pyams_utils/encoding.py:82
+msgid "Japanese (iso2022_jp)"
+msgstr "Japonais (iso2022-jp)"
+
+#: src/pyams_utils/encoding.py:83
+msgid "Japanese (iso2022_jp_1)"
+msgstr "Japonais (iso2022-jp-1)"
+
+#: src/pyams_utils/encoding.py:84
+msgid ""
+"Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)"
+msgstr ""
+"Japonais, Coréen, Chinois simplifié, Europe de l'ouest, Grec (iso2022-jp-2)"
+
+#: src/pyams_utils/encoding.py:85
+msgid "Japanese (iso2022_jp_2004)"
+msgstr "Japonais (iso2022-jp-2004)"
+
+#: src/pyams_utils/encoding.py:86
+msgid "Japanese (iso2022_jp_3)"
+msgstr "Japonais (iso2022-jp-3)"
+
+#: src/pyams_utils/encoding.py:87
+msgid "Japanese (iso2022_jp_ext)"
+msgstr "Japonais (iso2022-jp-ext)"
+
+#: src/pyams_utils/encoding.py:88
+msgid "Korean (iso2022_kr)"
+msgstr "Coréen (iso2022-kr)"
+
+#: src/pyams_utils/encoding.py:89
+msgid "West Europe (latin_1)"
+msgstr "Europe de l'ouest (latin-1)"
+
+#: src/pyams_utils/encoding.py:90
+msgid "Central and Eastern Europe (iso8859_2)"
+msgstr "Europe centrale et de l'est (iso8859-2)"
+
+#: src/pyams_utils/encoding.py:91
+msgid "Esperanto, Maltese (iso8859_3)"
+msgstr "Espéranto, Maltais (iso8859-3)"
+
+#: src/pyams_utils/encoding.py:92
+msgid "Baltic languages (iso8859_4)"
+msgstr "Langues baltes (iso8859-4)"
+
+#: src/pyams_utils/encoding.py:93
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)"
+msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (iso8859-5)"
+
+#: src/pyams_utils/encoding.py:94
+msgid "Arabic (iso8859_6)"
+msgstr "Arabe (iso8859-6)"
+
+#: src/pyams_utils/encoding.py:95
+msgid "Greek (iso8859_7)"
+msgstr "Grec (iso8869-7)"
+
+#: src/pyams_utils/encoding.py:96
+msgid "Hebrew (iso8859_8)"
+msgstr "Hébreu (iso8859-8)"
+
+#: src/pyams_utils/encoding.py:97
+msgid "Turkish (iso8859_9)"
+msgstr "Turc (iso8859-9)"
+
+#: src/pyams_utils/encoding.py:98
+msgid "Nordic languages (iso8859_10)"
+msgstr "Langues nordiques (iso8859-10)"
+
+#: src/pyams_utils/encoding.py:99
+msgid "Baltic languages (iso8859_13)"
+msgstr "Langues baltes (iso8859-13)"
+
+#: src/pyams_utils/encoding.py:100
+msgid "Celtic languages (iso8859_14)"
+msgstr "Langues celtes (iso8859-14)"
+
+#: src/pyams_utils/encoding.py:101
+msgid "Western Europe (iso8859_15)"
+msgstr "Europe de l'ouest (iso8859-15)"
+
+#: src/pyams_utils/encoding.py:102
+msgid "South-Eastern Europe (iso8859_16)"
+msgstr "Europe du sud-est (iso8859-16)"
+
+#: src/pyams_utils/encoding.py:103
+msgid "Korean (johab)"
+msgstr "Coréen (johab)"
+
+#: src/pyams_utils/encoding.py:104
+msgid "Russian (koi8_r)"
+msgstr "Russe (kio8-r)"
+
+#: src/pyams_utils/encoding.py:105
+msgid "Ukrainian (koi8_u)"
+msgstr "Ukrainien (kio8-u)"
+
+#: src/pyams_utils/encoding.py:106
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)"
+msgstr "Bulgare, Biolorusse, Macédonien, Russe, Serve (mac-cyrillic)"
+
+#: src/pyams_utils/encoding.py:107
+msgid "Greek (mac_greek)"
+msgstr "Grec (mac-greek)"
+
+#: src/pyams_utils/encoding.py:108
+msgid "Icelandic (mac_iceland)"
+msgstr "Islandais (mac-iceland)"
+
+#: src/pyams_utils/encoding.py:109
+msgid "Central and Eastern Europe (mac_latin2)"
+msgstr "Europe centrale et de l'ouest (mac-latin2)"
+
+#: src/pyams_utils/encoding.py:110
+msgid "Western Europe (mac_roman)"
+msgstr "Europe de l'Ouest (mac-roman)"
+
+#: src/pyams_utils/encoding.py:111
+msgid "Turkish (mac_turkish)"
+msgstr "Turc (mac-turkish)"
+
+#: src/pyams_utils/encoding.py:112
+msgid "Kazakh (ptcp154)"
+msgstr "Kazak (ptcp154)"
+
+#: src/pyams_utils/encoding.py:113
+msgid "Japanese (shift_jis)"
+msgstr "Japonais (shift_jis)"
+
+#: src/pyams_utils/encoding.py:114
+msgid "Japanese (shift_jis_2004)"
+msgstr "Japonais (shift-jis-2004)"
+
+#: src/pyams_utils/encoding.py:115
+msgid "Japanese (shift_jisx0213)"
+msgstr "Japonais (shift-jisx0213)"
+
+#: src/pyams_utils/encoding.py:116
+msgid "all languages (utf_32)"
+msgstr "toutes les langues (utf-32)"
+
+#: src/pyams_utils/encoding.py:117
+msgid "all languages (utf_32_be)"
+msgstr "toutes les langues (utf-32-be)"
+
+#: src/pyams_utils/encoding.py:118
+msgid "all languages (utf_32_le)"
+msgstr "toutes les langues (utf-32-le)"
+
+#: src/pyams_utils/encoding.py:119
+msgid "all languages (utf_16)"
+msgstr "toutes les langues (utf-16)"
+
+#: src/pyams_utils/encoding.py:120
+msgid "all languages (BMP only - utf_16_be)"
+msgstr "toutes les langues (BMP seulement - utf-16-be"
+
+#: src/pyams_utils/encoding.py:121
+msgid "all languages (BMP only - utf_16_le)"
+msgstr "toutes les langues (BMP seulement - utf-16-le)"
+
+#: src/pyams_utils/encoding.py:122
+msgid "all languages (utf_7)"
+msgstr "toutes les langues (utf-7)"
+
+#: src/pyams_utils/encoding.py:123
+msgid "all languages (utf_8)"
+msgstr "toutes les langues (utf-8)"
+
+#: src/pyams_utils/encoding.py:124
+msgid "all languages (utf_8_sig)"
+msgstr "toutes les langues (utf-8-sig)"
+
+#: src/pyams_utils/date.py:75
+msgid "%d/%m/%Y"
+msgstr "%d/%m/%Y"
+
+#: src/pyams_utils/date.py:76
+msgid "%d/%m/%Y - %H:%M"
+msgstr "%d/%m/%Y - %H:%M"
+
+#: src/pyams_utils/date.py:78
+msgid "on %d/%m/%Y"
+msgstr "le %d/%m/%Y"
+
+#: src/pyams_utils/date.py:79
+msgid "on %d/%m/%Y at %H:%M"
+msgstr "le %d/%m/%Y à %H:%M"
+
+#: src/pyams_utils/date.py:106
+#, c-format
+msgid "%d months ago"
+msgstr "Il y a %d mois"
+
+#: src/pyams_utils/date.py:135
+#, c-format
+msgid "%d months"
+msgstr "%d mois"
+
+#: src/pyams_utils/date.py:108
+#, c-format
+msgid "%d weeks ago"
+msgstr "Il y a %d semaines"
+
+#: src/pyams_utils/date.py:137
+#, c-format
+msgid "%d weeks"
+msgstr "%d semaines"
+
+#: src/pyams_utils/date.py:110
+#, c-format
+msgid "%d days ago"
+msgstr "Il y a %d jours"
+
+#: src/pyams_utils/date.py:112
+msgid "the day before yesterday"
+msgstr "avant-hier"
+
+#: src/pyams_utils/date.py:139
+#, c-format
+msgid "%d days"
+msgstr "%d jours"
+
+#: src/pyams_utils/date.py:114
+msgid "yesterday"
+msgstr "hier"
+
+#: src/pyams_utils/date.py:143
+#, c-format
+msgid "%d day and %d hours"
+msgstr "%d jours et %d heures"
+
+#: src/pyams_utils/date.py:146
+#, c-format
+msgid "%d hours"
+msgstr "%d heures"
+
+#: src/pyams_utils/date.py:118
+#, c-format
+msgid "%d hours ago"
+msgstr "Il y a %d heures"
+
+#: src/pyams_utils/date.py:122
+msgid "less than 5 minutes ago"
+msgstr "Il y a moins de 5 minutes"
+
+#: src/pyams_utils/date.py:150
+#, c-format
+msgid "%d minutes"
+msgstr "%d minutes"
+
+#: src/pyams_utils/date.py:152
+#, c-format
+msgid "%d seconds"
+msgstr "%d secondes"
+
+#: src/pyams_utils/date.py:120
+#, c-format
+msgid "%d minutes ago"
+msgstr "Il y a %d minutes"
+
+#: src/pyams_utils/size.py:61
+msgid "0.0## Gb"
+msgstr "0.0## Go"
+
+#: src/pyams_utils/size.py:53
+msgid "0 bytes"
+msgstr "0 octets"
+
+#: src/pyams_utils/size.py:56
+msgid "0.# Kb"
+msgstr "0.# Ko"
+
+#: src/pyams_utils/size.py:59
+msgid "0.0# Mb"
+msgstr "0.0# Mo"
+
+#: src/pyams_utils/schema.py:48
+msgid "Color length must be 3 or 6 characters"
+msgstr "La longueur d'une couleur doit être de 3 ou 6 caractères"
+
+#: src/pyams_utils/schema.py:51
+msgid ""
+"Color value must contain only valid hexadecimal color codes (numbers or "
+"letters between 'A' end 'F')"
+msgstr ""
+"Une couleur ne doit contenir que des valeurs hexadécimales correctes (nombres "
+"ou lettres de 'A' à 'F')"
+
+#: src/pyams_utils/timezone/interfaces.py:32
+msgid "Server timezone"
+msgstr "Fuseau horaire du serveur"
+
+#: src/pyams_utils/timezone/interfaces.py:33
+msgid "Default server timezone"
+msgstr "Fuseau horaire par défaut"
+
+#: src/pyams_utils/interfaces/text.py:36
+msgid "Renderer name"
+msgstr "Nom de l'outil de rendu"
+
+#: src/pyams_utils/browser/decimal.py:35
+msgid "The entered value is not a valid decimal literal."
+msgstr "La valeur saisie n'est pas une valeur décimale correcte."
+
+#~ msgid "Test"
+#~ msgstr "test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/locales/pyams_utils.pot	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,516 @@
+# 
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-01-24 16:53+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.8\n"
+
+#: ./src/pyams_utils/encoding.py:33
+msgid "English (ASCII)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:34
+msgid "Traditional Chinese (big5)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:35
+msgid "Traditional Chinese (big5hkscs)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:36
+msgid "English (cp037)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:37
+msgid "Hebrew (cp424)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:38
+msgid "English (cp437)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:39
+msgid "Western Europe (cp500)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:40
+msgid "Arabic (cp720)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:41
+msgid "Greek (cp737)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:42
+msgid "Baltic languages (cp775)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:43
+msgid "Western Europe (cp850)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:44
+msgid "Central and Eastern Europe (cp852)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:45
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:46
+msgid "Hebrew (cp856)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:47
+msgid "Turkish (cp857)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:48
+msgid "Western Europe (cp858)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:49
+msgid "Portuguese (cp860)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:50
+msgid "Icelandic (cp861)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:51
+msgid "Hebrew (cp862)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:52
+msgid "Canadian (cp863)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:53
+msgid "Arabic (cp864)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:54
+msgid "Danish, Norwegian (cp865)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:55
+msgid "Russian (cp866)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:56
+msgid "Greek (cp869)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:57
+msgid "Thai (cp874)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:58
+msgid "Greek (cp875)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:59
+msgid "Japanese (cp932)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:60
+msgid "Korean (cp949)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:61
+msgid "Traditional Chinese (cp950)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:62
+msgid "Urdu (cp1006)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:63
+msgid "Turkish (cp1026)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:64
+msgid "Western Europe (cp1140)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:65
+msgid "Central and Eastern Europe (cp1250)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:66
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:67
+msgid "Western Europe (cp1252)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:68
+msgid "Greek (cp1253)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:69
+msgid "Turkish (cp1254)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:70
+msgid "Hebrew (cp1255)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:71
+msgid "Arabic (cp1256)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:72
+msgid "Baltic languages (cp1257)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:73
+msgid "Vietnamese (cp1258)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:74
+msgid "Japanese (euc_jp)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:75
+msgid "Japanese (euc_jis_2004)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:76
+msgid "Japanese (euc_jisx0213)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:77
+msgid "Korean (euc_kr)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:78
+msgid "Simplified Chinese (gb2312)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:79
+msgid "Unified Chinese (gbk)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:80
+msgid "Unified Chinese (gb18030)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:81
+msgid "Simplified Chinese (hz)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:82
+msgid "Japanese (iso2022_jp)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:83
+msgid "Japanese (iso2022_jp_1)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:84
+msgid ""
+"Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:85
+msgid "Japanese (iso2022_jp_2004)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:86
+msgid "Japanese (iso2022_jp_3)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:87
+msgid "Japanese (iso2022_jp_ext)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:88
+msgid "Korean (iso2022_kr)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:89
+msgid "West Europe (latin_1)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:90
+msgid "Central and Eastern Europe (iso8859_2)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:91
+msgid "Esperanto, Maltese (iso8859_3)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:92
+msgid "Baltic languages (iso8859_4)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:93
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:94
+msgid "Arabic (iso8859_6)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:95
+msgid "Greek (iso8859_7)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:96
+msgid "Hebrew (iso8859_8)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:97
+msgid "Turkish (iso8859_9)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:98
+msgid "Nordic languages (iso8859_10)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:99
+msgid "Baltic languages (iso8859_13)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:100
+msgid "Celtic languages (iso8859_14)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:101
+msgid "Western Europe (iso8859_15)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:102
+msgid "South-Eastern Europe (iso8859_16)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:103
+msgid "Korean (johab)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:104
+msgid "Russian (koi8_r)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:105
+msgid "Ukrainian (koi8_u)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:106
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:107
+msgid "Greek (mac_greek)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:108
+msgid "Icelandic (mac_iceland)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:109
+msgid "Central and Eastern Europe (mac_latin2)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:110
+msgid "Western Europe (mac_roman)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:111
+msgid "Turkish (mac_turkish)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:112
+msgid "Kazakh (ptcp154)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:113
+msgid "Japanese (shift_jis)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:114
+msgid "Japanese (shift_jis_2004)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:115
+msgid "Japanese (shift_jisx0213)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:116
+msgid "all languages (utf_32)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:117
+msgid "all languages (utf_32_be)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:118
+msgid "all languages (utf_32_le)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:119
+msgid "all languages (utf_16)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:120
+msgid "all languages (BMP only - utf_16_be)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:121
+msgid "all languages (BMP only - utf_16_le)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:122
+msgid "all languages (utf_7)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:123
+msgid "all languages (utf_8)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:124
+msgid "all languages (utf_8_sig)"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:75
+msgid "%d/%m/%Y"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:76
+msgid "%d/%m/%Y - %H:%M"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:78
+msgid "on %d/%m/%Y"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:79
+msgid "on %d/%m/%Y at %H:%M"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:106
+#, c-format
+msgid "%d months ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:135
+#, c-format
+msgid "%d months"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:108
+#, c-format
+msgid "%d weeks ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:137
+#, c-format
+msgid "%d weeks"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:110
+#, c-format
+msgid "%d days ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:112
+msgid "the day before yesterday"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:139
+#, c-format
+msgid "%d days"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:114
+msgid "yesterday"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:143
+#, c-format
+msgid "%d day and %d hours"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:146
+#, c-format
+msgid "%d hours"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:118
+#, c-format
+msgid "%d hours ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:122
+msgid "less than 5 minutes ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:150
+#, c-format
+msgid "%d minutes"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:152
+#, c-format
+msgid "%d seconds"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:120
+#, c-format
+msgid "%d minutes ago"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:61
+msgid "0.0## Gb"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:53
+msgid "0 bytes"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:56
+msgid "0.# Kb"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:59
+msgid "0.0# Mb"
+msgstr ""
+
+#: ./src/pyams_utils/schema.py:48
+msgid "Color length must be 3 or 6 characters"
+msgstr ""
+
+#: ./src/pyams_utils/schema.py:51
+msgid ""
+"Color value must contain only valid hexadecimal color codes (numbers or "
+"letters between 'A' end 'F')"
+msgstr ""
+
+#: ./src/pyams_utils/timezone/interfaces.py:32
+msgid "Server timezone"
+msgstr ""
+
+#: ./src/pyams_utils/timezone/interfaces.py:33
+msgid "Default server timezone"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/text.py:36
+msgid "Renderer name"
+msgstr ""
+
+#: ./src/pyams_utils/browser/decimal.py:35
+msgid "The entered value is not a valid decimal literal."
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/property.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,105 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from pyams_utils.request import check_request, get_request_data, set_request_data
+from pyams_utils.session import get_session_data, set_session_data
+
+
+class cached(object):
+    """Custom property decorator to define a property or function
+    which is calculated only once
+       
+    When applied on a function, caching is based on input arguments
+    """
+
+    def __init__(self, function):
+        self._function = function
+        self._cache = {}
+
+    def __call__(self, *args):
+        try:
+            return self._cache[args]
+        except KeyError:
+            self._cache[args] = self._function(*args)
+            return self._cache[args]
+
+    def expire(self, *args):
+        del self._cache[args]
+
+
+class cached_property(object):
+    """A read-only @property decorator that is only evaluated once. The value is cached
+    on the object itself rather than the function or class; this should prevent
+    memory leakage.
+    """
+    def __init__(self, fget, doc=None):
+        self.fget = fget
+        self.__doc__ = doc or fget.__doc__
+        self.__name__ = fget.__name__
+        self.__module__ = fget.__module__
+
+    def __get__(self, obj, cls):
+        if obj is None:
+            return self
+        obj.__dict__[self.__name__] = result = self.fget(obj)
+        return result
+
+
+_marker = object()
+
+class request_property(object):
+    """Define a property stored in request annotations"""
+
+    def __init__(self, fget, key=None):
+        self.fget = fget
+        if key is None:
+            key = "%s.%s" % (fget.__module__, fget.__name__)
+        self.key = key
+
+    def __get__(self, obj, cls):
+        if obj is None:
+            return self
+        request = check_request()
+        data = get_request_data(request, self.key, _marker)
+        if data is _marker:
+            data = self.fget(obj)
+            set_request_data(request, self.key, data)
+        return data
+
+
+class session_property(object):
+    """Define a property stored into session"""
+
+    def __init__(self, fget, app, key=None):
+        self.fget = fget
+        self.app = app
+        if key is None:
+            key = "%s.%s" % (fget.__module__, fget.__name__)
+        self.key = key
+
+    def __get__(self, obj, cls):
+        if obj is None:
+            return self
+        request = check_request()
+        data = get_session_data(request, self.app, self.key, _marker)
+        if data is _marker:
+            data = self.fget(obj)
+            set_session_data(request, self.app, self.key, data)
+        return data
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/protocol/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/protocol/http.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,79 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import httplib2
+import urllib.parse
+
+# import interfaces
+
+# import packages
+
+
+class HTTPClient(object):
+    """HTTP client"""
+
+    def __init__(self, method, protocol, servername, url, params={}, credentials=(),
+                 proxy=(), rdns=True, proxy_auth=(), timeout=None, headers={}):
+        """Intialize HTTP connection"""
+        self.connection = None
+        self.method = method
+        self.protocol = protocol
+        self.servername = servername
+        self.url = url
+        self.params = params
+        self.location = None
+        self.credentials = credentials
+        self.proxy = proxy
+        self.rdns = rdns
+        self.proxy_auth = proxy_auth
+        self.timeout = timeout
+        self.headers = headers
+        if 'User-Agent' not in headers:
+            self.headers['User-Agent'] = 'PyAMS HTTP Client/1.0'
+
+    def get_response(self):
+        """Common HTTP request"""
+        if self.proxy:
+            proxy_info = httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP,
+                                            proxy_host=self.proxy[0],
+                                            proxy_port=self.proxy[1],
+                                            proxy_rdns=self.rdns,
+                                            proxy_user=self.proxy_auth and self.proxy_auth[0] or None,
+                                            proxy_pass=self.proxy_auth and self.proxy_auth[1] or None)
+        else:
+            proxy_info = None
+        http = httplib2.Http(timeout=self.timeout, proxy_info=proxy_info)
+        if self.credentials:
+            http.add_credentials(self.credentials[0], self.credentials[1])
+        uri = '%s://%s%s' % (self.protocol, self.servername, self.url)
+        if self.params:
+            uri += '?' + urllib.parse.urlencode(self.params)
+        response, content = http.request(uri, self.method, headers=self.headers)
+        return response, content
+
+
+def get_client(method, protocol, servername, url, params={}, credentials=(), proxy=(),
+               rdns=True, proxy_auth=(), timeout=None, headers={}):
+    """HTTP client factory"""
+    return HTTPClient(method, protocol, servername, url, params, credentials, proxy,
+                      rdns, proxy_auth, timeout, headers)
+
+
+def get_client_from_url(url, credentials=(), proxy=(), rdns=True, proxy_auth=(), timeout=None, headers={}):
+    """HTTP client factory from URL"""
+    elements = urllib.parse.urlparse(url)
+    return HTTPClient('GET', elements.scheme, elements.netloc, elements.path, elements.params,
+                      credentials, proxy, rdns, proxy_auth, timeout, headers)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/protocol/xmlrpc.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,144 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import base64
+import http.client
+import http.cookiejar
+import socket
+import urllib.request
+import xmlrpc.client
+
+# import interfaces
+
+# import packages
+
+
+class XMLRPCCookieAuthTransport(xmlrpc.client.Transport):
+    """An XML-RPC transport handling authentication via cookies"""
+
+    _http_connection = http.client.HTTPConnection
+
+    def __init__(self, user_agent, credentials=(), cookies=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, headers=None):
+        xmlrpc.client.Transport.__init__(self)
+        self.user_agent = user_agent
+        self.credentials = credentials
+        self.cookies = cookies
+        self.timeout = timeout
+        self.headers = headers
+
+    def make_connection(self, host):
+        # This is the make_connection that runs under Python 2.7 and newer.
+        # The code is pulled straight from 2.7 xmlrpclib, except replacing
+        # HTTPConnection with self._http_connection
+        if self._connection and host == self._connection[0]:
+            return self._connection[1]
+        chost, self._extra_headers, _x509 = self.get_host_info(host)
+        self._connection = host, self._http_connection(chost, timeout=self.timeout)
+        return self._connection[1]
+
+    # override the send_host hook to also send authentication info
+    def send_host(self, connection, host):
+        connection.putheader('Host', host)
+        if (self.cookies is not None) and (len(self.cookies) > 0):
+            for cookie in self.cookies:
+                connection.putheader('Cookie', '%s=%s' % (cookie.name, cookie.value))
+        elif self.credentials:
+            auth = 'Basic %s' % base64.encodebytes("%s:%s" % self.credentials).strip()
+            connection.putheader('Authorization', auth)
+
+    # send user agent
+    def send_user_agent(self, connection):
+        connection.putheader('User-Agent', self.user_agent)
+
+    # send custom headers
+    def send_headers(self, connection, headers):
+        xmlrpc.client.Transport.send_headers(self, connection, headers)
+        for k, v in (self.headers or {}).iteritems():
+            connection.putheader(k, v)
+
+    # dummy request class for extracting cookies
+    class CookieRequest(urllib.request.Request):
+        pass
+
+    # dummy response info headers helper
+    class CookieResponseHelper:
+        def __init__(self, response):
+            self.response = response
+        def getheaders(self, header):
+            return self.response.msg.getallmatchingheaders(header)
+
+    # dummy response class for extracting cookies
+    class CookieResponse:
+        def __init__(self, response):
+            self.response = response
+        def info(self):
+            return XMLRPCCookieAuthTransport.CookieResponseHelper(self.response)
+
+    def request(self, host, handler, request_body, verbose=False):
+        # issue XML-RPC request
+        connection = self.make_connection(host)
+        self.verbose = verbose
+        if verbose:
+            connection.set_debuglevel(1)
+        self.send_request(connection, handler, request_body)
+        self.send_host(connection, host)
+        self.send_user_agent(connection)
+        self.send_headers(connection)
+        self.send_content(connection, request_body)
+        # get response
+        return self.get_response(connection, host, handler)
+
+    def get_response(self, connection, host, handler):
+        response = connection.getresponse()
+        # extract cookies from response headers
+        if self.cookies is not None:
+            crequest = XMLRPCCookieAuthTransport.CookieRequest('http://%s/' % host)
+            cresponse = XMLRPCCookieAuthTransport.CookieResponse(response)
+            self.cookies.extract_cookies(cresponse, crequest)
+        if response.status != 200:
+            raise xmlrpc.client.ProtocolError(host + handler, response.status, response.reason, response.getheaders())
+        return self.parse_response(response)
+
+
+class SecureXMLRPCCookieAuthTransport(XMLRPCCookieAuthTransport):
+    """Secure XML-RPC transport"""
+
+    _http_connection = http.client.HTTPSConnection
+
+
+def get_client(uri, credentials=(), verbose=False, allow_none=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, headers=None):
+    """Get an XML-RPC client which supports basic authentication"""
+    if uri.startswith('https:'):
+        transport = SecureXMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS secure transport)', credentials,
+                                                    timeout=timeout, headers=headers)
+    else:
+        transport = XMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS basic transport)', credentials,
+                                              timeout=timeout, headers=headers)
+    return xmlrpc.client.Server(uri, transport=transport, verbose=verbose, allow_none=allow_none)
+
+
+def get_client_with_cookies(uri, credentials=(), verbose=False, allow_none=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
+                            headers=None, cookies=None):
+    """Get an XML-RPC client which supports authentication through cookies"""
+    if cookies is None:
+        cookies = http.cookiejar.CookieJar()
+    if uri.startswith('https:'):
+        transport = SecureXMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS secure cookie transport)',
+                                                    credentials, cookies, timeout, headers)
+    else:
+        transport = XMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS basic cookie transport)',
+                                              credentials, cookies, timeout, headers)
+    return xmlrpc.client.Server(uri, transport=transport, verbose=verbose, allow_none=allow_none)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/registry.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,123 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import venusian
+
+# import interfaces
+from zope.component.interfaces import ComponentLookupError
+
+# import packages
+from pyramid.threadlocal import get_current_registry, get_current_request
+from zope.interface import implementedBy, providedBy
+
+
+def _get_registries():
+    """Get list of component registries"""
+    registry = get_current_registry()
+    yield registry
+    request = get_current_request()
+    if (request is not None) and (request.registry != registry):
+        yield request.registry
+
+
+def registered_utilities():
+    """Get utilities registrations as generator"""
+    for registry in _get_registries():
+        for utility in registry.registeredUtilities():
+            yield utility
+
+
+def query_utility(provided, name='', default=None):
+    """Query utility registered with given interface"""
+    for registry in _get_registries():
+        utility = registry.queryUtility(provided, name, default)
+        if utility is not None:
+            return utility
+    return default
+
+
+def get_utility(provided, name=''):
+    """Get utility registered with given interface"""
+    for registry in _get_registries():
+        utility = registry.queryUtility(provided, name)
+        if utility is not None:
+            return utility
+    raise ComponentLookupError(provided, name)
+
+
+def get_utilities_for(interface):
+    """Get utilities registered with given interface as (name, util)"""
+    for registry in _get_registries():
+        for utility in registry.getUtilitiesFor(interface):
+            yield utility
+
+
+def get_all_utilities_registered_for(interface):
+    """Get list of registered utilities for given interface"""
+    result = []
+    for registry in _get_registries():
+        for utilities in registry.getAllUtilitiesRegisteredFor(interface):
+            result.extend(utilities)
+    return result
+
+
+class utility_config(object):
+    """Function or class decorator to declare a utility"""
+
+    venusian = venusian
+
+    def __init__(self, **settings):
+        self.__dict__.update(settings)
+
+    def __call__(self, wrapped):
+        settings = self.__dict__.copy()
+        depth = settings.pop('_depth', 0)
+
+        def callback(context, name, ob):
+            if type(ob) is type:
+                factory = ob
+                component = None
+            else:
+                factory = None
+                component = ob
+
+            provides = settings.get('provides')
+            if provides is None:
+                if factory:
+                    provides = list(implementedBy(factory))
+                else:
+                    provides = list(providedBy(component))
+                if len(provides) == 1:
+                    provides = provides[0]
+                else:
+                    raise TypeError("Missing 'provides' argument")
+
+            config = context.config.with_package(info.module)
+            config.registry.registerUtility(component=component, factory=factory,
+                                            provided=provides, name=settings.get('name', ''))
+
+        info = self.venusian.attach(wrapped, callback, category='pyams_utility',
+                                    depth=depth + 1)
+
+        if info.scope == 'class':
+            # if the decorator was attached to a method in a class, or
+            # otherwise executed at class scope, we need to set an
+            # 'attr' into the settings if one isn't already in there
+            if settings.get('attr') is None:
+                settings['attr'] = wrapped.__name__
+
+        settings['_info'] = info.codeinfo  # fbo "action_method"
+        return wrapped
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/request.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,80 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations
+from zope.security.interfaces import NoInteraction
+
+# import packages
+from pyramid.request import Request
+from pyramid.threadlocal import get_current_request
+from zope.interface import alsoProvides
+
+
+def get_request(raise_exception=True):
+    """Get current request
+
+    Raises a NoInteraction exception if there is no active request"""
+    request = get_current_request()
+    if (request is None) and raise_exception:
+        raise NoInteraction("No request")
+    return request
+
+
+def query_request():
+    """Query current request
+
+    Returns None if there is no active request"""
+    try:
+        return get_request()
+    except NoInteraction:
+        return None
+
+
+def check_request(path='/', environ=None, base_url=None, headers=None, POST=None, **kw):
+    """Get current request, or create a new blank one if missing"""
+    try:
+        return get_request()
+    except NoInteraction:
+        return Request.blank(path, environ, base_url, headers, POST, **kw)
+
+
+def get_annotations(request):
+    """Define 'annotations' request property"""
+    alsoProvides(request, IAttributeAnnotatable)
+    return IAnnotations(request)
+
+
+def get_debug(request):
+    """Define 'debug' request property"""
+    class Debug():
+        def __init__(self):
+            self.showTAL = False
+            self.sourceAnnotations = False
+    return Debug()
+
+
+def get_request_data(request, key, default=None):
+    """Get data associated with request"""
+    annotations = request.annotations
+    return annotations.get(key, default)
+
+
+def set_request_data(request, key, value):
+    """Associate data with request"""
+    annotations = request.annotations
+    annotations[key] = value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/schema.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,120 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import string
+
+# import interfaces
+from zope.schema.interfaces import ITextLine, IDecimal, IList, ITuple, IPassword
+
+# import Zope3 packages
+from zope.interface import implementer
+from zope.schema import TextLine, Decimal, List, Tuple, Password, ValidationError
+
+# import local packages
+
+from pyams_utils import _
+
+
+#
+# Encoded password field
+#
+
+class IEncodedPassword(IPassword):
+    """Encoded password field interface"""
+
+
+@implementer(IEncodedPassword)
+class EncodedPassword(Password):
+    """Encoded password field"""
+
+    _type = None
+
+    def fromUnicode(self, str):
+        return str
+
+    def constraint(self, value):
+        return True
+
+
+#
+# Color field
+#
+
+class IColorField(ITextLine):
+    """Marker interface for color fields"""
+
+
+@implementer(IColorField)
+class ColorField(TextLine):
+    """Color field"""
+
+    def __init__(self, *args, **kw):
+        super(ColorField, self).__init__(max_length=6, *args, **kw)
+
+    def _validate(self, value):
+        if len(value) not in (3, 6):
+            raise ValidationError(_("Color length must be 3 or 6 characters"))
+        for v in value:
+            if v not in string.hexdigits:
+                raise ValidationError(_("Color value must contain only valid hexadecimal color codes (numbers or "
+                                        "letters between 'A' end 'F')"))
+        super(ColorField, self)._validate(value)
+
+
+#
+# Pointed decimal field
+#
+
+class IDottedDecimalField(IDecimal):
+    """Marker interface for dotted decimal fields"""
+
+
+@implementer(IDottedDecimalField)
+class DottedDecimalField(Decimal):
+    """Dotted decimal field"""
+
+
+#
+# Dates range field
+#
+
+class IDatesRangeField(ITuple):
+    """Marker interface for dates range fields"""
+
+
+@implementer(IDatesRangeField)
+class DatesRangeField(Tuple):
+    """Dates range field"""
+
+    def __init__(self, value_type=None, unique=False, **kw):
+        super(DatesRangeField, self).__init__(value_type=None, unique=False,
+                                              min_length=2, max_length=2, **kw)
+
+
+#
+# TextLine list field
+#
+
+class ITextLineListField(IList):
+    """Marker interface for textline list field"""
+
+
+@implementer(ITextLineListField)
+class TextLineListField(List):
+    """TextLine list field"""
+
+    def __init__(self, value_type=None, unique=False, **kw):
+        super(TextLineListField, self).__init__(value_type=TextLine(), unique=True, **kw)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/scripts/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/scripts/zodb.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,46 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import optparse
+import sys
+import textwrap
+
+# import interfaces
+
+# import packages
+from pyams_utils.site import site_upgrade
+from pyramid.paster import bootstrap
+
+
+def upgrade_site():
+    """Check for site upgrade"""
+    usage = "usage: %prog config_uri"
+    description = """Check for database upgrade.
+                  Usage: pyams_upgrade production.ini
+                  """
+    parser = optparse.OptionParser(usage=usage,
+                                   description=textwrap.dedent(description))
+    options, args = parser.parse_args(sys.argv[1:])
+    if not len(args) >= 1:
+        print("You must provide at least one configuration file")
+        return 2
+    config_uri = args[0]
+    env = bootstrap(config_uri)
+    settings, closer = env['registry'].settings, env['closer']
+    try:
+        site_upgrade(env['request'])
+    finally:
+        closer()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/session.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def get_session_data(request, app, key, default=None):
+    """Get data associated with a given session"""
+    session = request.session
+    return session.get('{0}::{1}'.format(app, key), default)
+
+
+def set_session_data(request, app, key, value):
+    """Set data associated to a given session"""
+    session = request.session
+    session['{0}::{1}'.format(app, key)] = value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/site.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,167 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_utils.interfaces import PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME, \
+    PYAMS_APPLICATION_FACTORY_KEY
+from pyams_utils.interfaces.site import ISiteRoot, INewLocalSiteCreatedEvent, ISiteUpgradeEvent, ISiteGenerations, \
+    SITE_GENERATIONS_KEY, IConfigurationManager
+from zope.annotation.interfaces import IAnnotations
+from zope.component.interfaces import IPossibleSite, ISite, ObjectEvent
+from zope.traversing.interfaces import IBeforeTraverseEvent, ITraversable
+
+# import packages
+from persistent.dict import PersistentDict
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import get_utilities_for, query_utility
+from pyramid.events import subscriber
+from pyramid.exceptions import NotFound
+from pyramid.path import DottedNameResolver
+from pyramid.security import Allow, ALL_PERMISSIONS
+from pyramid.threadlocal import manager, get_current_registry
+from pyramid_zodbconn import get_connection
+from zope.container.folder import Folder
+from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.site import hooks
+from zope.site.site import LocalSiteManager, SiteManagerContainer
+
+
+@implementer(ISiteRoot, IConfigurationManager)
+class BaseSiteRoot(Folder, SiteManagerContainer):
+    """Default site root"""
+
+    __acl__ = [(Allow, 'system:admin', ALL_PERMISSIONS)]
+
+    config_klass = None
+
+
+@adapter_config(name='etc', context=ISiteRoot, provides=ITraversable)
+class SiteRootEtcTraverser(object):
+    """Site root ++etc++ namespace traverser"""
+
+    def __init__(self, context):
+        self.context = context
+
+    def traverse(self, name, furtherpath=None):
+        if name == 'site':
+            return self.context.getSiteManager()
+        raise NotFound
+
+
+@implementer(INewLocalSiteCreatedEvent)
+class NewLocalSiteCreatedEvent(ObjectEvent):
+    """New local site created event"""
+
+
+def site_factory(request):
+    """Build a new site including registered utilities"""
+    conn = get_connection(request)
+    root = conn.root()
+    application_key = request.registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY,
+                                                    PYAMS_APPLICATION_DEFAULT_NAME)
+    application = root.get(application_key)
+    if application is None:
+        factory = request.registry.settings.get(PYAMS_APPLICATION_FACTORY_KEY)
+        if factory:
+            resolver = DottedNameResolver()
+            factory = resolver.maybe_resolve(factory)
+        else:
+            factory = BaseSiteRoot
+        application = root[application_key] = factory()
+        if IPossibleSite.providedBy(application):
+            sm = LocalSiteManager(application, default_folder=False)
+            application.setSiteManager(sm)
+        try:
+            # if some components require a valid and complete registry
+            # with all registered utilities, they can subscribe to
+            # INewLocalSiteCreatedEvent event interface
+            hooks.setSite(application)
+            get_current_registry().notify(NewLocalSiteCreatedEvent(application))
+        finally:
+            hooks.setSite(None)
+        import transaction
+        transaction.commit()
+    return application
+
+
+@implementer(ISiteUpgradeEvent)
+class SiteUpgradeEvent(ObjectEvent):
+    """Site upgrade request event"""
+
+
+def site_upgrade(request):
+    """Upgrade site when needed
+
+    This function is executed by pyams_upgrade console script.
+    Site generations are registered as named utilities providing
+    ISiteGenerations interface.
+    Current site generations are stored into annotations.
+    """
+    application = site_factory(request)
+    if application is not None:
+        try:
+            hooks.setSite(application)
+            annotations = IAnnotations(application)
+            generations = annotations.get(SITE_GENERATIONS_KEY)
+            if generations is None:
+                generations = annotations[SITE_GENERATIONS_KEY] = PersistentDict()
+            for name, utility in get_utilities_for(ISiteGenerations):
+                if not name:
+                    name = '.'.join((utility.__module__, utility.__class__.__name__))
+                current = generations.get(name)
+                if (not current) or (current < utility.generation):
+                    print("Upgrading {0} from generation {1} to {2}...".format(name, current, utility.generation))
+                utility.evolve(application, current)
+                generations[name] = utility.generation
+        finally:
+            hooks.setSite(None)
+        import transaction
+        transaction.commit()
+    return application
+
+
+@subscriber(IBeforeTraverseEvent, context_selector=ISite)
+def handle_site_before_traverse(event):
+    """Push registry and request to threadlocal manager when an
+    object implementing ISite is traversed
+    """
+    manager.push({'registry': event.object.getSiteManager(),
+                  'request': event.request})
+    hooks.setSite(event.object)
+
+
+def check_required_utilities(site, utilities):
+    """Utility function to check for required utilities
+
+    utilities argument is a tuple made of:
+    - the utility interface
+    - the utility name
+    - the utility factory
+    - the default name when creating the utility
+    """
+    registry = get_current_registry()
+    for interface, name, factory, default_id in utilities:
+        utility = query_utility(interface, name=name)
+        if utility is None:
+            sm = site.getSiteManager()
+            if default_id in sm:
+                continue
+            utility = factory()
+            registry.notify(ObjectCreatedEvent(utility))
+            sm[default_id] = utility
+            sm.registerUtility(utility, interface, name=name)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/size.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,59 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from babel.core import Locale
+from babel.numbers import format_decimal
+from pyams_utils.request import check_request
+
+from pyams_utils import _
+
+
+def get_human_size(value, request=None):
+    """Convert given bytes value in human readable format
+
+    >>> from pyramid.testing import DummyRequest
+    >>> request = DummyRequest(params={'_LOCALE_': 'fr'})
+    >>> request.locale_name
+    'fr'
+
+    >>> from pyams_utils.size import get_human_size
+    >>> get_human_size(256, request)
+    '256 bytes'
+    >>> get_human_size(3678, request)
+    '3,6 Kb'
+    >>> get_human_size(6785342, request)
+    '6,47 Mb'
+    >>> get_human_size(3674815342, request)
+    '3,422 Gb'
+    """
+    if request is None:
+        request = check_request()
+    translate = request.localizer.translate
+    locale = Locale(request.locale_name)
+    if value < 1024:
+        return format_decimal(value, translate(_('0 bytes')), locale)
+    value /= 1024
+    if value < 1024:
+        return format_decimal(value, translate(_('0.# Kb')), locale)
+    value /= 1024
+    if value < 1024:
+        return format_decimal(value, translate(_('0.0# Mb')), locale)
+    value /= 1024
+    return format_decimal(value, translate(_('0.0## Gb')), locale)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/tales.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,80 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import re
+
+# import interfaces
+from pyams_utils.interfaces.tales import ITALESExtension
+
+# import packages
+from chameleon.astutil import Symbol
+from chameleon.codegen import template
+from chameleon.tales import StringExpr
+from zope.contentprovider.tales import addTALNamespaceData
+
+
+class ContextExprMixin(object):
+    """Mixin-class for expression compilers."""
+
+    transform = None
+
+    def __call__(self, target, engine):
+        # Make call to superclass to assign value to target
+        assignment = super(ContextExprMixin, self).__call__(target, engine)
+        transform = template("target = transform(econtext, target)",
+                             target=target,
+                             transform=self.transform)
+        return assignment + transform
+
+
+FUNCTION_EXPRESSION = re.compile('(.+)\((.+)\)')
+
+
+def render_extension(econtext, name):
+    name = name.strip()
+
+    context = econtext.get('context')
+    request = econtext.get('request')
+    view = econtext.get('view')
+
+    func_match = FUNCTION_EXPRESSION.match(name)
+    if func_match:
+        name, argument = func_match.groups()
+        arg_value = econtext.get(argument, argument)
+    else:
+        arg_value = None
+
+    registry = request.registry
+    extension = registry.queryMultiAdapter((context, request, view), ITALESExtension, name=name)
+    if extension is None:
+        extension = registry.queryMultiAdapter((context, request), ITALESExtension, name=name)
+    if extension is None:
+        extension = registry.queryAdapter(context, ITALESExtension, name=name)
+
+    # provide a useful error message, if the extension was not found.
+    if extension is None:
+        return None
+
+    # Insert the data gotten from the context
+    addTALNamespaceData(extension, econtext)
+
+    return extension.render(arg_value)
+
+
+class ExtensionExpr(ContextExprMixin, StringExpr):
+    """extension: TALES expression"""
+
+    transform = Symbol(render_extension)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/tests/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/tests/test_utilsdocs.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,62 @@
+### -*- coding: utf-8 -*- ####################################################
+##############################################################################
+#
+# Copyright (c) 2008-2010 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+"""
+Generic Test case for ztfy.utils doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.dirname(__file__)
+
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
+    """Returns a test suite, based on doctests found in /doctest."""
+    suite = []
+    if globs is None:
+        globs = globals()
+
+    flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+             doctest.REPORT_ONLY_FIRST_FAILURE)
+
+    package_dir = os.path.split(test_dir)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+
+    doctest_dir = os.path.join(package_dir, 'doctests')
+
+    # filtering files on extension
+    docs = [os.path.join(doctest_dir, doc) for doc in
+            os.listdir(doctest_dir) if doc.endswith('.txt')]
+
+    for test in docs:
+        suite.append(doctest.DocFileSuite(test, optionflags=flags,
+                                          globs=globs, setUp=setUp,
+                                          tearDown=tearDown,
+                                          module_relative=False))
+
+    return unittest.TestSuite(suite)
+
+def test_suite():
+    """returns the test suite"""
+    return doc_suite(current_dir)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/tests/test_utilsdocstrings.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,65 @@
+### -*- coding: utf-8 -*- ####################################################
+##############################################################################
+#
+# Copyright (c) 2008-2010 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+"""
+Generic Test case for pyams_utils doc strings
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+
+def doc_suite(test_dir, globs=None):
+    """Returns a test suite, based on doc tests strings found in /*.py"""
+    suite = []
+    if globs is None:
+        globs = globals()
+
+    flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+             doctest.REPORT_ONLY_FIRST_FAILURE)
+
+    package_dir = os.path.split(test_dir)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+
+    # filtering files on extension
+    docs = [doc for doc in
+            os.listdir(package_dir) if doc.endswith('.py')]
+    docs = [doc for doc in docs if not doc.startswith('__')]
+
+    for test in docs:
+        fd = open(os.path.join(package_dir, test))
+        content = fd.read()
+        fd.close()
+        if '>>> ' not in content:
+            continue
+        test = test.replace('.py', '')
+        location = 'pyams_utils.%s' % test
+        suite.append(doctest.DocTestSuite(location, optionflags=flags,
+                                          globs=globs))
+
+    return unittest.TestSuite(suite)
+
+def test_suite():
+    """returns the test suite"""
+    return doc_suite(current_dir)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/text.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,140 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import html
+import docutils.core
+
+# import interfaces
+from pyams_utils.interfaces.tales import ITALESExtension
+from pyams_utils.interfaces.text import IHTMLRenderer
+from pyramid.interfaces import IRequest
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
+from pyams_utils.request import check_request
+from zope.component import adapter
+from zope.interface import implementer, provider, Interface
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+
+def get_text_start(text, length, max=0):
+    """Get first words of given text with maximum given length
+    
+    If @max is specified, text is shortened only if remaining text is longer than @max
+    
+    @param text: initial text
+    @param length: maximum length of resulting text
+    @param max: if > 0, @text is shortened only if remaining text is longer than max
+
+    >>> from pyams_utils.text import get_text_start
+    >>> get_text_start('This is a long string', 10)
+    'This is a&#133;'
+    >>> get_text_start('This is a long string', 20)
+    'This is a long&#133;'
+    >>> get_text_start('This is a long string', 20, 7)
+    'This is a long string'
+    """
+    result = text or ''
+    if length > len(result):
+        return result
+    index = length - 1
+    text_length = len(result)
+    while (index > 0) and (result[index] != ' '):
+        index -= 1
+    if (index > 0) and (text_length > index + max):
+        return result[:index] + '&#133;'
+    return text
+
+
+@adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer)
+class BaseHTMLRenderer(object):
+    """Raw text renderer utility class"""
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def render(self, **kwargs):
+        return self.context
+
+
+@adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer)
+class TextRenderer(BaseHTMLRenderer):
+    """Render raw text to HTML"""
+
+    def render(self, **kwargs):
+        return html.escape(self.context).replace('\n', '<br />\n')
+
+
+@adapter_config(name='rest', context=(str, IRequest), provides=IHTMLRenderer)
+class ReStructuredTextRenderer(BaseHTMLRenderer):
+    """Render reStructuredText to HTML"""
+
+    def render(self, **kwargs):
+        """Render reStructuredText to HTML"""
+        overrides = {
+            'halt_level': 6,
+            'input_encoding': 'unicode',
+            'output_encoding': 'unicode',
+            'initial_header_level': 3,
+        }
+        if 'settings' in kwargs:
+            overrides.update(kwargs['settings'])
+        parts = docutils.core.publish_parts(self.context,
+                                            writer_name='html',
+                                            settings_overrides=overrides)
+        return ''.join((parts['body_pre_docinfo'], parts['docinfo'], parts['body']))
+
+
+def text_to_html(text, renderer='text'):
+    """Convert text to HTML using the given renderer"""
+    request = check_request()
+    registry = request.registry
+    renderer = registry.queryMultiAdapter((text, request), IHTMLRenderer, name=renderer)
+    if renderer is not None:
+        return renderer.render()
+
+
+@adapter_config(name='html', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class HTMLTalesExtension(ContextRequestViewAdapter):
+    """extension:html TALES expression"""
+
+    def render(self, context):
+        if context is None:
+            context = self.context
+        renderer = self.request.registry.queryMultiAdapter((context, self.request, self.view), IHTMLRenderer)
+        if renderer is not None:
+            return renderer.render()
+        elif isinstance(context, str):
+            return text_to_html(context, 'text')
+        else:
+            return str(context)
+
+
+@provider(IVocabularyFactory)
+class RenderersVocabulary(SimpleVocabulary):
+    """Text renderers vocabulary"""
+
+    def __init__(self):
+        request = check_request()
+        registry = request.registry
+        translate = registry.localizer.translate
+        terms = [SimpleTerm(name, name, translate(adapt.title).label)
+                 for name, adapt in registry.getAdapters(('', request), IHTMLRenderer)]
+        super(RenderersVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS HTML renderers', RenderersVocabulary)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/timezone/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,68 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from datetime import datetime
+
+import pytz
+
+
+# import interfaces
+from pyams_utils.interfaces.timezone import IServerTimezone
+from pyramid.interfaces import IRequest
+from zope.interface.common.idatetime import ITZInfo
+
+# import packages
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import query_utility
+
+
+GMT = pytz.timezone('GMT')
+_tz = pytz.timezone('Europe/Paris')
+tz = _tz
+
+
+@adapter_config(context=IRequest, provides=ITZInfo)
+def tzinfo(request=None):
+    """request to timezone adapter
+
+    There is no easy way to get timezone from a request.
+    This adapter assumes that the timezone is given by
+    a registered utility...
+    """
+    util = query_utility(IServerTimezone)
+    if util is not None:
+        return pytz.timezone(util.timezone)
+    return GMT
+
+
+def tztime(value):
+    if not value:
+        return None
+    if not isinstance(value, datetime):
+        return value
+    if not value.tzinfo:
+        value = GMT.localize(value)
+    return value.astimezone(tzinfo())
+
+
+def gmtime(value):
+    if not value:
+        return None
+    if not isinstance(value, datetime):
+        return value
+    if not value.tzinfo:
+        value = GMT.localize(value)
+    return value.astimezone(GMT)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/timezone/utility.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,57 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_utils.interfaces.site import ISiteGenerations
+from pyams_utils.interfaces.timezone import IServerTimezone
+from zope.site.interfaces import INewLocalSite
+
+# import packages
+from persistent import Persistent
+from pyams_utils.registry import utility_config
+from pyams_utils.site import check_required_utilities
+from pyramid.events import subscriber
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(IServerTimezone)
+class ServerTimezoneUtility(Persistent, Contained):
+
+    timezone = FieldProperty(IServerTimezone['timezone'])
+
+
+REQUIRED_UTILITIES = ((IServerTimezone, '', ServerTimezoneUtility, 'Server timezone'),)
+
+
+@subscriber(INewLocalSite)
+def handle_new_local_site(event):
+    """Create a new ServerTimezoneUtility when a site is created"""
+    site = event.manager.__parent__
+    check_required_utilities(site, REQUIRED_UTILITIES)
+
+
+@utility_config(name='PyAMS timezone checker', provides=ISiteGenerations)
+class TimezoneGenerationsChecker(object):
+    """Timezone generations checker"""
+
+    generation = 1
+
+    def evolve(self, site, current=None):
+        """Check for required utilities"""
+        check_required_utilities(site, REQUIRED_UTILITIES)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/timezone/vocabulary.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import pytz
+
+# import interfaces
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from zope.interface import provider
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary, getVocabularyRegistry
+
+
+@provider(IVocabularyFactory)
+class TimezonesVocabulary(SimpleVocabulary):
+    """Timezones vocabulary"""
+
+    def __init__(self, *args, **kw):
+        terms = [SimpleTerm(t, t, t) for t in pytz.all_timezones]
+        super(TimezonesVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS timezones', TimezonesVocabulary)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/traversing.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,174 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyramid.interfaces import VH_ROOT_KEY
+from zope.traversing.interfaces import ITraversable, BeforeTraverseEvent
+
+# import packages
+from pyramid.compat import decode_path_info, is_nonstr_iter
+from pyramid.exceptions import URLDecodeError, NotFound
+from pyramid.threadlocal import get_current_registry
+from pyramid.traversal import ResourceTreeTraverser, slash, split_path_info, empty
+from zope.interface import Interface
+
+
+class NamespaceTraverser(ResourceTreeTraverser):
+    """Custom traverser handling views and namespaces
+
+    This is an upgraded version of native Pyramid traverser.
+    It adds:
+     - a new BeforeTraverseEvent before traversing each object in the path
+     - support for namespaces with "++" notation
+    """
+
+    NAMESPACE_SELECTOR = '++'
+
+    def __call__(self, request):
+
+        environ = request.environ
+        matchdict = request.matchdict
+
+        if matchdict is not None:
+            path = matchdict.get('traverse', slash) or slash
+            if is_nonstr_iter(path):
+                # this is a *traverse stararg (not a {traverse})
+                # routing has already decoded these elements, so we just
+                # need to join them
+                path = '/' + slash.join(path) or slash
+
+            subpath = matchdict.get('subpath', ())
+            if not is_nonstr_iter(subpath):
+                # this is not a *subpath stararg (just a {subpath})
+                # routing has already decoded this string, so we just need
+                # to split it
+                subpath = split_path_info(subpath)
+
+        else:
+            subpath = ()
+            try:
+                # empty if mounted under a path in mod_wsgi, for example
+                path = request.path_info or slash
+            except KeyError:
+                # if environ['PATH_INFO'] is just not there
+                path = slash
+            except UnicodeDecodeError as e:
+                raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
+
+        if VH_ROOT_KEY in environ:
+            # HTTP_X_VHM_ROOT
+            vroot_path = decode_path_info(environ[VH_ROOT_KEY])
+            vroot_tuple = split_path_info(vroot_path)
+            vpath = vroot_path + path
+            vroot_idx = len(vroot_tuple) - 1
+        else:
+            vroot_tuple = ()
+            vpath = path
+            vroot_idx = -1
+
+        root = self.root
+        ob = vroot = root
+
+        request.registry.notify(BeforeTraverseEvent(root, request))
+
+        if vpath == slash:
+            # invariant: vpath must not be empty
+            # prevent a call to traversal_path if we know it's going
+            # to return the empty tuple
+            vpath_tuple = ()
+
+        else:
+            # we do dead reckoning here via tuple slicing instead of
+            # pushing and popping temporary lists for speed purposes
+            # and this hurts readability; apologies
+            i = 0
+            view_selector = self.VIEW_SELECTOR
+            ns_selector = self.NAMESPACE_SELECTOR
+            vpath_tuple = split_path_info(vpath)
+
+            for segment in vpath_tuple:
+                request.registry.notify(BeforeTraverseEvent(ob, request))
+
+                if segment[:2] == view_selector:
+                    return {'context': ob,
+                            'view_name': segment[2:],
+                            'subpath': vpath_tuple[i + 1:],
+                            'traversed': vpath_tuple[:vroot_idx + i + 1],
+                            'virtual_root': vroot,
+                            'virtual_root_path': vroot_tuple,
+                            'root': root}
+
+                if segment[:2] == ns_selector:
+                    ns, name = segment[2:].split(ns_selector, 1)
+                    registry = get_current_registry()
+                    traverser = registry.queryMultiAdapter((ob, request), ITraversable, ns)
+                    if traverser is None:
+                        traverser = registry.queryAdapter(ob, ITraversable, ns)
+                    if traverser is None:
+                        traverser = registry.queryAdapter(request, ITraversable, ns)
+                    if traverser is None:
+                        raise NotFound()
+                    ob = traverser.traverse(name, vpath_tuple[vroot_idx + i + 1:])
+                    i += 1
+                    continue
+
+                try:
+                    getitem = ob.__getitem__
+                except AttributeError:
+                    return {'context': ob,
+                            'view_name': segment,
+                            'subpath': vpath_tuple[i + 1:],
+                            'traversed': vpath_tuple[:vroot_idx + i + 1],
+                            'virtual_root': vroot,
+                            'virtual_root_path': vroot_tuple,
+                            'root': root}
+
+                try:
+                    next = getitem(segment)
+                except KeyError:
+                    return {'context': ob,
+                            'view_name': segment,
+                            'subpath': vpath_tuple[i + 1:],
+                            'traversed': vpath_tuple[:vroot_idx + i + 1],
+                            'virtual_root': vroot,
+                            'virtual_root_path': vroot_tuple,
+                            'root': root}
+                if i == vroot_idx:
+                    vroot = next
+                ob = next
+                i += 1
+
+        return {'context': ob,
+                'view_name': empty,
+                'subpath': subpath,
+                'traversed': vpath_tuple,
+                'virtual_root': vroot,
+                'virtual_root_path': vroot_tuple,
+                'root': root}
+
+
+def get_parent(context, interface=Interface, allow_context=True):
+    """Get first parent of the context that implements given interface"""
+    if allow_context:
+        parent = context
+    else:
+        parent = getattr(context, '__parent__', None)
+    while parent is not None:
+        if interface.providedBy(parent):
+            return interface(parent)
+        parent = getattr(parent, '__parent__', None)
+    return None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/unicode.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,173 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import codecs
+import string
+
+# import interfaces
+
+# import packages
+
+
+_unicodeTransTable = {}
+def _fillUnicodeTransTable():
+    _corresp = [
+        ("A", [0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104]),
+        ("AE", [0x00C6]),
+        ("a", [0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x0101, 0x0103, 0x0105]),
+        ("ae", [0x00E6]),
+        ("C", [0x00C7, 0x0106, 0x0108, 0x010A, 0x010C]),
+        ("c", [0x00E7, 0x0107, 0x0109, 0x010B, 0x010D]),
+        ("D", [0x00D0, 0x010E, 0x0110]),
+        ("d", [0x00F0, 0x010F, 0x0111]),
+        ("E", [0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0112, 0x0114, 0x0116, 0x0118, 0x011A]),
+        ("e", [0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0113, 0x0115, 0x0117, 0x0119, 0x011B]),
+        ("G", [0x011C, 0x011E, 0x0120, 0x0122]),
+        ("g", [0x011D, 0x011F, 0x0121, 0x0123]),
+        ("H", [0x0124, 0x0126]),
+        ("h", [0x0125, 0x0127]),
+        ("I", [0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x0128, 0x012A, 0x012C, 0x012E, 0x0130]),
+        ("i", [0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x0129, 0x012B, 0x012D, 0x012F, 0x0131]),
+        ("IJ", [0x0132]),
+        ("ij", [0x0133]),
+        ("J", [0x0134]),
+        ("j", [0x0135]),
+        ("K", [0x0136]),
+        ("k", [0x0137, 0x0138]),
+        ("L", [0x0139, 0x013B, 0x013D, 0x013F, 0x0141]),
+        ("l", [0x013A, 0x013C, 0x013E, 0x0140, 0x0142]),
+        ("N", [0x00D1, 0x0143, 0x0145, 0x0147, 0x014A]),
+        ("n", [0x00F1, 0x0144, 0x0146, 0x0148, 0x0149, 0x014B]),
+        ("O", [0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D8, 0x014C, 0x014E, 0x0150]),
+        ("o", [0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F8, 0x014D, 0x014F, 0x0151]),
+        ("OE", [0x0152]),
+        ("oe", [0x0153]),
+        ("R", [0x0154, 0x0156, 0x0158]),
+        ("r", [0x0155, 0x0157, 0x0159]),
+        ("S", [0x015A, 0x015C, 0x015E, 0x0160]),
+        ("s", [0x015B, 0x015D, 0x015F, 0x01610, 0x017F]),
+        ("T", [0x0162, 0x0164, 0x0166]),
+        ("t", [0x0163, 0x0165, 0x0167]),
+        ("U", [0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0168, 0x016A, 0x016C, 0x016E, 0x0170, 0x172]),
+        ("u", [0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x016D, 0x016F, 0x0171]),
+        ("W", [0x0174]),
+        ("w", [0x0175]),
+        ("Y", [0x00DD, 0x0176, 0x0178]),
+        ("y", [0x00FD, 0x00FF, 0x0177]),
+        ("Z", [0x0179, 0x017B, 0x017D]),
+        ("z", [0x017A, 0x017C, 0x017E])
+    ]
+    for char, codes in _corresp:
+        for code in codes:
+            _unicodeTransTable[code] = char
+
+_fillUnicodeTransTable()
+
+
+def translate_string(s, escape_slashes=False, force_lower=True, spaces=' ', keep_chars='_-.'):
+    """Remove extended characters from string and replace them with 'basic' ones
+    
+    @param s: text to be cleaned.
+    @type s: str or unicode
+    @param escape_slashes: if True, slashes are also converted
+    @type escape_slashes: boolean
+    @param force_lower: if True, result is automatically converted to lower case
+    @type force_lower: boolean
+    @return: text without diacritics
+    @rtype: unicode
+    """
+    if escape_slashes:
+        s = s.replace("\\", "/").split("/")[-1]
+    s = s.strip()
+    if isinstance(s, bytes):
+        s = s.decode("utf-8", "replace")
+    s = s.translate(_unicodeTransTable)
+    s = ''.join([a for a in s.translate(_unicodeTransTable)
+                 if a.replace(' ', '-') in (string.ascii_letters + string.digits + (keep_chars or ''))])
+    if force_lower:
+        s = s.lower()
+    if spaces != ' ':
+        s = s.replace(' ', spaces)
+    return s
+
+
+def nvl(value, default=''):
+    """Get specified value, or an empty string if value is empty
+    
+    @param value: text to be checked
+    @param default: default value
+    @return: value, or default if value is empty
+    """
+    return value or default
+
+
+def uninvl(value, default='', encoding='utf-8'):
+    """Get specified value converted to unicode, or an empty unicode string if value is empty
+    
+    @param value: text to be checked
+    @type value: str or unicode
+    @param default: default value
+    @return: value, or default if value is empty
+    @rtype: unicode
+    """
+    if isinstance(value, str):
+        return value
+    try:
+        return codecs.decode(value or default, encoding)
+    except:
+        return codecs.decode(value or default, 'latin1')
+
+
+def unidict(value, encoding='utf-8'):
+    """Get specified dict with values converted to unicode
+    
+    @param value: input dict of strings which may be converted to unicode
+    @type value: dict
+    @return: input dict converted to unicode
+    @rtype: dict
+    """
+    result = {}
+    for key in value:
+        result[key] = uninvl(value[key], encoding)
+    return result
+
+
+def unilist(value, encoding='utf-8'):
+    """Get specified list with values converted to unicode
+    
+    @param value: input list of strings which may be converted to unicode
+    @type value: list
+    @return: input list converted to unicode
+    @rtype: list
+    """
+    if not isinstance(value, (list, tuple)):
+        return uninvl(value, encoding)
+    return [uninvl(v, encoding) for v in value]
+
+
+def encode(value, encoding='utf-8'):
+    """Encode given Unicode value to bytes with given encoding"""
+    return value.encode(encoding) if isinstance(value, str) else value
+
+
+def utf8(value):
+    """Encode given unicode value to UTF-8 encoded bytes"""
+    return encode(value, 'utf-8')
+
+
+def decode(value, encoding='utf-8'):
+    """Decode given bytes value to unicode with given encoding"""
+    return value.decode(encoding) if isinstance(value, bytes) else value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/url.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,48 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from persistent.interfaces import IPersistent
+from pyams_utils.interfaces.tales import ITALESExtension
+
+# import packages
+from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
+from pyramid.url import resource_url
+from zope.interface import Interface
+
+
+def absolute_url(context, request, view_name=None):
+    """Get resource absolute_url"""
+    result = resource_url(context, request)
+    if result.endswith('/'):
+        result = result[:-1]
+    if view_name:
+        if view_name.startswith('#'):
+            result += view_name
+        else:
+            result += '/' + view_name
+    return result
+
+
+@adapter_config(name='absolute_url', context=(IPersistent, Interface, Interface), provides=ITALESExtension)
+class AbsoluteUrlTalesExtension(ContextRequestViewAdapter):
+    """extension:absolute_url(context) TALES extension"""
+
+    def render(self, context):
+        if context is None:
+            context = self.context
+        return absolute_url(context, self.request)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/views/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/views/decimal.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import decimal
+
+# import interfaces
+from z3c.form.interfaces import IWidget, IDataConverter
+
+# import packages
+from pyams_utils.adapter import adapter_config
+from pyams_utils.schema import IDottedDecimalField
+from z3c.form.converter import BaseDataConverter, FormatterValidationError
+
+from pyams_utils import _
+
+
+@adapter_config(context=(IDottedDecimalField, IWidget), provides=IDataConverter)
+class DottedDecimalDataConverter(BaseDataConverter):
+    """Dotted decimal field data converter"""
+
+    errorMessage = _('The entered value is not a valid decimal literal.')
+
+    def __init__(self, field, widget):
+        super(DottedDecimalDataConverter, self).__init__(field, widget)
+
+    def toWidgetValue(self, value):
+        if not value:
+            return self.field.missing_value
+        return value
+
+    def toFieldValue(self, value):
+        if value is self.field.missing_value:
+            return ''
+        if not value:
+            return None
+        try:
+            return decimal.Decimal(value)
+        except decimal.InvalidOperation:
+            raise FormatterValidationError(self.errorMessage, value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/wsgi.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,42 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def wsgi_environ_cache(*names):
+    """Wrap a function/method to cache its result for call into request.environ
+
+    :param list[string] names: keys to cache into environ, the len(names) must
+    be equal to the result's length or scalar
+    :return:
+    """
+    def decorator(fn):
+        def function_wrapper(self, request):
+            scalar = len(names) == 1
+            try:
+                rs = [request.environ[cached_key] for cached_key in names]
+            except KeyError:
+                rs = fn(self, request)
+                if scalar:
+                    rs = [rs, ]
+                request.environ.update(zip(names, rs))
+            return rs[0] if scalar else rs
+        return function_wrapper
+
+    return decorator
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/zmi/__init__.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/zmi/configure.zcml	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,6 @@
+<configure
+		xmlns="http://pylonshq.com/pyramid">
+
+	<include package="pyramid_zcml" />
+
+</configure>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/zmi/timezone.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,53 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces.timezone import IServerTimezone
+
+# import packages
+from pyams_form.form import AJAXEditForm
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_zmi.form import AdminDialogEditForm
+from pyramid.view import view_config
+from z3c.form import field
+
+from pyams_utils import _
+
+
+@pagelet_config(name='properties.html', context=IServerTimezone, layer=IPyAMSLayer,
+                permission='system.view')
+class ServerTimezonePropertiesEditForm(AdminDialogEditForm):
+    """Server timezone properties edit form"""
+
+    legend = _("Update server timezone properties")
+    fields = field.Fields(IServerTimezone)
+    ajax_handler = 'properties.json'
+
+    @property
+    def title(self):
+        return self.context.__name__
+
+    def updateWidgets(self, prefix=None):
+        super(ServerTimezonePropertiesEditForm, self).updateWidgets()
+        self.widgets['timezone'].addClass('select2')
+
+
+@view_config(name='properties.json', context=IServerTimezone, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class ServerTimezonePropertiesAJAXEditForm(AJAXEditForm, ServerTimezonePropertiesEditForm):
+    """Server timezone properties edit form, AJAX renderer"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/zodb.py	Thu Feb 19 00:46:48 2015 +0100
@@ -0,0 +1,55 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from persistent.interfaces import IPersistent
+from transaction.interfaces import ITransactionManager
+from ZODB.interfaces import IConnection
+
+# import packages
+from pyams_utils.adapter import adapter_config
+
+
+@adapter_config(context=IPersistent, provides=IConnection)
+def get_connection(obj):
+    """An adapter which gets a ZODB connection of a persistent object.
+
+    We are assuming the object has a parent if it has been created in
+    this transaction.
+
+    Raises ValueError if it is impossible to get a connection.
+    """
+    cur = obj
+    while not getattr(cur, '_p_jar', None):
+        cur = getattr(cur, '__parent__', None)
+        if cur is None:
+            return None
+    return cur._p_jar
+
+
+# IPersistent adapters copied from zc.twist package
+# also register this for adapting from IConnection
+@adapter_config(context=IPersistent, provides=ITransactionManager)
+def get_transaction_manager(obj):
+    conn = IConnection(obj)  # typically this will be
+                             # zope.app.keyreference.persistent.connectionOfPersistent
+    try:
+        return conn.transaction_manager
+    except AttributeError:
+        return conn._txn_mgr
+        # or else we give up; who knows.  transaction_manager is the more
+        # recent spelling.