Rebuild repository after corruption
authorThierry Florac <tflorac@ulthar.net>
Wed, 05 Dec 2018 12:45:56 +0100
changeset 289 c8e21d7dd685
child 290 1c40b34d2646
Rebuild repository after corruption
.hgignore
.hgtags
LICENSE
MANIFEST.in
bootstrap.py
buildout.cfg
docs/HISTORY.txt
docs/README.txt
setup.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/__init__.py
src/pyams_utils/adapter.py
src/pyams_utils/attr.py
src/pyams_utils/cache.py
src/pyams_utils/configure.zcml
src/pyams_utils/container.py
src/pyams_utils/context.py
src/pyams_utils/data.py
src/pyams_utils/date.py
src/pyams_utils/decorator.py
src/pyams_utils/dict.py
src/pyams_utils/doctests/README.txt
src/pyams_utils/doctests/dates.txt
src/pyams_utils/doctests/request.txt
src/pyams_utils/doctests/unicode.txt
src/pyams_utils/encoding.py
src/pyams_utils/factory.py
src/pyams_utils/fanstatic.py
src/pyams_utils/html.py
src/pyams_utils/i18n.py
src/pyams_utils/include.py
src/pyams_utils/inherit.py
src/pyams_utils/interfaces/__init__.py
src/pyams_utils/interfaces/data.py
src/pyams_utils/interfaces/inherit.py
src/pyams_utils/interfaces/intids.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/interfaces/traversing.py
src/pyams_utils/interfaces/tree.py
src/pyams_utils/interfaces/url.py
src/pyams_utils/interfaces/zeo.py
src/pyams_utils/intids.py
src/pyams_utils/json.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/lock.py
src/pyams_utils/progress.py
src/pyams_utils/property.py
src/pyams_utils/protocol/__init__.py
src/pyams_utils/protocol/http.py
src/pyams_utils/protocol/tcp.py
src/pyams_utils/protocol/xmlrpc.py
src/pyams_utils/pygments.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/vocabulary.py
src/pyams_utils/widget/__init__.py
src/pyams_utils/widget/decimal.py
src/pyams_utils/wsgi.py
src/pyams_utils/zodb.py
tox.ini
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,21 @@
+
+syntax: regexp
+^develop-eggs$
+syntax: regexp
+^parts$
+syntax: regexp
+^bin$
+syntax: regexp
+^\.installed\.cfg$
+syntax: regexp
+^\.settings$
+syntax: regexp
+^build$
+syntax: regexp
+^dist$
+syntax: regexp
+^\.idea$
+syntax: regexp
+.*\.pyc$
+syntax: regexp
+^docs/build$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgtags	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,28 @@
+7398e25bad99bd5fccf12aa2d786a51fc555bab7 0.1.0
+d4c28f4078cdac97b90ea9a99e8eb5e1170f8ca6 0.1.1
+19019589cc19a2ed4cdbd99f3e728845794f9438 0.1.2
+c191cc6756f51581cb4ab172f6ee0fc3bca5a7e4 0.1.3
+f05c48fbf5b0943cdeb6cfd9a175b0ea0939c911 0.1.4
+8d39d5ba0e41a8580cba5ffc10e3503fd0ce1a1c 0.1.5
+8d39d5ba0e41a8580cba5ffc10e3503fd0ce1a1c 0.1.5
+27756366144a0e35af89f33ad69fdd5490ec0e6c 0.1.5
+0e9f9f3d573ec1692138754c31f7d868769bacf0 0.1.6
+15565a46783ed44c3ba21cc80d024b944cb31c7e 0.1.7
+a50633077766ba9658cd2c3406b2fdeea936dce1 0.1.8
+6543a754f22ef58a536a4abffe8b451e25741e5a 0.1.9
+8d441cc3e741aedbf2fdec98215e18471a25ca10 0.1.10
+f6cdd1bca5d59459aae8388c2bf528ec3eb0a54c 0.1.11
+1a9c15d8b12c6bb5b45567faa4612142f6ca22fe 0.1.12
+6530ade8bd68c67a8927f440699500769f8ac4ea 0.1.13
+3aa5dceefdbc37c9275c64f8729e1386764262a0 0.1.14
+5ac26944ecf9179a432e1fcb8b53c01279aca361 0.1.15
+120d6d178251ce47be3b78dc3c0a258a174de2d8 0.1.16
+059cb7042d715db6a2212de5992b22e9168ba23f 0.1.17
+011633e7163bfd4beaf62ce4b5a1215437e2646f 0.1.17.1
+daead521c0136f47c093cf9c238e680709639728 0.1.18
+9f68544fb8ad3fe8a8bab82ee6cc7e7dbcc6acf0 0.1.19
+52e8477afe089b6c770e9fe887f8bc8cbdac72d2 0.1.20
+f2a03777cdc711b13d4d8706c8dae3e913da191c 0.1.21
+30423155632c0126ace6c3417f31b35a99e5aaa4 0.1.22
+4dc1c762e798bd086e0ecce8d55378498783d929 0.1.23
+4bc91bf749a82ed7d79269a9b3e4f3bc54a0f084 0.1.24
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,42 @@
+Zope Public License (ZPL) Version 2.1
+=====================================
+
+A copyright notice accompanies this license document that identifies
+the copyright holders.
+
+This license has been certified as open source. It has also been designated
+as GPL compatible by the Free Software Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+   1. Redistributions in source code must retain the accompanying copyright
+      notice, this list of conditions, and the following disclaimer.
+   2. Redistributions in binary form must reproduce the accompanying copyright
+      notice, this list of conditions, and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+   3. Names of the copyright holders must not be used to endorse or promote
+      products derived from this software without prior written permission
+      from the copyright holders.
+   4. The right to distribute this software or to use it for any purpose does
+      not give you the right to use Servicemarks (sm) or Trademarks (tm) of the
+      copyright holders. Use of them is covered by separate agreement with the
+      copyright holders.
+   5. If any files are modified, you must cause the modified files to carry
+      prominent notices stating that you changed the files and the date of any
+      change.
+
+
+Disclaimer
+==========
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,5 @@
+include *.txt
+recursive-include docs *
+recursive-include src *
+global-exclude *.pyc
+global-exclude *.*~
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bootstrap.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,210 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# 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.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+__version__ = '2015-07-01'
+# See zc.buildout's changelog if this version is up to date.
+
+tmpeggs = tempfile.mkdtemp(prefix='bootstrap-')
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("--version",
+                  action="store_true", default=False,
+                  help=("Return bootstrap.py version."))
+parser.add_option("-t", "--accept-buildout-test-releases",
+                  dest='accept_buildout_test_releases',
+                  action="store_true", default=False,
+                  help=("Normally, if you do not specify a --buildout-version, "
+                        "the bootstrap script and buildout gets the newest "
+                        "*final* versions of zc.buildout and its recipes and "
+                        "extensions for you.  If you use this flag, "
+                        "bootstrap and buildout will get the newest releases "
+                        "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+                  help=("Specify the path to the buildout configuration "
+                        "file to be used."))
+parser.add_option("-f", "--find-links",
+                  help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+                  action="store_true", default=False,
+                  help=("Let bootstrap.py use existing site packages"))
+parser.add_option("--buildout-version",
+                  help="Use a specific zc.buildout version")
+parser.add_option("--setuptools-version",
+                  help="Use a specific setuptools version")
+parser.add_option("--setuptools-to-dir",
+                  help=("Allow for re-use of existing directory of "
+                        "setuptools versions"))
+
+options, args = parser.parse_args()
+if options.version:
+    print("bootstrap.py version %s" % __version__)
+    sys.exit(0)
+
+
+######################################################################
+# load/install setuptools
+
+try:
+    from urllib.request import urlopen
+except ImportError:
+    from urllib2 import urlopen
+
+ez = {}
+if os.path.exists('ez_setup.py'):
+    exec(open('ez_setup.py').read(), ez)
+else:
+    exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+    # ez_setup imports site, which adds site packages
+    # this will remove them from the path to ensure that incompatible versions
+    # of setuptools are not in the path
+    import site
+    # inside a virtualenv, there is no 'getsitepackages'.
+    # We can't remove these reliably
+    if hasattr(site, 'getsitepackages'):
+        for sitepackage_path in site.getsitepackages():
+            # Strip all site-packages directories from sys.path that
+            # are not sys.prefix; this is because on Windows
+            # sys.prefix is a site-package directory.
+            if sitepackage_path != sys.prefix:
+                sys.path[:] = [x for x in sys.path
+                               if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+
+if options.setuptools_version is not None:
+    setup_args['version'] = options.setuptools_version
+if options.setuptools_to_dir is not None:
+    setup_args['to_dir'] = options.setuptools_to_dir
+
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set.  We will
+# do it.
+for path in sys.path:
+    if path not in pkg_resources.working_set.entries:
+        pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+setuptools_path = ws.find(
+    pkg_resources.Requirement.parse('setuptools')).location
+
+# Fix sys.path here as easy_install.pth added before PYTHONPATH
+cmd = [sys.executable, '-c',
+       'import sys; sys.path[0:0] = [%r]; ' % setuptools_path +
+       'from setuptools.command.easy_install import main; main()',
+       '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+    'bootstrap-testing-find-links',
+    options.find_links or
+    ('http://downloads.buildout.org/'
+     if options.accept_buildout_test_releases else None)
+    )
+if find_links:
+    cmd.extend(['-f', find_links])
+
+requirement = 'zc.buildout'
+version = options.buildout_version
+if version is None and not options.accept_buildout_test_releases:
+    # Figure out the most recent final version of zc.buildout.
+    import setuptools.package_index
+    _final_parts = '*final-', '*final'
+
+    def _final_version(parsed_version):
+        try:
+            return not parsed_version.is_prerelease
+        except AttributeError:
+            # Older setuptools
+            for part in parsed_version:
+                if (part[:1] == '*') and (part not in _final_parts):
+                    return False
+            return True
+
+    index = setuptools.package_index.PackageIndex(
+        search_path=[setuptools_path])
+    if find_links:
+        index.add_find_links((find_links,))
+    req = pkg_resources.Requirement.parse(requirement)
+    if index.obtain(req) is not None:
+        best = []
+        bestv = None
+        for dist in index[req.project_name]:
+            distv = dist.parsed_version
+            if _final_version(distv):
+                if bestv is None or distv > bestv:
+                    best = [dist]
+                    bestv = distv
+                elif distv == bestv:
+                    best.append(dist)
+        if best:
+            best.sort()
+            version = best[-1].version
+if version:
+    requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd) != 0:
+    raise Exception(
+        "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+    args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+    args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildout.cfg	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,99 @@
+[buildout]
+eggs-directory = /var/local/env/pyams/eggs
+extends = http://download.ztfy.org/pyams/pyams-dev.cfg
+find-links = http://download.ztfy.org/eggs
+
+socket-timeout = 3
+
+#allow-picked-versions = false
+show-picked-versions = true
+newest = false
+
+allow-hosts =
+    bitbucket.org
+    *.python.org
+    *.sourceforge.net
+    github.com
+
+versions = versions
+
+src = src
+develop =
+    .
+    ../ext/lingua
+    ../pyams_catalog
+    ../pyams_file
+    ../pyams_form
+    ../pyams_i18n
+    ../pyams_pagelet
+    ../pyams_skin
+    ../pyams_template
+    ../pyams_viewlet
+    ../pyams_zmi
+
+parts =
+    package
+    i18n
+    pyflakes
+    test
+
+[package]
+recipe = zc.recipe.egg
+eggs =
+    babel
+    BTrees
+    chameleon
+    docutils
+    httplib2
+    persistent
+    pyams_utils
+    pyramid
+    pyramid_zodbconn
+    pysocks
+    pytz
+    transaction
+    z3c.form
+    z3c.pt
+    z3c.ptcompat
+    ZEO
+    ZODB
+    zope.annotation
+    zope.component
+    zope.container
+    zope.contentprovider
+    zope.datetime
+    zope.interface
+    zope.intid
+    zope.keyreference
+    zope.lifecycleevent
+    zope.location
+    zope.publisher
+    zope.schema
+    zope.site
+    zope.traversing
+interpreter = py
+
+[i18n]
+recipe = zc.recipe.egg
+eggs =
+    babel
+    lingua
+
+[pyflakes]
+recipe = zc.recipe.egg
+eggs = pyflakes
+scripts = pyflakes
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"]
+
+[pyflakesrun]
+recipe = collective.recipe.cmd
+on_install = true
+cmds = ${buildout:develop}/bin/${pyflakes:scripts}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = pyams_utils [test]
+
+[versions]
+pyams_utils = 0.1.24.1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/HISTORY.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,153 @@
+Changelog
+=========
+
+0.1.24.1
+--------
+ - added 'order' attribute to INode interface
+
+0.1.24
+------
+ - added Pygments utilities
+
+0.1.23
+------
+ - added function to check if TCP port is already opened
+ - added "truncate" TALES extension
+ - updated Fanstatic package docstring
+
+0.1.22
+------
+ - updated Fanstatic's external resource declaration to handle resources types not based on URLs
+
+0.1.21
+------
+ - added "display_context" property to request
+ - updated 'timestamp' TALES extension to specify optional ISO output format
+
+0.1.20
+------
+ - added support for server's locale configuration
+ - added 'need_resource' TALES extension to include Fanstatic resource from template
+
+0.1.19
+------
+ - updated "br" TALES extension to add custom start and end tags
+
+0.1.18
+------
+ - added default JSON encoder to serialize date and datetime objects (required for PyAMS_apm package)
+ - added JS text renderer
+ - use "yield from" in container's ISublocations adapters
+
+0.1.17.1
+--------
+ - update "br" TALES extension to disallow empty context
+
+0.1.17
+------
+ - added functions to get unique or random members from iterator
+
+0.1.16
+------
+ - updated annotation adapter factory to use a registered object factory if given factory is an
+   interface
+ - added "parent_selector" predicate for IObjectMoved/IObjectAdded events
+ - added "is_interface" function to check if an object is an interface
+ - added IRelativeURL interface, default adapter and TALES "relative_url" extension to get
+   content's URL based on current display context
+ - added "cache_key" and "timestamp" TALES extensions
+ - removed configuration factory interfaces and use standard object factory
+
+0.1.15
+------
+ - added "inherit_from" attribute to IInheritInfo interface
+ - added helper to get a persistent adapter through context's annotations
+ - added "canonical_url" function, interface and TALES extension
+ - updated custom PyAMS traverser to notify BeforeTraverseEvent on last traversed object
+
+0.1.14
+------
+ - updated request_property decorator cache key
+ - updated base site root ACL to grant 'public' permission to everyone
+ - use object ID in default cache key adapter
+ - add cache key adapters for string and persistent objects
+
+0.1.13
+------
+ - added site root factory interface
+ - updated site factory to use new factory interface utility
+
+0.1.12
+------
+ - added "request_selector" subscriber predicate
+ - updated ZEO connection timeout arguments for ZEO 5.x
+ - remove INewLocalSite event subscribers; database upgrade is now only done through command line script, and
+   utilities providing ISiteGenerations can be ordered
+ - removed static configuration manager interface
+ - make sure that requests created manually support annotations in get/set request data functions
+
+0.1.11
+------
+ - moved PyAMS documentation to "PyAMS User Guide" package
+ - added "inherit" interface and module to handle inheritance between located components easier
+ - added exception handling when trying to get or set request annotations
+ - added property annotation to define volatile attributes on persistent classes
+
+0.1.10
+------
+ - added ZODB connection class and vocabulary based on Pyramid's settings
+ - updated DocFieldProperty to correctly handle attributes documentation
+ - small updates in registry management functions
+
+0.1.9
+-----
+ - added "NullAdapter" class to be able to remove a default adapter for a given context
+ - added "get_global_registry" function
+
+0.1.8
+-----
+ - updated TALES extensions interfaces
+ - added validation method and exception for email schema field
+
+0.1.7
+-----
+ - added mail address schema field
+
+0.1.6
+-----
+ - updated exceptions messages translations
+ - added 'query' parameter to 'absolute_url' function
+
+0.1.5
+-----
+ - refactored request and session properties management
+ - added check for multi-adapter lookup on IHTMLRenderer interface in 'html' TALES extension
+ - update docstrings
+
+0.1.4
+-----
+ - added "condition" optional argument to "get_parent" traversing helper to retrieve a parent only if given function
+   returns a "True" value when called with parent as argument
+ - added annotation for vocabulary registry
+ - added 'prefix' argument to 'request_property' and 'session_property' decorators
+ - handle POSError in 'query_utility' registry function
+ - updated unit tests
+ - updated documentation
+
+0.1.3
+-----
+ - corrected XML-RPC client for Python 3
+ - added container's "find_objects_*" functions based on ISubLocations interface
+
+0.1.2
+-----
+ - imports cleanup
+ - moved IIntIds related modules from pyams_base package
+
+0.1.1
+-----
+ - corrected cookies management in XML-RPC authenticated transport
+
+0.1.0
+-----
+ - initial release
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/README.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,36 @@
+===================
+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:
+ - registry: local registry management tools
+ - adapter: custom adapters and annotations
+ - 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
+ - session: to store and get properties values from current session
+ - timezone: convert datetime to a given timezone ; provides a server default timezone utility
+ - traversing: custom traverser utility supporting namespaces
+ - unicode: convert any text to unicode for easy storage
+ - protocol: utility functions and modules for several nerwork protocols
+ - text: simple text operations and text to HTML conversion
+ - html: HTML parser and HTML to text converter
+ - file: file upload data converter
+ - tales: custom "extension:" TALES expression using adapters
+
+
+How to use pyams_utils ?
+========================
+
+A set of pyams_utils usages are given as doctests in pyams_utils/doctests/README.txt
+
+You will also a whole set of documentations into the "docs" directory, available on
+`ReadTheDocs <http://pyams-utils.readthedocs.org>`_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,96 @@
+### -*- 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.
+#
+##############################################################################
+
+"""
+This module contains pyams_utils package
+"""
+import os
+from setuptools import setup, find_packages
+
+DOCS = os.path.join(os.path.dirname(__file__),
+                    'docs')
+
+README = os.path.join(DOCS, 'README.txt')
+HISTORY = os.path.join(DOCS, 'HISTORY.txt')
+
+version = '0.1.24.1'
+long_description = open(README).read() + '\n\n' + open(HISTORY).read()
+
+tests_require = [
+    'pyramid_zcml',
+    'zope.exceptions'
+]
+
+setup(name='pyams_utils',
+      version=version,
+      description="Utility functions and classes for PyAMS",
+      long_description=long_description,
+      classifiers=[
+          "License :: OSI Approved :: Zope Public License",
+          "Development Status :: 4 - Beta",
+          "Programming Language :: Python",
+          "Framework :: Zope3",
+          "Topic :: Software Development :: Libraries :: Python Modules",
+      ],
+      keywords='Pyramid PyAMS utilities',
+      author='Thierry Florac',
+      author_email='tflorac@ulthar.net',
+      url='http://www.ztfy.org',
+      license='ZPL',
+      packages=find_packages('src'),
+      package_dir={'': 'src'},
+      namespace_packages=[],
+      include_package_data=True,
+      package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo',
+                         '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']},
+      zip_safe=False,
+      # uncomment this to be able to run tests with setup.py
+      test_suite="pyams_utils.tests.test_utilsdocs.test_suite",
+      tests_require=tests_require,
+      extras_require=dict(test=tests_require),
+      install_requires=[
+          'setuptools',
+          # -*- Extra requirements: -*-
+          'babel',
+          'beaker',
+          '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.location',
+          'zope.schema',
+          'zope.site',
+          'zope.traversing'
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [console_scripts]
+      pyams_upgrade = pyams_utils.scripts.zodb:upgrade_site
+      """,
+      )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/PKG-INFO	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,204 @@
+Metadata-Version: 2.1
+Name: pyams-utils
+Version: 0.1.24
+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:
+         - registry: local registry management tools
+         - adapter: custom adapters and annotations
+         - 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
+         - session: to store and get properties values from current session
+         - timezone: convert datetime to a given timezone ; provides a server default timezone utility
+         - traversing: custom traverser utility supporting namespaces
+         - unicode: convert any text to unicode for easy storage
+         - protocol: utility functions and modules for several nerwork protocols
+         - text: simple text operations and text to HTML conversion
+         - html: HTML parser and HTML to text converter
+         - file: file upload data converter
+         - tales: custom "extension:" TALES expression using adapters
+        
+        
+        How to use pyams_utils ?
+        ========================
+        
+        A set of pyams_utils usages are given as doctests in pyams_utils/doctests/README.txt
+        
+        You will also a whole set of documentations into the "docs" directory, available on
+        `ReadTheDocs <http://pyams-utils.readthedocs.org>`_
+        
+        
+        Changelog
+        =========
+        
+        0.1.24
+        ------
+         - added Pygments utilities
+        
+        0.1.23
+        ------
+         - added function to check if TCP port is already opened
+         - added "truncate" TALES extension
+         - updated Fanstatic package docstring
+        
+        0.1.22
+        ------
+         - updated Fanstatic's external resource declaration to handle resources types not based on URLs
+        
+        0.1.21
+        ------
+         - added "display_context" property to request
+         - updated 'timestamp' TALES extension to specify optional ISO output format
+        
+        0.1.20
+        ------
+         - added support for server's locale configuration
+         - added 'need_resource' TALES extension to include Fanstatic resource from template
+        
+        0.1.19
+        ------
+         - updated "br" TALES extension to add custom start and end tags
+        
+        0.1.18
+        ------
+         - added default JSON encoder to serialize date and datetime objects (required for PyAMS_apm package)
+         - added JS text renderer
+         - use "yield from" in container's ISublocations adapters
+        
+        0.1.17.1
+        --------
+         - update "br" TALES extension to disallow empty context
+        
+        0.1.17
+        ------
+         - added functions to get unique or random members from iterator
+        
+        0.1.16
+        ------
+         - updated annotation adapter factory to use a registered object factory if given factory is an
+           interface
+         - added "parent_selector" predicate for IObjectMoved/IObjectAdded events
+         - added "is_interface" function to check if an object is an interface
+         - added IRelativeURL interface, default adapter and TALES "relative_url" extension to get
+           content's URL based on current display context
+         - added "cache_key" and "timestamp" TALES extensions
+         - removed configuration factory interfaces and use standard object factory
+        
+        0.1.15
+        ------
+         - added "inherit_from" attribute to IInheritInfo interface
+         - added helper to get a persistent adapter through context's annotations
+         - added "canonical_url" function, interface and TALES extension
+         - updated custom PyAMS traverser to notify BeforeTraverseEvent on last traversed object
+        
+        0.1.14
+        ------
+         - updated request_property decorator cache key
+         - updated base site root ACL to grant 'public' permission to everyone
+         - use object ID in default cache key adapter
+         - add cache key adapters for string and persistent objects
+        
+        0.1.13
+        ------
+         - added site root factory interface
+         - updated site factory to use new factory interface utility
+        
+        0.1.12
+        ------
+         - added "request_selector" subscriber predicate
+         - updated ZEO connection timeout arguments for ZEO 5.x
+         - remove INewLocalSite event subscribers; database upgrade is now only done through command line script, and
+           utilities providing ISiteGenerations can be ordered
+         - removed static configuration manager interface
+         - make sure that requests created manually support annotations in get/set request data functions
+        
+        0.1.11
+        ------
+         - moved PyAMS documentation to "PyAMS User Guide" package
+         - added "inherit" interface and module to handle inheritance between located components easier
+         - added exception handling when trying to get or set request annotations
+         - added property annotation to define volatile attributes on persistent classes
+        
+        0.1.10
+        ------
+         - added ZODB connection class and vocabulary based on Pyramid's settings
+         - updated DocFieldProperty to correctly handle attributes documentation
+         - small updates in registry management functions
+        
+        0.1.9
+        -----
+         - added "NullAdapter" class to be able to remove a default adapter for a given context
+         - added "get_global_registry" function
+        
+        0.1.8
+        -----
+         - updated TALES extensions interfaces
+         - added validation method and exception for email schema field
+        
+        0.1.7
+        -----
+         - added mail address schema field
+        
+        0.1.6
+        -----
+         - updated exceptions messages translations
+         - added 'query' parameter to 'absolute_url' function
+        
+        0.1.5
+        -----
+         - refactored request and session properties management
+         - added check for multi-adapter lookup on IHTMLRenderer interface in 'html' TALES extension
+         - update docstrings
+        
+        0.1.4
+        -----
+         - added "condition" optional argument to "get_parent" traversing helper to retrieve a parent only if given function
+           returns a "True" value when called with parent as argument
+         - added annotation for vocabulary registry
+         - added 'prefix' argument to 'request_property' and 'session_property' decorators
+         - handle POSError in 'query_utility' registry function
+         - updated unit tests
+         - updated documentation
+        
+        0.1.3
+        -----
+         - corrected XML-RPC client for Python 3
+         - added container's "find_objects_*" functions based on ISubLocations interface
+        
+        0.1.2
+        -----
+         - imports cleanup
+         - moved IIntIds related modules from pyams_base package
+        
+        0.1.1
+        -----
+         - corrected cookies management in XML-RPC authenticated transport
+        
+        0.1.0
+        -----
+         - 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
+Provides-Extra: test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/SOURCES.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,85 @@
+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/cache.py
+src/pyams_utils/configure.zcml
+src/pyams_utils/container.py
+src/pyams_utils/context.py
+src/pyams_utils/data.py
+src/pyams_utils/date.py
+src/pyams_utils/decorator.py
+src/pyams_utils/dict.py
+src/pyams_utils/encoding.py
+src/pyams_utils/factory.py
+src/pyams_utils/fanstatic.py
+src/pyams_utils/html.py
+src/pyams_utils/i18n.py
+src/pyams_utils/include.py
+src/pyams_utils/inherit.py
+src/pyams_utils/intids.py
+src/pyams_utils/json.py
+src/pyams_utils/list.py
+src/pyams_utils/lock.py
+src/pyams_utils/progress.py
+src/pyams_utils/property.py
+src/pyams_utils/pygments.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/vocabulary.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/doctests/README.txt
+src/pyams_utils/doctests/dates.txt
+src/pyams_utils/doctests/request.txt
+src/pyams_utils/doctests/unicode.txt
+src/pyams_utils/interfaces/__init__.py
+src/pyams_utils/interfaces/data.py
+src/pyams_utils/interfaces/inherit.py
+src/pyams_utils/interfaces/intids.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/interfaces/traversing.py
+src/pyams_utils/interfaces/tree.py
+src/pyams_utils/interfaces/url.py
+src/pyams_utils/interfaces/zeo.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/tcp.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/utility.py
+src/pyams_utils/timezone/vocabulary.py
+src/pyams_utils/widget/__init__.py
+src/pyams_utils/widget/decimal.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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/entry_points.txt	Wed Dec 05 12:45:56 2018 +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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/not-zip-safe	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/requires.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,29 @@
+setuptools
+babel
+beaker
+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.location
+zope.schema
+zope.site
+zope.traversing
+
+[test]
+pyramid_zcml
+zope.exceptions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils.egg-info/top_level.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+pyams_utils
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,43 @@
+#
+# 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.schema.fieldproperty import FieldProperty
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_utils')
+
+
+def get_field_doc(self):
+    field = self._FieldProperty__field
+    if field.title and field.description:
+        return '{0}: {1}'.format(field.title, field.description)
+    elif field.title:
+        return field.title
+    else:
+        return super(self.__class__, self).__doc__
+
+
+FieldProperty.__doc__ = property(get_field_doc)
+
+
+def includeme(config):
+    """pyams_utils features include"""
+    from .include import include_package
+    include_package(config)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/adapter.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,185 @@
+#
+# 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.
+#
+
+"""Adapters management package
+
+This package provides a small set of standard base adapters for *context*, *context* and *request*, and
+*context* and *request* and *view*.
+
+See :ref:`zca` to see how PyAMS can help components management.
+"""
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import logging
+logger = logging.getLogger('PyAMS (utils)')
+
+import venusian
+
+# import interfaces
+from zope.annotation.interfaces import IAnnotations
+
+# import packages
+from pyams_utils.factory import get_object_factory, is_interface
+from pyams_utils.registry import get_current_registry
+from zope.interface import implementedBy, alsoProvides, Interface
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate as zope_locate
+
+
+class ContextAdapter(object):
+    """Context adapter"""
+
+    def __init__(self, context):
+        self.context = context
+
+
+class ContextRequestAdapter(object):
+    """Context + request multi-adapter"""
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+
+class ContextRequestViewAdapter(object):
+    """Context + request + view multi-adapter"""
+
+    def __init__(self, context, request, view):
+        self.context = context
+        self.request = request
+        self.view = view
+
+
+class NullAdapter(object):
+    """An adapter which always return None!
+
+    Can be useful to override a default adapter...
+    """
+
+    def __new__(cls, *arsg, **kwargs):
+        return None
+
+
+class adapter_config(object):
+    """Function or class decorator to declare an adapter
+
+    Annotation parameters can be:
+
+    :param str='' name: name of the adapter
+    :param [Interface...] context: an interface, or a tuple of interfaces, that the component adapts
+    :param Interface provides: the interface that the adapter provides
+    """
+
+    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 'provides' argument")
+
+            config = context.config.with_package(info.module)
+            logger.debug("Registering adapter {0} for {1} providing {2}".format(str(ob),
+                                                                                str(adapts),
+                                                                                str(provides)))
+            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
+
+
+def get_annotation_adapter(context, key, factory=None, markers=None, notify=True,
+                           locate=True, parent=None, name=None, callback=None, **kwargs):
+    """Get an adapter via object's annotations, creating it if not existent
+    
+    :param object context: context object which should be adapted
+    :param str key: annotations key to look for
+    :param factory: if annotations key is not found, this is the factory which will be used to
+        create a new object; if factory is None and is requested object can't be found, None is returned
+    :param markers: if not None, list of marker interfaces which created adapter should provide
+    :param bool=True notify: if 'False', no notification event will be sent on object creation
+    :param bool=True locate: if 'False', the new object is not attached to any parent
+    :param object=None parent: parent to which new object is attached
+    :param str=None name: if locate is not False, this is the name with which the new object is attached
+        to it's parent.
+    :param callback: if not None, callback function which will be called after
+    """
+    annotations = IAnnotations(context, None)
+    if annotations is None:
+        return None
+    adapter = annotations.get(key)
+    if adapter is None:
+        if 'default' in kwargs:
+            return kwargs['default']
+        elif factory is None:
+            return None
+        else:
+            if is_interface(factory):
+                factory = get_object_factory(factory)
+                assert factory is not None, "Missing object factory"
+            adapter = annotations[key] = factory()
+            if markers:
+                if not isinstance(markers, (list, tuple, set)):
+                    markers = {markers}
+                for marker in markers:
+                    alsoProvides(adapter, marker)
+            if notify:
+                get_current_registry().notify(ObjectCreatedEvent(adapter))
+            if locate:
+                zope_locate(adapter, context if parent is None else parent, name)
+            if callback:
+                callback(adapter)
+    return adapter
+
+
+def get_adapter_weight(item):
+    """Get adapters weight sort key"""
+    name, adapter = item
+    try:
+        return int(adapter.weight), name
+    except (TypeError, AttributeError):
+        return 0, name
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/attr.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,43 @@
+#
+# 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
+
+    This custom traversing adapter can be used to access an object attribute directly from
+    an URL by using a path like this::
+
+    /path/to/object/++attr++name
+
+    Whare *name* is the name of the requested attribute
+    """
+
+    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/cache.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,56 @@
+#
+# 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 import ICacheKeyValue
+from pyams_utils.interfaces.tales import ITALESExtension
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from zope.interface import Interface
+
+
+@adapter_config(context=object, provides=ICacheKeyValue)
+def object_cache_key_adapter(obj):
+    return str(id(obj))
+
+
+@adapter_config(context=str, provides=ICacheKeyValue)
+def string_cache_key_adapter(obj):
+    return obj
+
+
+@adapter_config(context=IPersistent, provides=ICacheKeyValue)
+def persistent_cache_key_adapter(obj):
+    if obj._p_oid:
+        return str(int.from_bytes(obj._p_oid, byteorder='big'))
+    else:  # unsaved object
+        return str(id(obj))
+
+
+@adapter_config(name='cache_key', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class CacheKeyTalesExtension(ContextRequestViewAdapter):
+    """extension:cache_key(context) TALES extension
+
+    A PyAMS TALES extension which allows to render cache key value for a given context.
+    """
+
+    def render(self, context=None):
+        if context is None:
+            context = self.request.context
+        return ICacheKeyValue(context)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/configure.zcml	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,26 @@
+<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.browserresource" 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="zope.location" />
+
+	<include package="z3c.form" />
+	<include package="z3c.pt" />
+	<include package="z3c.ptcompat" />
+
+</configure>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/container.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,137 @@
+#
+# 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.container.interfaces import IContainer, IContained
+from zope.lifecycleevent.interfaces import IObjectMovedEvent
+from zope.location.interfaces import ISublocations
+
+# import packages
+from BTrees.OOBTree import OOBTree
+from persistent.list import PersistentList
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyramid.threadlocal import get_current_registry
+from zope.container.ordered import OrderedContainer
+
+
+class BTreeOrderedContainer(OrderedContainer):
+    """BTree based ordered container
+
+    This container maintain a manual order of it's contents
+    """
+
+    def __init__(self):
+        self._data = OOBTree()
+        self._order = PersistentList()
+
+
+class ParentSelector(object):
+    """Interface based parent selector
+
+    This selector can be used as a subscriber predicate on IObjectAddedEvent to define
+    an interface that the new parent must support for the event to be applied::
+
+    .. code-block:: python
+
+        from pyams_utils.interfaces.site import ISiteRoot
+
+        @subscriber(IObjectAddedEvent, parent_selector=ISiteRoot)
+        def siteroot_object_added_event_handler(event):
+            '''This is an event handler for an ISiteRoot object added event'''
+    """
+
+    def __init__(self, ifaces, config):
+        if not isinstance(ifaces, (list, tuple, set)):
+            ifaces = (ifaces,)
+        self.interfaces = ifaces
+
+    def text(self):
+        return 'parent_selector = %s' % str(self.interfaces)
+
+    phash = text
+
+    def __call__(self, event):
+        if not IObjectMovedEvent.providedBy(event):
+            return False
+        for intf in self.interfaces:
+            try:
+                if intf.providedBy(event.newParent):
+                    return True
+            except (AttributeError, TypeError):
+                if isinstance(event.newParent, intf):
+                    return True
+        return False
+
+
+@adapter_config(context=IContained, provides=ISublocations)
+class ContainerSublocationsAdapter(ContextAdapter):
+    """Contained object sub-locations adapter
+
+    This adapter checks for custom ISublocations interface adapters which can
+    be defined by any component to get access to inner locations, defined for
+    example via annotations.
+    """
+
+    def sublocations(self):
+        """See `zope.location.interfaces.ISublocations` interface"""
+        context = self.context
+        # Check for adapted sub-locations first...
+        registry = get_current_registry()
+        for name, adapter in registry.getAdapters((context,), ISublocations):
+            if not name:  # don't reuse default adapter!!
+                continue
+            yield from adapter.sublocations()
+        # then yield container items
+        if IContainer.providedBy(context):
+            yield from context.values()
+
+
+def find_objects_matching(root, condition, ignore_root=False):
+    """Find all objects in root that match the condition
+
+    The condition is a Python callable object that takes an object as
+    argument and must return a boolean result.
+
+    All sub-objects of the root will also be searched recursively.
+
+    :param object root: the parent object from which search is started
+    :param callable condition: a callable object which may return true for a given
+        object to be selected
+    :param boolean ignore_root: if *True*, the root object will not be returned, even if it matches
+        the given condition
+    :return: an iterator for all root's sub-objects matching condition
+    """
+    if (not ignore_root) and condition(root):
+        yield root
+    locations = ISublocations(root, None)
+    if locations is not None:
+        for location in locations.sublocations():
+            if condition(location):
+                yield location
+            yield from find_objects_matching(location, condition, ignore_root=True)
+
+
+def find_objects_providing(root, interface):
+    """Find all objects in root that provide the specified interface
+
+    All sub-objects of the root will also be searched recursively.
+
+    :param object root: object; the parent object from which search is started
+    :param Interface interface: interface; an interface that sub-objects should provide
+    :return: an iterator for all root's sub-objects that provide the given interface
+    """
+    yield from find_objects_matching(root, interface.providedBy)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/context.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,56 @@
+#
+# 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 as a subscriber predicate to define
+    an interface that the context must support for the event to be applied:
+
+    .. code-block:: python
+
+        from pyams_utils.interfaces.site import ISiteRoot
+
+        @subscriber(IObjectModifiedEvent, context_selector=ISiteRoot)
+        def siteroot_modified_event_handler(event):
+            '''This is an event handler for an ISiteRoot object modification event'''
+    """
+
+    def __init__(self, ifaces, config):
+        if not isinstance(ifaces, (list, tuple, set)):
+            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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,110 @@
+#
+# 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.
+#
+
+__doc__ = """Object data API module
+
+The *IObjectData* interface is a generic interface which can be used to assign custom data to nay object.
+This object data may be any object which can be serialized to JSON, and assigned to any HTML *data* attribute.
+It can typically be used to set a *data-ams-data* attribute to objects, which is afterwards converted to
+classic *data-* attributes by **MyAMS.js** framework.
+
+For example, for a custom widget in a form::
+
+    def updateWidgets(self):
+        super(MyForm, self).updateWidgets()
+        widget = self.widgets['mywidget']
+        alsoProvides(widget, IObjectData)
+        widget.object_data = {'ams-colorpicker-position': 'top left'}
+
+You can then set an attribute in a TAL template like this::
+
+    <div tal:attributes="data-ams-data extension:object_data(widget)">...</div>
+
+After data initialization by **MyAMS.js**, the following code will be converted to::
+
+    <div data-ams-colorpicker-position="top left">...</div>
+"""
+
+__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):
+        """See `pyams_utils.interfaces.data.IObjectDataRenderer` interface"""
+        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
+
+    This TALES extension is to be used in Chameleon templates to define a custom data attribute
+    which stores all object data (see :py:class:`pyams_utils.interfaces.data.IObjectData` interface), like this::
+
+        <div tal:attributes="data-ams-data extension:object_data(context)">...</div>
+    """
+
+    def render(self, context=None):
+        """See `pyams_utils.interfaces.tales.ITALESExtension` interface"""
+        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
+
+    This TALES extension can be used to get a request data, previously stored in the request via an annotation.
+    For example::
+
+        <div tal:content="extension:request_data('my.annotation.key')">...</div>
+    """
+
+    def render(self, params=None):
+        """See `pyams_utils.interfaces.tales.ITALESExtension` interface"""
+        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
+
+    This TALES extension can be used to get a request data, previously stored in the request via an annotation.
+    For example::
+
+        <div tal:content="extension:request_data('my.annotation.key')">...</div>
+    """
+
+    def render(self, params=None):
+        """See `pyams_utils.interfaces.tales.ITALESExtension` interface"""
+        return self.request.annotations.get(params)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/date.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,272 @@
+#
+# 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
+from pyams_utils.interfaces.tales import ITALESExtension
+from zope.dublincore.interfaces import IZopeDublinCore
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from pyams_utils.request import check_request
+from pyams_utils.timezone import gmtime, tztime
+from zope.datetime import parseDatetimetz
+from zope.interface import Interface
+
+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 date value: input date to convert to unicode
+    :return: unicode; input date converted to unicode
+
+    >>> from datetime import datetime
+    >>> from pyams_utils.date import unidate
+    >>> value = datetime(2016, 11, 15, 10, 13, 12)
+    >>> unidate(value)
+    '2016-11-15T10:13:12+00:00'
+    """
+    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 str value: unicode date to be parsed
+    :return: datetime; the specified value, converted to datetime
+
+    >>> from pyams_utils.date import parse_date
+    >>> parse_date('2016-11-15T10:13:12+00:00')
+    datetime.datetime(2016, 11, 15, 10, 13, 12, tzinfo=<StaticTzInfo 'GMT'>)
+    """
+    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 date/datetime value: a date or datetime value to convert
+    :return: datetime; input value converted to datetime
+
+    >>> from datetime import date, datetime
+    >>> from pyams_utils.date import date_to_datetime
+    >>> value = date(2016, 11, 15)
+    >>> date_to_datetime(value)
+    datetime.datetime(2016, 11, 15, 0, 0)
+    >>> value = datetime(2016, 11, 15, 10, 13, 12)
+    >>> value
+    datetime.datetime(2016, 11, 15, 10, 13, 12)
+    >>> date_to_datetime(value) is value
+    True
+    """
+    if not value:
+        return None
+    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
+
+    :param datetime value: the value to format
+    :param str format: a format string to use by `strftime` function
+    :param request: the request from which to extract localization info for translation
+    :return: str; input datetime converted to given format
+
+    >>> from datetime import datetime
+    >>> from pyams_utils.date import format_date, SH_DATE_FORMAT
+    >>> value = datetime(2016, 11, 15, 10, 13, 12)
+    >>> format_date(value)
+    'on 15/11/2016'
+    >>> format_date(value, SH_DATE_FORMAT)
+    '15/11/2016'
+    """
+    if not value:
+        return '--'
+    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 including time
+
+    :param datetime value: the value to format
+    :param str format: a format string to use by `strftime` function
+    :param request: request; the request from which to extract localization info for translation
+    :return: str; input datetime converted to given format
+
+    >>> from datetime import datetime
+    >>> from pyams_utils.date import format_datetime, SH_DATETIME_FORMAT
+    >>> value = datetime(2016, 11, 15, 10, 13, 12)
+    >>> format_datetime(value)
+    'on 15/11/2016 at 10:13'
+    >>> format_datetime(value, SH_DATETIME_FORMAT)
+    '15/11/2016 - 10:13'
+    """
+    return format_date(value, format, request)
+
+
+def get_age(value, request=None):
+    """Get 'human' age of a given datetime (including timezone) compared to current datetime (in UTC)
+
+    :param datetime value: input datetime to be compared with current datetime
+    :return: str; the delta value, converted to months, weeks, days, hours or minutes
+    """
+    if request is None:
+        request = check_request()
+    translate = request.localizer.translate
+    now = gmtime(datetime.utcnow())
+    delta = now - gmtime(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 'human' delta as string between two dates
+
+    :param datetime v1: start date
+    :param datetime v2: end date, or current date (in UTC) if None
+    :param request: the request from which to extract localization infos
+    :return: str; approximate delta between the two input 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
+
+
+@adapter_config(name='timestamp', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class TimestampTalesAdapter(ContextRequestViewAdapter):
+    """extension:timestamp(context) TALES adapter
+
+    A PyAMS TALES extension to get timestamp based on last context modification date.
+    """
+
+    def render(self, context=None, formatting=None):
+        if context is None:
+            context = self.request.context
+        if formatting == 'iso':
+            format_func = datetime.isoformat
+        else:
+            format_func = datetime.timestamp
+        dc = IZopeDublinCore(context, None)
+        if dc is None:
+            return format_func(datetime.utcnow())
+        else:
+            return format_func(dc.modified)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/decorator.py	Wed Dec 05 12:45:56 2018 +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.__code__.co_filename,
+                                   lineno=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/dict.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,54 @@
+#
+# 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 update_dict(input, key, value):
+    """Update given mapping if input value is a boolean 'true' value
+
+    :param dict input: input dictionary
+    :param key: mapping key
+    :param value: new value
+
+    'False' values leave mapping unchanged::
+
+    >>> from pyams_utils.dict import update_dict
+    >>> mydict = {}
+    >>> update_dict(mydict, 'key1', None)
+    >>> mydict
+    {}
+    >>> update_dict(mydict, 'key1', '')
+    >>> mydict
+    {}
+    >>> update_dict(mydict, 'key1', 0)
+    >>> mydict
+    {}
+
+    'True' values modify the mapping::
+
+    >>> update_dict(mydict, 'key1', 'value')
+    >>> mydict
+    {'key1': 'value'}
+    >>> update_dict(mydict, 'key1', 'value2')
+    >>> mydict
+    {'key1': 'value2'}
+    """
+    if value:
+        input[key] = value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/doctests/README.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,10 @@
+===================
+pyams_utils package
+===================
+
+Introduction
+------------
+
+This package is composed of a set of utility functions, usable into any Pyramid application.
+
+Associated files provide additional automated doctests.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/doctests/dates.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,57 @@
+
+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 gave 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/doctests/request.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,68 @@
+
+Managing requests
+-----------------
+
+PyAMS_utils package provides some useful functions to handle requests.
+
+The "check_request" function can be used when you have to be sure that a request is active in
+the current execution thread; if no "real" request is active, a new one is created:
+
+    >>> from pyams_utils.request import query_request, check_request
+    >>> request = query_request()
+    >>> request is None
+    True
+    >>> request = check_request()
+    >>> request
+    <PyAMSRequest at ... GET http://localhost/>
+
+If a new request is created "from scratch", it's registry is assigned to global registry:
+
+    >>> request.registry
+    <Registry global>
+
+A request context can be used to activate a request into execution thread:
+
+    >>> from pyramid.threadlocal import RequestContext
+    >>> with RequestContext(request) as context_request:
+    ...     context_request is request
+    True
+    >>> with RequestContext(request):
+    ...     context_request = check_request()
+    ...     context_request is request
+    True
+
+Requests can now support annotations to set and retrieve any information to a given request:
+
+    >>> from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations
+    >>> from zope.annotation.attribute import AttributeAnnotations
+    >>> from pyams_utils.registry import get_global_registry
+    >>> registry = get_global_registry()
+    >>> registry.registerAdapter(AttributeAnnotations, (IAttributeAnnotatable, ), IAnnotations)
+
+    >>> from pyams_utils.request import get_request_data, set_request_data
+    >>> set_request_data(request, 'test', 'This is request data')
+    >>> get_request_data(request, 'test')
+    'This is request data'
+
+Annotations can be used to automatically reify a given property into request annotations:
+
+    >>> from pyams_utils.request import request_property
+    >>> class RequestPropertyTestClass(object):
+    ...
+    ...     @request_property(key='My property')
+    ...     def my_property(self):
+    ...         print("This is my property")
+    ...         return 1
+    ...
+    >>> with RequestContext(request):
+    ...     instance = RequestPropertyTestClass()
+    ...     instance.my_property()
+    This is my property
+    1
+
+As property value is cached into request annotations, other property calls will just return
+cached value:
+
+    >>> with RequestContext(request):
+    ...     instance.my_property()
+    1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/doctests/unicode.txt	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,80 @@
+
+Unicode functions
+-----------------
+
+While working with extended characters sets containing accentuated characters, it's necessary to
+convert 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 by 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'
+
+    >>> sample = 'Texte accentué avec "ponctuation" !'
+    >>> unicode.translate_string(sample, force_lower=True, spaces=' ')
+    'texte accentue avec ponctuation'
+    >>> unicode.translate_string(sample, force_lower=True, remove_punctuation=False, spaces=' ')
+    'texte accentue avec "ponctuation" !'
+    >>> unicode.translate_string(sample, force_lower=True, remove_punctuation=False, spaces='-')
+    'texte-accentue-avec-"ponctuation"-!'
+    >>> unicode.translate_string(sample, force_lower=True, remove_punctuation=True, spaces='-')
+    'texte-accentue-avec-ponctuation'
+    >>> unicode.translate_string(sample, force_lower=True, remove_punctuation=True, spaces=' ', keep_chars='!')
+    'texte accentue avec ponctuation !'
+
+
+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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/encoding.py	Wed Dec 05 12:45:56 2018 +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 IChoice
+
+# import packages
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+from zope.interface import implementer
+from zope.schema import Choice
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
+
+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)'),
+}
+
+
+@vocabulary_config(name='PyAMS encodings')
+class EncodingsVocabulary(SimpleVocabulary):
+    """A vocabulary containing a set of registered encodings"""
+
+    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)
+
+
+class IEncodingField(IChoice):
+    """Encoding field interface"""
+
+
+@implementer(IEncodingField)
+class EncodingField(Choice):
+    """Encoding schema 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/factory.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,171 @@
+#
+# Copyright (c) 2008-2018 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.
+#
+
+"""Objects factory management
+
+This module provides a decorator and a small set of functions to handle object factories.
+
+Instead of directly using a class as an object factory, the object of this module is to
+let you create an object based on an interface. The first step is to create an object 
+implementing this interface, and then to register it as a factory:
+
+.. code-block:: python
+
+    @implementer(IMyInterface)
+    class MyClass(object):
+        '''Class implementing my interface'''
+    
+    register_factory(IMyInterface, MyClass)
+    
+Factory registry can also be handle by a decorator called "factory_config":
+
+.. code-block:: python
+
+    @implementer(IMyInterface)
+    @factory_config(IMyInterface)
+    class MyClass(object):
+        '''Class implementing my interface'''
+
+When a factory is registered, you can look for a factory:
+
+.. code-block:: python
+
+    factory = get_object_factory(IMyInterface)
+    if factory is not None:
+        myobject = factory()
+    else:
+        myobject = MyDefaultImplementation()
+
+By registering their own objects factories, extension packages can easily provide their
+own implementation of any PyAMS interface handled by factories.
+"""
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import logging
+logger = logging.getLogger('PyAMS (utils)')
+
+import venusian
+
+# import interfaces
+from pyams_utils.interfaces import IObjectFactory
+
+# import packages
+from pyams_utils.registry import get_global_registry
+from zope.component import adapter, queryAdapter
+from zope.interface import implementer, Interface
+from zope.interface.interface import InterfaceClass
+
+
+@adapter(Interface)
+@implementer(IObjectFactory)
+class ObjectFactoryAdapter(object):
+    """Most basic object factory adapter"""
+
+    factory = None
+
+    def __init__(self, context):
+        self.context = context
+
+    def __call__(self):
+        return self.factory()
+
+
+def is_interface(object):
+    """Check if given object is an interface"""
+    return issubclass(object.__class__, InterfaceClass)
+
+
+def get_interface_name(iface):
+    """Get interface full name"""
+    return iface.__module__ + '.' + iface.__name__
+
+
+def register_factory(interface, klass, registry=None, name=''):
+    """Register factory for a given interface
+
+    :param interface: the interface for which the factory is registered
+    :param klass: the object factory
+    :param registry: the registry into which factory adapter should be registered; if None, the global
+        registry is used
+    :param name: custom name given to registered factory
+    """
+
+    class Temp(ObjectFactoryAdapter):
+        factory = klass
+
+    if_name = get_interface_name(interface)
+    if name:
+        if_name = '{0}::{1}'.format(if_name, name)
+    if registry is None:
+        registry = get_global_registry()
+    registry.registerAdapter(Temp, name=if_name)
+
+
+class factory_config(object):
+    """Class decorator to declare a default object factory"""
+
+    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):
+            name = settings.get('name', '')
+            provided = settings.get('provided')
+            if not provided:
+                raise TypeError("No provided interface(s) was given for registered factory %r" % ob)
+            if not isinstance(provided, tuple):
+                provided = (provided,)
+
+            config = context.config.with_package(info.module)
+            for interface in provided:
+                if name:
+                    logger.debug("Registering factory {0} for interface {1} with name {2}".format(str(ob),
+                                                                                                  str(interface),
+                                                                                                  name))
+                else:
+                    logger.debug("Registering default factory {0} for interface {1}".format(str(ob), str(interface)))
+                register_factory(interface, ob, config.registry, name)
+
+        info = self.venusian.attach(wrapped, callback, category='pyams_factory', 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
+
+
+def get_object_factory(interface, registry=None, name=''):
+    """Get registered factory for given interface
+
+    :param interface: the interface for which a factory is requested
+    :param registry: the registry into which registered factory should be looked for
+    :param name: name of requested factory
+    :return: the requested object factory, or None if it can't be found
+    """
+    if_name = get_interface_name(interface)
+    if name:
+        if_name = '{0}::{1}'.format(if_name, name)
+    if registry is None:
+        registry = get_global_registry()
+    return registry.queryAdapter(interface, IObjectFactory, name=if_name)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/fanstatic.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,107 @@
+#
+# 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'
+
+from fanstatic import Resource
+from fanstatic.core import NeededResources, render_css, set_resource_file_existence_checking
+from pyramid.path import DottedNameResolver
+from zope.interface import Interface
+
+from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config
+from pyams_utils.interfaces.tales import ITALESExtension
+
+
+def render_js(url, defer=False):
+    return '<script type="text/javascript" src="%s" %s></script>' % (url, 'defer' if defer else '')
+
+
+class ExternalResource(Resource):
+    """Fanstatic external resource"""
+
+    dependency_nr = 0
+
+    def __init__(self, library, path, defer=False, resource_type=None, **kwargs):
+        set_resource_file_existence_checking(False)
+        try:
+            if 'renderer' in kwargs:
+                del kwargs['renderer']
+            if 'bottom' not in kwargs:
+                kwargs['bottom'] = path.endswith('.js')
+            Resource.__init__(self, library, path, renderer=self.render, **kwargs)
+        finally:
+            set_resource_file_existence_checking(True)
+        self.defer = defer
+        if resource_type:
+            self.resource_type = resource_type
+        else:
+            self.resource_type = path.rsplit('.', 1)[1].lower()
+
+    def render(self, library_url):
+        if self.resource_type == 'css':
+            return render_css(self.relpath)
+        elif self.resource_type == 'js':
+            return render_js(self.relpath, self.defer)
+        else:
+            return ''
+
+
+def get_resource_path(resource, signature='--static--', versioning=True):
+    """Get path for given resource"""
+    res = NeededResources(publisher_signature=signature, versioning=versioning)
+    return '{0}/{1}'.format(res.library_url(resource.library), resource.relpath)
+
+
+@adapter_config(name='resource_path', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class FanstaticTalesExtension(ContextRequestViewAdapter):
+    """tales:resource_path() TALES extension
+
+    This TALES extension generates an URL matching a given Fanstatic resource.
+    Resource is given as a string made of package name (in dotted form) followed by a colon and by the resource name.
+
+    For example::
+
+    .. code-block:: html
+
+        <div tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.zmi:pyams_content')" />
+    """
+
+    def render(self, resource):
+        library, resource_name = resource.split(':')
+        resolver = DottedNameResolver()
+        module = resolver.maybe_resolve(library)
+        resource = getattr(module, resource_name)
+        return get_resource_path(resource)
+
+
+@adapter_config(name='need_resource', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class FanstaticNeededResourceTalesExtension(ContextRequestViewAdapter):
+    """tales:need_resource() TALES extension
+
+    This extension generates a call to Fanstatic resource.need() function to include given resource
+    into generated HTML code.
+    Resource is given as a string made of package name (in dotted form) followed by a colon and by the resource name.
+
+    For example::
+
+    .. code-block:: html
+
+        <tal:var define="tales:need_resource('pyams_content.zmi:pyams_content')" />
+    """
+
+    def render(self, resource):
+        library, resource_name = resource.split(':')
+        resolver = DottedNameResolver()
+        module = resolver.maybe_resolve(library)
+        resource = getattr(module, resource_name)
+        resource.need()
+        return ''
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/html.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,132 @@
+#
+# 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 TypeError:
+            self.data += data.decode('utf-8')
+
+    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 = '''<p>This is text with french accents: <strong>é à è ù</strong></p>'''
+    >>> html_to_text(html)
+    'This is text with french accents: é à è ù\\n'
+
+    HTML parser should handle entities correctly:
+
+    >>> html = '''<div><p>Header</p><p>This is an &lt; &#242; &gt; entity.<br /></p></div>'''
+    >>> html_to_text(html)
+    'Header\\nThis is an < ò > entity.\\n\\n'
+
+    >>> 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 <\xa0ò\xa0> 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	Wed Dec 05 12:45:56 2018 +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 locale
+
+
+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
+
+
+def set_locales(config):
+    """Define locale environment variables"""
+    for attr in ('LC_CTYPE', 'LC_COLLATE', 'LC_TIME', 'LC_MONETARY', 'LC_NUMERIC', 'LC_ALL'):
+        value = config.get('pyams.locale.{0}'.format(attr.lower()))
+        if value:
+            locale.setlocale(getattr(locale, attr), value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/include.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,73 @@
+#
+# 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'
+
+from chameleon import PageTemplateFile
+from persistent.interfaces import IPersistent
+from z3c.pt.pagetemplate import PageTemplateFile as Z3cPageTemplateFile
+from zope.annotation.attribute import AttributeAnnotations
+from zope.annotation.interfaces import IAnnotations, IAttributeAnnotatable
+from zope.keyreference.interfaces import IKeyReference
+from zope.keyreference.persistent import KeyReferenceToPersistent
+
+from pyams_utils.container import ParentSelector
+from pyams_utils.context import ContextSelector
+from pyams_utils.i18n import set_locales
+from pyams_utils.request import RequestSelector, get_annotations, get_debug
+from pyams_utils.site import site_factory
+from pyams_utils.tales import ExtensionExpr
+from pyams_utils.url import get_display_context
+from pyams_utils.traversing import NamespaceTraverser
+
+
+def include_package(config):
+    """Pyramid package include"""
+
+    # add translations
+    config.add_translation_dirs('pyams_utils:locales')
+
+    # define locale
+    set_locales(config.registry.settings)
+
+    # 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)
+    config.add_request_method(get_display_context, 'display_context', property=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)
+    config.add_subscriber_predicate('parent_selector', ParentSelector)
+    config.add_subscriber_predicate('request_selector', RequestSelector)
+
+    # load registry components
+    config.registry.registerAdapter(AttributeAnnotations, (IAttributeAnnotatable, ), IAnnotations)
+    config.registry.registerAdapter(KeyReferenceToPersistent, (IPersistent, ), IKeyReference)
+
+    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['tales'] = ExtensionExpr
+    Z3cPageTemplateFile.expression_types['tales'] = ExtensionExpr
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/inherit.py	Wed Dec 05 12:45:56 2018 +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
+
+from zope.interface import implementer, Interface
+from zope.location import Location
+from zope.schema.fieldproperty import FieldProperty
+
+# import interfaces
+from pyams_utils.interfaces.inherit import IInheritInfo
+# import packages
+from pyams_utils.traversing import get_parent
+from pyams_utils.zodb import volatile_property
+
+
+@implementer(IInheritInfo)
+class BaseInheritInfo(Location):
+    """Base inherit class"""
+
+    target_interface = Interface
+    adapted_interface = Interface
+
+    _inherit = FieldProperty(IInheritInfo['inherit'])
+
+    @volatile_property
+    def parent(self):
+        return get_parent(self.__parent__, self.target_interface, allow_context=False)
+
+    @property
+    def can_inherit(self):
+        return self.target_interface.providedBy(self.parent)
+
+    @property
+    def inherit(self):
+        return self._inherit if self.can_inherit else False
+
+    @inherit.setter
+    def inherit(self, value):
+        if self.can_inherit:
+            self._inherit = value
+        del self.parent
+
+    @property
+    def no_inherit(self):
+        return not bool(self.inherit)
+
+    @no_inherit.setter
+    def no_inherit(self, value):
+        self.inherit = not bool(value)
+
+    @property
+    def inherit_from(self):
+        if not self.inherit:
+            return self
+        parent = self.parent
+        while self.adapted_interface(parent).inherit:
+            parent = parent.parent
+        return parent
+
+
+class InheritedFieldProperty(object):
+    """Inherited field property"""
+
+    def __init__(self, field, name=None):
+        if name is None:
+            name = field.__name__
+
+        self.__field = field
+        self.__name = name
+
+    def __get__(self, inst, klass):
+        if inst is None:
+            return self
+        inherit_info = IInheritInfo(inst)
+        if inherit_info.inherit:
+            return getattr(inherit_info.adapted_interface(inherit_info.parent), self.__name)
+        else:
+            return getattr(inst, '_{0}'.format(self.__name))
+
+    def __set__(self, inst, value):
+        inherit_info = IInheritInfo(inst)
+        if not (inherit_info.can_inherit and inherit_info.inherit):
+            setattr(inst, '_{0}'.format(self.__name), value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,119 @@
+#
+# 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 WrongContainedType, NotUnique, SchemaNotFullyImplemented, SchemaNotProvided, \
+    InvalidURI, InvalidId, InvalidDottedName, Unbound, RequiredMissing, WrongType, TooBig, TooSmall, TooLong, \
+    TooShort, InvalidValue, ConstraintNotSatisfied, NotAContainer, NotAnIterator
+
+# import packages
+from zope.interface import Interface
+
+from pyams_utils import _
+
+
+#
+# Custom zope.schema exceptions messages
+#
+
+WrongContainedType.__doc__ = _("""Wrong contained type""")
+NotUnique.__doc__ = _("""One or more entries of sequence are not unique.""")
+SchemaNotFullyImplemented.__doc__ = _("""Schema not fully implemented""")
+SchemaNotProvided.__doc__ = _("""Schema not provided""")
+InvalidURI.__doc__ = _("""The specified URI is not valid.""")
+InvalidId.__doc__ = _("""The specified id is not valid.""")
+InvalidDottedName.__doc__ = _("""The specified dotted name is not valid.""")
+Unbound.__doc__ = _("""The field is not bound.""")
+
+RequiredMissing.__doc__ = _("""Required input is missing.""")
+WrongType.__doc__ = _("""Object is of wrong type.""")
+TooBig.__doc__ = _("""Value is too big""")
+TooSmall.__doc__ = _("""Value is too small""")
+TooLong.__doc__ = _("""Value is too long""")
+TooShort.__doc__ = _("""Value is too short""")
+InvalidValue.__doc__ = _("""Invalid value""")
+ConstraintNotSatisfied.__doc__ = _("""Constraint not satisfied""")
+NotAContainer.__doc__ = _("""Not a container""")
+NotAnIterator.__doc__ = _("""Not an iterator""")
+
+
+#
+# Custom permissions
+#
+
+FORBIDDEN_PERMISSION = 'system.forbidden'
+'''Custom permission which is never granted to any user'''
+
+PUBLIC_PERMISSION = 'public'
+'''Public permission which is granted to every principal'''
+
+VIEW_PERMISSION = 'view'
+'''View permission is a custom permission used to view contents'''
+
+MANAGE_PERMISSION = 'manage'
+'''Permission used to manage basic information; this permission is generally not used by custom contents'''
+
+VIEW_SYSTEM_PERMISSION = 'pyams.ViewSystem'
+'''Permission used to access management screens'''
+
+MANAGE_SYSTEM_PERMISSION = 'pyams.ManageSystem'
+'''Permission used to manage system settings'''
+
+MANAGE_SKIN_PERMISSION = 'pyams.ManageSkin'
+'''Permission used to manage skin'''
+
+MANAGE_SECURITY_PERMISSION = 'pyams.ManageSecurity'
+'''Permission used to manage security settings'''
+
+MANAGE_ROLES_PERMISSION = 'pyams.ManageRoles'
+'''Permission used to manage roles'''
+
+
+#
+# Custom string constants
+#
+
+PYAMS_APPLICATION_SETTINGS_KEY = 'pyams.application_name'
+'''ZODB application name settings key'''
+
+PYAMS_APPLICATION_DEFAULT_NAME = 'application'
+'''ZODB default application name'''
+
+PYAMS_APPLICATION_FACTORY_KEY = 'pyams.application_factory'
+'''Settings key to define site root factory'''
+
+
+class MissingRequestError(Exception):
+    """Error raised when no request is available"""
+
+
+class IObjectFactory(Interface):
+    """Object factory interface
+
+    This interface can be used to register an "interface's object factory".
+    For a given interface, such factory can be used to get an instance of an object providing
+    this interface; several factories can be registered for the same interface if they have distinct
+    names. See :py:mod:`pyams_utils.factory` module.
+    """
+
+
+class ICacheKeyValue(Interface):
+    """Interface used to get string representation of a given object as cache key
+
+    Several default adapters are given for objects (using their "id()"), strings (using string as key)
+    and for persistent objects (using their persistent OID); you are free to provide your own adapters.
+    """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/data.py	Wed Dec 05 12:45:56 2018 +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/inherit.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,45 @@
+#
+# 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 ILocation
+
+# import packages
+from zope.interface import Attribute
+from zope.schema import Bool
+
+from pyams_utils import _
+
+
+class IInheritInfo(ILocation):
+    """Inheritance info"""
+
+    target_interface = Attribute("Parent target interface")
+    adapted_interface = Attribute("Context or parent adapted interface")
+
+    parent = Attribute("First parent supporting target interface")
+
+    can_inherit = Attribute("Can inherit from parent?")
+
+    inherit = Bool(title=_("Inherit from parent?"),
+                   required=False,
+                   default=True)
+
+    no_inherit = Bool(title=_("Don't inherit from parent?"),
+                      required=False,
+                      default=False)
+
+    inherit_from = Attribute("Parent from which adapted interface is inherited")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/intids.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,39 @@
+#
+# 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
+
+# import interfaces
+from zope.interface import Interface
+
+# import packages
+from zope.schema import TextLine, Int
+
+
+#
+# Generic interfaces
+#
+
+class IIndexLength(Interface):
+    """Index length interface"""
+
+    count = Int(title="Indexed elements count")
+
+
+class IUniqueID(Interface):
+    """Interface used to get unique ID of an object"""
+
+    oid = TextLine(title="Unique ID",
+                   description="Globally unique identifier of this object can be used to create internal links",
+                   readonly=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/site.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,60 @@
+#
+# 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 ISiteRootFactory(Interface):
+    """Site root utility factory interface"""
+
+
+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"""
+
+    order = Attribute("Order in which generations should be upgraded")
+    generation = Attribute("Current schema generation")
+
+    def evolve(self, site, current=None):
+        """Evolve database from current generation to last one"""
+
+
+class IConfigurationManager(IAttributeAnnotatable):
+    """Configuration manager marker interface"""
+
+
+class IOptionalUtility(Interface):
+    """Marker interface for site utilities that can be removed"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/size.py	Wed Dec 05 12:45:56 2018 +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	Wed Dec 05 12:45:56 2018 +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	Wed Dec 05 12:45:56 2018 +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	Wed Dec 05 12:45:56 2018 +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/interfaces/traversing.py	Wed Dec 05 12:45:56 2018 +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
+from zope.interface import Interface
+from zope.schema import List, Int
+
+
+class IPathElements(Interface):
+    """Path elements interface"""
+
+    parents = List(title="Element parents",
+                   description="Internal IDs of parents objects",
+                   value_type=Int())
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/tree.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,44 @@
+### -*- coding: utf-8 -*- ####################################################
+##############################################################################
+#
+# Copyright (c) 2012 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.
+#
+##############################################################################
+
+from zope.interface import Interface, Attribute
+
+
+class INode(Interface):
+    """Tree node interface"""
+
+    context = Attribute("Node's context")
+
+    label = Attribute("Node's label")
+
+    css_class = Attribute("Node's CSS class")
+
+    order = Attribute("Node's order")
+
+    def get_level(self):
+        """Get depth level of current node"""
+
+    def has_children(self, filter_value=None):
+        """Check if current node has children"""
+
+    def get_children(self, filter_value=None):
+        """Get list of node children"""
+
+
+class ITree(Interface):
+    """Tree interface"""
+
+    def get_root_nodes(self):
+        """Get list of root nodes"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/url.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2008-2018 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'
+
+from zope.interface import Interface
+
+
+class ICanonicalURL(Interface):
+    """Interface used to get content's canonical URL"""
+
+    def get_url(self, view_name=None, query=None):
+        """Get content's canonical URL"""
+
+
+DISPLAY_CONTEXT = 'pyams_utils.display_context'
+
+
+class IRelativeURL(Interface):
+    """Get content URL based on another context"""
+
+    def get_url(self, display_context=None, view_name=None, query=None):
+        """Get content URL relative to given display context"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/interfaces/zeo.py	Wed Dec 05 12:45:56 2018 +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
+
+# import packages
+from zope.interface import Interface, Attribute
+from zope.schema import TextLine, Int, Password, Bool
+
+from pyams_utils import _
+
+
+class IZEOConnection(Interface):
+    """ZEO connection settings interface"""
+
+    name = TextLine(title=_("Connection name"),
+                    description=_("Registration name of ZEO connection"),
+                    required=True)
+
+    server_name = TextLine(title=_("ZEO server name"),
+                           description=_("Hostname of ZEO server"),
+                           required=True,
+                           default='localhost')
+
+    server_port = Int(title=_("ZEO server port"),
+                      description=_("Port number of ZEO server"),
+                      required=True,
+                      default=8100)
+
+    storage = TextLine(title=_("ZEO server storage"),
+                       description=_("Storage name on ZEO server"),
+                       required=True,
+                       default='1')
+
+    username = TextLine(title=_("ZEO user name"),
+                        description=_("User name on ZEO server; only for ZEO server before 5.0"),
+                        required=False)
+
+    password = Password(title=_("ZEO password"),
+                        description=_("User password on ZEO server; only for ZEO server before 5.0"),
+                        required=False)
+
+    server_realm = TextLine(title=_("ZEO server realm"),
+                            description=_("Realm name on ZEO server; only for ZEO server before 5.0"),
+                            required=False)
+
+    blob_dir = TextLine(title=_("BLOBs directory"),
+                        description=_("Directory path for blob data"),
+                        required=False)
+
+    shared_blob_dir = Bool(title=_("Shared BLOBs directory ?"),
+                           description=_("""Flag whether the blob_dir is a server-shared filesystem """
+                                         """that should be used instead of transferring blob data over zrpc."""),
+                           required=True,
+                           default=False)
+
+    connection = Attribute(_("Opened ZEO connection"))
+
+    def get_settings(self):
+        """Get ZEO connection setting as a JSON dict"""
+
+    def update(self, settings):
+        """Update internal fields with given settings dict"""
+
+    def get_connection(self, wait_timeout=30, get_storage=False):
+        """Open ZEO connection with given settings"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/intids.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,106 @@
+#
+# 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
+
+# import interfaces
+from persistent.interfaces import IPersistent
+from pyams_utils.interfaces.intids import IUniqueID
+from zope.intid.interfaces import IIntIds, IIntIdEvent, IntIdAddedEvent, IntIdRemovedEvent
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
+from zope.location.interfaces import ISublocations
+from zope.keyreference.interfaces import IKeyReference, NotYet
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import get_all_utilities_registered_for, query_utility
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from zope.intid import intIdEventNotify
+from zope.lifecycleevent import ObjectRemovedEvent
+
+
+@adapter_config(context=IPersistent, provides=IUniqueID)
+class UniqueIdAdapter(ContextAdapter):
+    """Object unique ID adapter
+
+    This adapter is based on a registered IIntIds utility to get a unique ID
+    for any persistent object.
+    """
+
+    @property
+    def oid(self):
+        """Get context ID in hexadecimal form"""
+        intids = query_utility(IIntIds)
+        if intids is not None:
+            return hex(intids.queryId(self.context))[2:]
+
+
+@subscriber(IObjectAddedEvent, context_selector=IPersistent)
+def handle_added_object(event):
+    """Notify IntId utility for added objects
+
+    This subscriber is used for all persistent objects to be registered
+    in all locally registered IIntIds utilities.
+    """
+    utilities = tuple(get_all_utilities_registered_for(IIntIds))
+    if utilities:
+        # assert that there are any utilities
+        try:
+            key = IKeyReference(event.object, None)
+        except NotYet:
+            pass
+        else:
+            # Register only objects that adapt to key reference
+            if key is not None:
+                idmap = {}
+                for utility in utilities:
+                    idmap[utility] = utility.register(key)
+                # Notify the catalogs that this object was added.
+                get_current_registry().notify(IntIdAddedEvent(event.object, event, idmap))
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IPersistent)
+def handle_removed_object(event):
+    """Notify IntId utility for removed objects
+
+    This subscriber is used for all persistent objects to be unregistered
+    from all locally registered IIntIds utilities.
+    """
+    registry = get_current_registry()
+    locations = ISublocations(event.object, None)
+    if locations is not None:
+        for location in locations.sublocations():
+            registry.notify(ObjectRemovedEvent(location))
+    utilities = tuple(get_all_utilities_registered_for(IIntIds))
+    if utilities:
+        key = IKeyReference(event.object, None)
+        # Register only objects that adapt to key reference
+        if key is not None:
+            # Notify the catalogs that this object is about to be removed.
+            registry.notify(IntIdRemovedEvent(event.object, event))
+            for utility in utilities:
+                try:
+                    utility.unregister(key)
+                except KeyError:
+                    pass
+
+
+@subscriber(IIntIdEvent)
+def handle_intid_event(event):
+    """Event subscriber used to dispatch all IIntIdEvent events using Pyramid events subscribers to matching
+    subscribers using Zope events
+    """
+    intIdEventNotify(event)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/json.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2008-2018 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
+
+from datetime import date, datetime
+
+# import interfaces
+
+# import packages
+
+
+def default_json_encoder(obj):
+    if isinstance(obj, (date, datetime)):
+        return obj.isoformat()
+    else:
+        return obj
+
+
+json._default_encoder = json.JSONEncoder(skipkeys=False,
+                                         ensure_ascii=True,
+                                         check_circular=True,
+                                         allow_nan=True,
+                                         indent=None,
+                                         separators=None,
+                                         default=default_json_encoder)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/list.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,125 @@
+#
+# 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 itertools import filterfalse
+from random import random, shuffle
+
+# import interfaces
+
+# import packages
+
+
+def unique(seq, key=None):
+    """Extract unique values from list, preserving order
+
+    :param iterator seq: input list
+    :param callable key: an identity function which is used to get 'identity' value of each element
+        in the list
+    :return: list; a new list containing only unique elements of the original list in their initial order.
+        Original list is not modified.
+
+    >>> from pyams_utils.list import unique
+    >>> mylist = [1, 2, 3, 2, 1]
+    >>> unique(mylist)
+    [1, 2, 3]
+
+    >>> mylist = [3, 2, 2, 1, 4, 2]
+    >>> unique(mylist)
+    [3, 2, 1, 4]
+
+    You can also set an 'id' function applied on each element:
+
+    >>> mylist = [1, 2, 3, '2', 4]
+    >>> unique(mylist, key=str)
+    [1, 2, 3, 4]
+    >>> mylist = ['A', 'B', 'b', '2', 4]
+    >>> unique(mylist, key=lambda x: str(x).lower())
+    ['A', 'B', '2', 4]
+    """
+    seen = set()
+    seen_add = seen.add
+    result = []
+    if key is None:
+        for element in filterfalse(seen.__contains__, seq):
+            seen_add(element)
+            result.append(element)
+    else:
+        for element in seq:
+            k = key(element)
+            if k not in seen:
+                seen_add(k)
+                result.append(element)
+    return result
+
+
+def unique_iter(iterable, key=None):
+    """Iterate over iterator values, yielding only unique values
+
+    :param iterator iterable: input iterator
+    :param callable key: an identity function which is used to get 'identity' value of each element
+        in the list
+    :return: an iterator of unique values
+
+    >>> from pyams_utils.list import unique_iter
+    >>> mylist = [1, 2, 3, 2, 1]
+    >>> list(unique_iter(mylist))
+    [1, 2, 3]
+
+    >>> mylist = [3, 2, 2, 1, 4, 2]
+    >>> list(unique_iter(mylist))
+    [3, 2, 1, 4]
+
+    You can also set an 'id' function applied on each element:
+
+    >>> mylist = [1, 2, 3, '2', 4]
+    >>> list(unique_iter(mylist, key=str))
+    [1, 2, 3, 4]
+    >>> mylist = ['A', 'B', 'b', '2', 4]
+    >>> list(unique_iter(mylist, key=lambda x: str(x).lower()))
+    ['A', 'B', '2', 4]
+    """
+    seen = set()
+    seen_add = seen.add
+    if key is None:
+        for element in filterfalse(seen.__contains__, iterable):
+            seen_add(element)
+            yield element
+    else:
+        for element in iterable:
+            k = key(element)
+            if k not in seen:
+                seen_add(k)
+                yield element
+
+
+def random_iter(iterable, limit=1):
+    """Get items randomly from an iterator
+
+    >>> from pyams_utils.list import random_iter
+    >>> mylist = [1, 2, 3, 2, 1]
+    >>> list(random_iter(mylist, 2))
+    [..., ...]
+    """
+    selected = [None] * limit
+    for index, item in enumerate(iterable):
+        if index < limit:
+            selected[index] = item
+        else:
+            selected_index = int(random() * (index+1))
+            if selected_index < limit:
+                selected[selected_index] = item
+    shuffle(selected)
+    return iter(selected)
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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,782 @@
+# 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: 2018-11-30 11:17+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/date.py:94
+msgid "%d/%m/%Y"
+msgstr "%d/%m/%Y"
+
+#: src/pyams_utils/date.py:95
+msgid "%d/%m/%Y - %H:%M"
+msgstr "%d/%m/%Y - %H:%M"
+
+#: src/pyams_utils/date.py:97
+msgid "on %d/%m/%Y"
+msgstr "le %d/%m/%Y"
+
+#: src/pyams_utils/date.py:98
+msgid "on %d/%m/%Y at %H:%M"
+msgstr "le %d/%m/%Y à %H:%M"
+
+#: src/pyams_utils/date.py:156
+#, c-format
+msgid "%d months ago"
+msgstr "Il y a %d mois"
+
+#: src/pyams_utils/date.py:231
+#, c-format
+msgid "%d months"
+msgstr "%d mois"
+
+#: src/pyams_utils/date.py:158
+#, c-format
+msgid "%d weeks ago"
+msgstr "Il y a %d semaines"
+
+#: src/pyams_utils/date.py:233
+#, c-format
+msgid "%d weeks"
+msgstr "%d semaines"
+
+#: src/pyams_utils/date.py:160
+#, c-format
+msgid "%d days ago"
+msgstr "Il y a %d jours"
+
+#: src/pyams_utils/date.py:162
+msgid "the day before yesterday"
+msgstr "avant-hier"
+
+#: src/pyams_utils/date.py:235
+#, c-format
+msgid "%d days"
+msgstr "%d jours"
+
+#: src/pyams_utils/date.py:164
+msgid "yesterday"
+msgstr "hier"
+
+#: src/pyams_utils/date.py:240
+msgid "24 hours"
+msgstr "24 heures"
+
+#: src/pyams_utils/date.py:242
+#, c-format
+msgid "%d day and %d hours"
+msgstr "%d jours et %d heures"
+
+#: src/pyams_utils/date.py:245
+#, c-format
+msgid "%d hours"
+msgstr "%d heures"
+
+#: src/pyams_utils/date.py:168
+#, c-format
+msgid "%d hours ago"
+msgstr "Il y a %d heures"
+
+#: src/pyams_utils/date.py:172
+msgid "less than 5 minutes ago"
+msgstr "Il y a moins de 5 minutes"
+
+#: src/pyams_utils/date.py:249
+#, c-format
+msgid "%d minutes"
+msgstr "%d minutes"
+
+#: src/pyams_utils/date.py:251
+#, c-format
+msgid "%d seconds"
+msgstr "%d secondes"
+
+#: src/pyams_utils/date.py:170
+#, c-format
+msgid "%d minutes ago"
+msgstr "Il y a %d minutes"
+
+#: src/pyams_utils/size.py:69
+msgid "0.0## Gb"
+msgstr "0.0## Go"
+
+#: src/pyams_utils/size.py:61
+msgid "0 bytes"
+msgstr "0 octets"
+
+#: src/pyams_utils/size.py:64
+msgid "0.# Kb"
+msgstr "0.# Ko"
+
+#: src/pyams_utils/size.py:67
+msgid "0.0# Mb"
+msgstr "0.0# Mo"
+
+#: src/pyams_utils/encoding.py:32
+msgid "English (ASCII)"
+msgstr "Anglais (ASCII)"
+
+#: src/pyams_utils/encoding.py:33
+msgid "Traditional Chinese (big5)"
+msgstr "Chinois traditionnel (big5)"
+
+#: src/pyams_utils/encoding.py:34
+msgid "Traditional Chinese (big5hkscs)"
+msgstr "Chinois traditionnel (big5hkscs)"
+
+#: src/pyams_utils/encoding.py:35
+msgid "English (cp037)"
+msgstr "Anglais (cp037)"
+
+#: src/pyams_utils/encoding.py:36
+msgid "Hebrew (cp424)"
+msgstr "Hébreu (cp424)"
+
+#: src/pyams_utils/encoding.py:37
+msgid "English (cp437)"
+msgstr "Anglais (cp437)"
+
+#: src/pyams_utils/encoding.py:38
+msgid "Western Europe (cp500)"
+msgstr "Europe de l'ouest (cp500)"
+
+#: src/pyams_utils/encoding.py:39
+msgid "Arabic (cp720)"
+msgstr "Arabe (cp720)"
+
+#: src/pyams_utils/encoding.py:40
+msgid "Greek (cp737)"
+msgstr "Grec (cp737)"
+
+#: src/pyams_utils/encoding.py:41
+msgid "Baltic languages (cp775)"
+msgstr "Langues baltes (cp775)"
+
+#: src/pyams_utils/encoding.py:42
+msgid "Western Europe (cp850)"
+msgstr "Europe de l'ouest (cp850)"
+
+#: src/pyams_utils/encoding.py:43
+msgid "Central and Eastern Europe (cp852)"
+msgstr "Europe centrale et de l'est (cp852)"
+
+#: src/pyams_utils/encoding.py:44
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)"
+msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (cp855)"
+
+#: src/pyams_utils/encoding.py:45
+msgid "Hebrew (cp856)"
+msgstr "Hébreu (cp856)"
+
+#: src/pyams_utils/encoding.py:46
+msgid "Turkish (cp857)"
+msgstr "Turc (cp857)"
+
+#: src/pyams_utils/encoding.py:47
+msgid "Western Europe (cp858)"
+msgstr "Europe de l'ouest (cp858)"
+
+#: src/pyams_utils/encoding.py:48
+msgid "Portuguese (cp860)"
+msgstr "Portugais (cp860)"
+
+#: src/pyams_utils/encoding.py:49
+msgid "Icelandic (cp861)"
+msgstr "Islandais (cp861)"
+
+#: src/pyams_utils/encoding.py:50
+msgid "Hebrew (cp862)"
+msgstr "Hébreu (cp862)"
+
+#: src/pyams_utils/encoding.py:51
+msgid "Canadian (cp863)"
+msgstr "Canadien (cp863)"
+
+#: src/pyams_utils/encoding.py:52
+msgid "Arabic (cp864)"
+msgstr "Arabe (cp864)"
+
+#: src/pyams_utils/encoding.py:53
+msgid "Danish, Norwegian (cp865)"
+msgstr "Danois, Norvégien (cp865)"
+
+#: src/pyams_utils/encoding.py:54
+msgid "Russian (cp866)"
+msgstr "Russe (cp866)"
+
+#: src/pyams_utils/encoding.py:55
+msgid "Greek (cp869)"
+msgstr "Grec (cp869)"
+
+#: src/pyams_utils/encoding.py:56
+msgid "Thai (cp874)"
+msgstr "Thaï (cp874)"
+
+#: src/pyams_utils/encoding.py:57
+msgid "Greek (cp875)"
+msgstr "Grec (cp875)"
+
+#: src/pyams_utils/encoding.py:58
+msgid "Japanese (cp932)"
+msgstr "Japonais (cp932)"
+
+#: src/pyams_utils/encoding.py:59
+msgid "Korean (cp949)"
+msgstr "Coréen (cp949)"
+
+#: src/pyams_utils/encoding.py:60
+msgid "Traditional Chinese (cp950)"
+msgstr "Chinois traditionnel (cp950)"
+
+#: src/pyams_utils/encoding.py:61
+msgid "Urdu (cp1006)"
+msgstr "Ourdou (cp1006)"
+
+#: src/pyams_utils/encoding.py:62
+msgid "Turkish (cp1026)"
+msgstr "Turc (cp1026)"
+
+#: src/pyams_utils/encoding.py:63
+msgid "Western Europe (cp1140)"
+msgstr "Europe de l'ouest (cp1140)"
+
+#: src/pyams_utils/encoding.py:64
+msgid "Central and Eastern Europe (cp1250)"
+msgstr "Europe centrale et de l'est (cp1250)"
+
+#: src/pyams_utils/encoding.py:65
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)"
+msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (cp1251)"
+
+#: src/pyams_utils/encoding.py:66
+msgid "Western Europe (cp1252)"
+msgstr "Europe de l'ouest (cp1252)"
+
+#: src/pyams_utils/encoding.py:67
+msgid "Greek (cp1253)"
+msgstr "Grec (cp1253)"
+
+#: src/pyams_utils/encoding.py:68
+msgid "Turkish (cp1254)"
+msgstr "Turc (cp1254)"
+
+#: src/pyams_utils/encoding.py:69
+msgid "Hebrew (cp1255)"
+msgstr "Hébreu (cp1255)"
+
+#: src/pyams_utils/encoding.py:70
+msgid "Arabic (cp1256)"
+msgstr "Arabe (cp1256)"
+
+#: src/pyams_utils/encoding.py:71
+msgid "Baltic languages (cp1257)"
+msgstr "Langues baltes (cp1257)"
+
+#: src/pyams_utils/encoding.py:72
+msgid "Vietnamese (cp1258)"
+msgstr "Viernamien (cp1258)"
+
+#: src/pyams_utils/encoding.py:73
+msgid "Japanese (euc_jp)"
+msgstr "Japonais (euc-jp)"
+
+#: src/pyams_utils/encoding.py:74
+msgid "Japanese (euc_jis_2004)"
+msgstr "Japonais (euc-jis-2004)"
+
+#: src/pyams_utils/encoding.py:75
+msgid "Japanese (euc_jisx0213)"
+msgstr "Japonais (euc-jisx0213)"
+
+#: src/pyams_utils/encoding.py:76
+msgid "Korean (euc_kr)"
+msgstr "Coréen (euc-kr)"
+
+#: src/pyams_utils/encoding.py:77
+msgid "Simplified Chinese (gb2312)"
+msgstr "Chinois simplifié (gb2312)"
+
+#: src/pyams_utils/encoding.py:78
+msgid "Unified Chinese (gbk)"
+msgstr "Chinois unifié (gbk)"
+
+#: src/pyams_utils/encoding.py:79
+msgid "Unified Chinese (gb18030)"
+msgstr "Chinois unifié (gb18030)"
+
+#: src/pyams_utils/encoding.py:80
+msgid "Simplified Chinese (hz)"
+msgstr "Chinois simplifié (hz)"
+
+#: src/pyams_utils/encoding.py:81
+msgid "Japanese (iso2022_jp)"
+msgstr "Japonais (iso2022-jp)"
+
+#: src/pyams_utils/encoding.py:82
+msgid "Japanese (iso2022_jp_1)"
+msgstr "Japonais (iso2022-jp-1)"
+
+#: src/pyams_utils/encoding.py:83
+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:84
+msgid "Japanese (iso2022_jp_2004)"
+msgstr "Japonais (iso2022-jp-2004)"
+
+#: src/pyams_utils/encoding.py:85
+msgid "Japanese (iso2022_jp_3)"
+msgstr "Japonais (iso2022-jp-3)"
+
+#: src/pyams_utils/encoding.py:86
+msgid "Japanese (iso2022_jp_ext)"
+msgstr "Japonais (iso2022-jp-ext)"
+
+#: src/pyams_utils/encoding.py:87
+msgid "Korean (iso2022_kr)"
+msgstr "Coréen (iso2022-kr)"
+
+#: src/pyams_utils/encoding.py:88
+msgid "West Europe (latin_1)"
+msgstr "Europe de l'ouest (latin-1)"
+
+#: src/pyams_utils/encoding.py:89
+msgid "Central and Eastern Europe (iso8859_2)"
+msgstr "Europe centrale et de l'est (iso8859-2)"
+
+#: src/pyams_utils/encoding.py:90
+msgid "Esperanto, Maltese (iso8859_3)"
+msgstr "Espéranto, Maltais (iso8859-3)"
+
+#: src/pyams_utils/encoding.py:91
+msgid "Baltic languages (iso8859_4)"
+msgstr "Langues baltes (iso8859-4)"
+
+#: src/pyams_utils/encoding.py:92
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)"
+msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (iso8859-5)"
+
+#: src/pyams_utils/encoding.py:93
+msgid "Arabic (iso8859_6)"
+msgstr "Arabe (iso8859-6)"
+
+#: src/pyams_utils/encoding.py:94
+msgid "Greek (iso8859_7)"
+msgstr "Grec (iso8869-7)"
+
+#: src/pyams_utils/encoding.py:95
+msgid "Hebrew (iso8859_8)"
+msgstr "Hébreu (iso8859-8)"
+
+#: src/pyams_utils/encoding.py:96
+msgid "Turkish (iso8859_9)"
+msgstr "Turc (iso8859-9)"
+
+#: src/pyams_utils/encoding.py:97
+msgid "Nordic languages (iso8859_10)"
+msgstr "Langues nordiques (iso8859-10)"
+
+#: src/pyams_utils/encoding.py:98
+msgid "Baltic languages (iso8859_13)"
+msgstr "Langues baltes (iso8859-13)"
+
+#: src/pyams_utils/encoding.py:99
+msgid "Celtic languages (iso8859_14)"
+msgstr "Langues celtes (iso8859-14)"
+
+#: src/pyams_utils/encoding.py:100
+msgid "Western Europe (iso8859_15)"
+msgstr "Europe de l'ouest (iso8859-15)"
+
+#: src/pyams_utils/encoding.py:101
+msgid "South-Eastern Europe (iso8859_16)"
+msgstr "Europe du sud-est (iso8859-16)"
+
+#: src/pyams_utils/encoding.py:102
+msgid "Korean (johab)"
+msgstr "Coréen (johab)"
+
+#: src/pyams_utils/encoding.py:103
+msgid "Russian (koi8_r)"
+msgstr "Russe (kio8-r)"
+
+#: src/pyams_utils/encoding.py:104
+msgid "Ukrainian (koi8_u)"
+msgstr "Ukrainien (kio8-u)"
+
+#: src/pyams_utils/encoding.py:105
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)"
+msgstr "Bulgare, Biolorusse, Macédonien, Russe, Serve (mac-cyrillic)"
+
+#: src/pyams_utils/encoding.py:106
+msgid "Greek (mac_greek)"
+msgstr "Grec (mac-greek)"
+
+#: src/pyams_utils/encoding.py:107
+msgid "Icelandic (mac_iceland)"
+msgstr "Islandais (mac-iceland)"
+
+#: src/pyams_utils/encoding.py:108
+msgid "Central and Eastern Europe (mac_latin2)"
+msgstr "Europe centrale et de l'ouest (mac-latin2)"
+
+#: src/pyams_utils/encoding.py:109
+msgid "Western Europe (mac_roman)"
+msgstr "Europe de l'Ouest (mac-roman)"
+
+#: src/pyams_utils/encoding.py:110
+msgid "Turkish (mac_turkish)"
+msgstr "Turc (mac-turkish)"
+
+#: src/pyams_utils/encoding.py:111
+msgid "Kazakh (ptcp154)"
+msgstr "Kazak (ptcp154)"
+
+#: src/pyams_utils/encoding.py:112
+msgid "Japanese (shift_jis)"
+msgstr "Japonais (shift_jis)"
+
+#: src/pyams_utils/encoding.py:113
+msgid "Japanese (shift_jis_2004)"
+msgstr "Japonais (shift-jis-2004)"
+
+#: src/pyams_utils/encoding.py:114
+msgid "Japanese (shift_jisx0213)"
+msgstr "Japonais (shift-jisx0213)"
+
+#: src/pyams_utils/encoding.py:115
+msgid "all languages (utf_32)"
+msgstr "toutes les langues (utf-32)"
+
+#: src/pyams_utils/encoding.py:116
+msgid "all languages (utf_32_be)"
+msgstr "toutes les langues (utf-32-be)"
+
+#: src/pyams_utils/encoding.py:117
+msgid "all languages (utf_32_le)"
+msgstr "toutes les langues (utf-32-le)"
+
+#: src/pyams_utils/encoding.py:118
+msgid "all languages (utf_16)"
+msgstr "toutes les langues (utf-16)"
+
+#: src/pyams_utils/encoding.py:119
+msgid "all languages (BMP only - utf_16_be)"
+msgstr "toutes les langues (BMP seulement - utf-16-be"
+
+#: src/pyams_utils/encoding.py:120
+msgid "all languages (BMP only - utf_16_le)"
+msgstr "toutes les langues (BMP seulement - utf-16-le)"
+
+#: src/pyams_utils/encoding.py:121
+msgid "all languages (utf_7)"
+msgstr "toutes les langues (utf-7)"
+
+#: src/pyams_utils/encoding.py:122
+msgid "all languages (utf_8)"
+msgstr "toutes les langues (utf-8)"
+
+#: src/pyams_utils/encoding.py:123
+msgid "all languages (utf_8_sig)"
+msgstr "toutes les langues (utf-8-sig)"
+
+#: src/pyams_utils/schema.py:181
+msgid ""
+"Email address must be entered as « name@domain.name », without '<' and '>' "
+"characters"
+msgstr ""
+"L'adresse est incorrecte, la saisie ne respecte pas la forme « xxx@yyy.com »."
+
+#: src/pyams_utils/schema.py:115
+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:118
+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/pygments.py:106
+msgid "Selected lexer"
+msgstr "Format du code"
+
+#: src/pyams_utils/pygments.py:107
+msgid "Lexer used to format source code"
+msgstr "Lexeur utilisé pour analyser le code source"
+
+#: src/pyams_utils/pygments.py:112
+msgid "Display line numbers?"
+msgstr "Numéros de lignes ?"
+
+#: src/pyams_utils/pygments.py:113
+msgid "If 'no', line numbers will be hidden"
+msgstr "Si 'non', les numéros de lignes ne seront pas affichés"
+
+#: src/pyams_utils/pygments.py:117
+msgid "Lines wrap?"
+msgstr "Sauts de lignes ?"
+
+#: src/pyams_utils/pygments.py:118
+msgid ""
+"If 'yes', lines wraps will be enabled; line numbers will not be displayed if "
+"lines wrap is enabled..."
+msgstr ""
+"Si 'oui', les sauts de lignes seront possibles ; dans ce mode, l'affichage "
+"des numéros de lignes n'est pas possible..."
+
+#: src/pyams_utils/pygments.py:123
+msgid "Color style"
+msgstr "Style de couleur"
+
+#: src/pyams_utils/pygments.py:124
+msgid "Selected color style"
+msgstr "Style de mise en forme de la syntaxe"
+
+#: src/pyams_utils/pygments.py:75
+msgid "Automatic detection"
+msgstr "Détection automatique"
+
+#: src/pyams_utils/widget/decimal.py:31
+msgid "The entered value is not a valid decimal literal."
+msgstr "La valeur saisie n'est pas une valeur décimale correcte."
+
+#: src/pyams_utils/interfaces/text.py:34
+msgid "Renderer name"
+msgstr "Nom de l'outil de rendu"
+
+#: src/pyams_utils/interfaces/inherit.py:37
+msgid "Inherit from parent?"
+msgstr "Hériter du parent ?"
+
+#: src/pyams_utils/interfaces/inherit.py:41
+msgid "Don't inherit from parent?"
+msgstr "Ne pas hériter du parent ?"
+
+#: src/pyams_utils/interfaces/zeo.py:71
+msgid "Opened ZEO connection"
+msgstr "Connexion ZEO"
+
+#: src/pyams_utils/interfaces/zeo.py:30
+msgid "Connection name"
+msgstr "Nom de la connexion"
+
+#: src/pyams_utils/interfaces/zeo.py:31
+msgid "Registration name of ZEO connection"
+msgstr "Nom d'inscription de la connexion ZEO"
+
+#: src/pyams_utils/interfaces/zeo.py:34
+msgid "ZEO server name"
+msgstr "Nom du serveur"
+
+#: src/pyams_utils/interfaces/zeo.py:35
+msgid "Hostname of ZEO server"
+msgstr "Nom DNS (ou adresse IP) du serveur hébergeant la base de données"
+
+#: src/pyams_utils/interfaces/zeo.py:39
+msgid "ZEO server port"
+msgstr "Numéro de port"
+
+#: src/pyams_utils/interfaces/zeo.py:40
+msgid "Port number of ZEO server"
+msgstr "Numéro du port d'écoute du serveur ZEO"
+
+#: src/pyams_utils/interfaces/zeo.py:44
+msgid "ZEO server storage"
+msgstr "Stockage"
+
+#: src/pyams_utils/interfaces/zeo.py:45
+msgid "Storage name on ZEO server"
+msgstr "Nom du stockage correspondant à la base de données"
+
+#: src/pyams_utils/interfaces/zeo.py:49
+msgid "ZEO user name"
+msgstr "Code utilisateur"
+
+#: src/pyams_utils/interfaces/zeo.py:50
+msgid "User name on ZEO server; only for ZEO server before 5.0"
+msgstr ""
+"Code utilisateur nécessaire lorsque le serveur demande une authentification; "
+"pour les serveurs ZEO avant la version 5.0 uniquement!!"
+
+#: src/pyams_utils/interfaces/zeo.py:53
+msgid "ZEO password"
+msgstr "Mot de passe"
+
+#: src/pyams_utils/interfaces/zeo.py:54
+msgid "User password on ZEO server; only for ZEO server before 5.0"
+msgstr ""
+"Mot de passe correspondant au code utilisateur indiqué; pour les serveurs "
+"ZEO avant la version 5.0 uniquement!!"
+
+#: src/pyams_utils/interfaces/zeo.py:57
+msgid "ZEO server realm"
+msgstr "Domaine"
+
+#: src/pyams_utils/interfaces/zeo.py:58
+msgid "Realm name on ZEO server; only for ZEO server before 5.0"
+msgstr ""
+"Nom du domaine d'authentification; pour les serveurs ZEO avant la version "
+"5.0 uniquement!!"
+
+#: src/pyams_utils/interfaces/zeo.py:61
+msgid "BLOBs directory"
+msgstr "Dossier des BLOBs"
+
+#: src/pyams_utils/interfaces/zeo.py:62
+msgid "Directory path for blob data"
+msgstr "Nom du répertoire destiné au stockage des blobs"
+
+#: src/pyams_utils/interfaces/zeo.py:65
+msgid "Shared BLOBs directory ?"
+msgstr "Répertoire partagé ?"
+
+#: src/pyams_utils/interfaces/zeo.py:66
+msgid ""
+"Flag whether the blob_dir is a server-shared filesystem that should be used "
+"instead of transferring blob data over zrpc."
+msgstr ""
+"Indique si le dossier indiqué pour le stockage des BLOBs est un répertoire "
+"partagé avec le serveur ou non ; dans ce cas, les fichiers seront accédés "
+"directement au lieu d'être chargés via ZRPC"
+
+#: src/pyams_utils/interfaces/timezone.py:47
+msgid "Server timezone"
+msgstr "Fuseau horaire du serveur"
+
+#: src/pyams_utils/interfaces/timezone.py:48
+msgid "Default server timezone"
+msgstr "Fuseau horaire par défaut"
+
+#: src/pyams_utils/interfaces/__init__.py:33
+msgid "Wrong contained type"
+msgstr "Type de contenu incorrect"
+
+#: src/pyams_utils/interfaces/__init__.py:34
+msgid "One or more entries of sequence are not unique."
+msgstr "Toutes les entrées de la séquence ne sont pas uniques"
+
+#: src/pyams_utils/interfaces/__init__.py:35
+msgid "Schema not fully implemented"
+msgstr "Schéma non implémenté"
+
+#: src/pyams_utils/interfaces/__init__.py:36
+msgid "Schema not provided"
+msgstr "Schéma non fourni"
+
+#: src/pyams_utils/interfaces/__init__.py:37
+msgid "The specified URI is not valid."
+msgstr ""
+"L'URL indiquée est invalide ; elle doit comprendre le protocole d'accès au "
+"site, comme « http:// » ou « https:// »"
+
+#: src/pyams_utils/interfaces/__init__.py:38
+msgid "The specified id is not valid."
+msgstr "L'ID indiqué est invalide"
+
+#: src/pyams_utils/interfaces/__init__.py:39
+msgid "The specified dotted name is not valid."
+msgstr "Le nom indiqué est invalide"
+
+#: src/pyams_utils/interfaces/__init__.py:40
+msgid "The field is not bound."
+msgstr "Le chanmp n'est pas lié"
+
+#: src/pyams_utils/interfaces/__init__.py:42
+msgid "Required input is missing."
+msgstr "Ce champ est obligatoire"
+
+#: src/pyams_utils/interfaces/__init__.py:43
+msgid "Object is of wrong type."
+msgstr "L'objet est d'un type incorrect"
+
+#: src/pyams_utils/interfaces/__init__.py:44
+msgid "Value is too big"
+msgstr "Valeur trop grande"
+
+#: src/pyams_utils/interfaces/__init__.py:45
+msgid "Value is too small"
+msgstr "Valeur trop petite"
+
+#: src/pyams_utils/interfaces/__init__.py:46
+msgid "Value is too long"
+msgstr "Valeur trop longue"
+
+#: src/pyams_utils/interfaces/__init__.py:47
+msgid "Value is too short"
+msgstr "Valeur trop courte"
+
+#: src/pyams_utils/interfaces/__init__.py:48
+msgid "Invalid value"
+msgstr "Valeur incorrecte"
+
+#: src/pyams_utils/interfaces/__init__.py:49
+msgid "Constraint not satisfied"
+msgstr "Contrainte non satisfaite"
+
+#: src/pyams_utils/interfaces/__init__.py:50
+msgid "Not a container"
+msgstr "Ce n'est pas un conteneur"
+
+#: src/pyams_utils/interfaces/__init__.py:51
+msgid "Not an iterator"
+msgstr "Ce n'est pas un itérateur"
+
+#~ msgid "Add ZEO connection..."
+#~ msgstr "Ajouter une connexion ZEO..."
+
+#~ msgid "Utilities"
+#~ msgstr "Utilitaires"
+
+#~ msgid "Add ZEO connection"
+#~ msgstr "Ajout d'une connexion ZEO"
+
+#~ msgid "Update ZEO connection properties"
+#~ msgstr "Propriétés d'une connexion ZEO"
+
+#~ msgid "Test ZEO connection..."
+#~ msgstr "Tester la connexion ZEO..."
+
+#~ msgid "Test ZEO database connection"
+#~ msgstr "Test de la connexion ZEO"
+
+#~ msgid "Close"
+#~ msgstr "Fermer"
+
+#~ msgid "Test connection"
+#~ msgstr "Tester la connexion"
+
+#~ msgid "Specified connection name is already used!"
+#~ msgstr "Ce nom de connexion est déjà utilisé !"
+
+#~ msgid "A ZEO connection is already registered with this name!"
+#~ msgstr "Une connexion ZEO est déjà enregistrées avec ce nom !"
+
+#~ msgid "ZEO: {0}"
+#~ msgstr "ZEO : {0}"
+
+#~ msgid "ZEO connection: {0}"
+#~ msgstr "Connexion ZEO : {0}"
+
+#~ msgid "Update server timezone properties"
+#~ msgstr "Fuseau horaire du serveur"
+
+#~ msgid "User name on ZEO server"
+#~ msgstr ""
+#~ "Code utilisateur nécessaire lorsque le serveur demande une "
+#~ "authentification"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/locales/pyams_utils.pot	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,722 @@
+#
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2018-11-30 11:17+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.10.dev0\n"
+
+#: ./src/pyams_utils/date.py:94
+msgid "%d/%m/%Y"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:95
+msgid "%d/%m/%Y - %H:%M"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:97
+msgid "on %d/%m/%Y"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:98
+msgid "on %d/%m/%Y at %H:%M"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:156
+#, c-format
+msgid "%d months ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:231
+#, c-format
+msgid "%d months"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:158
+#, c-format
+msgid "%d weeks ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:233
+#, c-format
+msgid "%d weeks"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:160
+#, c-format
+msgid "%d days ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:162
+msgid "the day before yesterday"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:235
+#, c-format
+msgid "%d days"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:164
+msgid "yesterday"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:240
+msgid "24 hours"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:242
+#, c-format
+msgid "%d day and %d hours"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:245
+#, c-format
+msgid "%d hours"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:168
+#, c-format
+msgid "%d hours ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:172
+msgid "less than 5 minutes ago"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:249
+#, c-format
+msgid "%d minutes"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:251
+#, c-format
+msgid "%d seconds"
+msgstr ""
+
+#: ./src/pyams_utils/date.py:170
+#, c-format
+msgid "%d minutes ago"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:69
+msgid "0.0## Gb"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:61
+msgid "0 bytes"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:64
+msgid "0.# Kb"
+msgstr ""
+
+#: ./src/pyams_utils/size.py:67
+msgid "0.0# Mb"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:32
+msgid "English (ASCII)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:33
+msgid "Traditional Chinese (big5)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:34
+msgid "Traditional Chinese (big5hkscs)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:35
+msgid "English (cp037)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:36
+msgid "Hebrew (cp424)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:37
+msgid "English (cp437)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:38
+msgid "Western Europe (cp500)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:39
+msgid "Arabic (cp720)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:40
+msgid "Greek (cp737)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:41
+msgid "Baltic languages (cp775)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:42
+msgid "Western Europe (cp850)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:43
+msgid "Central and Eastern Europe (cp852)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:44
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:45
+msgid "Hebrew (cp856)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:46
+msgid "Turkish (cp857)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:47
+msgid "Western Europe (cp858)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:48
+msgid "Portuguese (cp860)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:49
+msgid "Icelandic (cp861)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:50
+msgid "Hebrew (cp862)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:51
+msgid "Canadian (cp863)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:52
+msgid "Arabic (cp864)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:53
+msgid "Danish, Norwegian (cp865)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:54
+msgid "Russian (cp866)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:55
+msgid "Greek (cp869)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:56
+msgid "Thai (cp874)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:57
+msgid "Greek (cp875)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:58
+msgid "Japanese (cp932)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:59
+msgid "Korean (cp949)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:60
+msgid "Traditional Chinese (cp950)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:61
+msgid "Urdu (cp1006)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:62
+msgid "Turkish (cp1026)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:63
+msgid "Western Europe (cp1140)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:64
+msgid "Central and Eastern Europe (cp1250)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:65
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:66
+msgid "Western Europe (cp1252)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:67
+msgid "Greek (cp1253)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:68
+msgid "Turkish (cp1254)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:69
+msgid "Hebrew (cp1255)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:70
+msgid "Arabic (cp1256)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:71
+msgid "Baltic languages (cp1257)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:72
+msgid "Vietnamese (cp1258)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:73
+msgid "Japanese (euc_jp)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:74
+msgid "Japanese (euc_jis_2004)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:75
+msgid "Japanese (euc_jisx0213)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:76
+msgid "Korean (euc_kr)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:77
+msgid "Simplified Chinese (gb2312)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:78
+msgid "Unified Chinese (gbk)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:79
+msgid "Unified Chinese (gb18030)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:80
+msgid "Simplified Chinese (hz)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:81
+msgid "Japanese (iso2022_jp)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:82
+msgid "Japanese (iso2022_jp_1)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:83
+msgid ""
+"Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:84
+msgid "Japanese (iso2022_jp_2004)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:85
+msgid "Japanese (iso2022_jp_3)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:86
+msgid "Japanese (iso2022_jp_ext)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:87
+msgid "Korean (iso2022_kr)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:88
+msgid "West Europe (latin_1)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:89
+msgid "Central and Eastern Europe (iso8859_2)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:90
+msgid "Esperanto, Maltese (iso8859_3)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:91
+msgid "Baltic languages (iso8859_4)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:92
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:93
+msgid "Arabic (iso8859_6)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:94
+msgid "Greek (iso8859_7)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:95
+msgid "Hebrew (iso8859_8)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:96
+msgid "Turkish (iso8859_9)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:97
+msgid "Nordic languages (iso8859_10)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:98
+msgid "Baltic languages (iso8859_13)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:99
+msgid "Celtic languages (iso8859_14)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:100
+msgid "Western Europe (iso8859_15)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:101
+msgid "South-Eastern Europe (iso8859_16)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:102
+msgid "Korean (johab)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:103
+msgid "Russian (koi8_r)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:104
+msgid "Ukrainian (koi8_u)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:105
+msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:106
+msgid "Greek (mac_greek)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:107
+msgid "Icelandic (mac_iceland)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:108
+msgid "Central and Eastern Europe (mac_latin2)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:109
+msgid "Western Europe (mac_roman)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:110
+msgid "Turkish (mac_turkish)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:111
+msgid "Kazakh (ptcp154)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:112
+msgid "Japanese (shift_jis)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:113
+msgid "Japanese (shift_jis_2004)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:114
+msgid "Japanese (shift_jisx0213)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:115
+msgid "all languages (utf_32)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:116
+msgid "all languages (utf_32_be)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:117
+msgid "all languages (utf_32_le)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:118
+msgid "all languages (utf_16)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:119
+msgid "all languages (BMP only - utf_16_be)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:120
+msgid "all languages (BMP only - utf_16_le)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:121
+msgid "all languages (utf_7)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:122
+msgid "all languages (utf_8)"
+msgstr ""
+
+#: ./src/pyams_utils/encoding.py:123
+msgid "all languages (utf_8_sig)"
+msgstr ""
+
+#: ./src/pyams_utils/schema.py:181
+msgid ""
+"Email address must be entered as « name@domain.name », without '<' and '>' "
+"characters"
+msgstr ""
+
+#: ./src/pyams_utils/schema.py:115
+msgid "Color length must be 3 or 6 characters"
+msgstr ""
+
+#: ./src/pyams_utils/schema.py:118
+msgid ""
+"Color value must contain only valid hexadecimal color codes (numbers or "
+"letters between 'A' end 'F')"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:106
+msgid "Selected lexer"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:107
+msgid "Lexer used to format source code"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:112
+msgid "Display line numbers?"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:113
+msgid "If 'no', line numbers will be hidden"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:117
+msgid "Lines wrap?"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:118
+msgid ""
+"If 'yes', lines wraps will be enabled; line numbers will not be displayed if "
+"lines wrap is enabled..."
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:123
+msgid "Color style"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:124
+msgid "Selected color style"
+msgstr ""
+
+#: ./src/pyams_utils/pygments.py:75
+msgid "Automatic detection"
+msgstr ""
+
+#: ./src/pyams_utils/widget/decimal.py:31
+msgid "The entered value is not a valid decimal literal."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/text.py:34
+msgid "Renderer name"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/inherit.py:37
+msgid "Inherit from parent?"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/inherit.py:41
+msgid "Don't inherit from parent?"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:71
+msgid "Opened ZEO connection"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:30
+msgid "Connection name"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:31
+msgid "Registration name of ZEO connection"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:34
+msgid "ZEO server name"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:35
+msgid "Hostname of ZEO server"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:39
+msgid "ZEO server port"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:40
+msgid "Port number of ZEO server"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:44
+msgid "ZEO server storage"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:45
+msgid "Storage name on ZEO server"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:49
+msgid "ZEO user name"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:50
+msgid "User name on ZEO server; only for ZEO server before 5.0"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:53
+msgid "ZEO password"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:54
+msgid "User password on ZEO server; only for ZEO server before 5.0"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:57
+msgid "ZEO server realm"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:58
+msgid "Realm name on ZEO server; only for ZEO server before 5.0"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:61
+msgid "BLOBs directory"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:62
+msgid "Directory path for blob data"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:65
+msgid "Shared BLOBs directory ?"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/zeo.py:66
+msgid ""
+"Flag whether the blob_dir is a server-shared filesystem that should be used "
+"instead of transferring blob data over zrpc."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/timezone.py:47
+msgid "Server timezone"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/timezone.py:48
+msgid "Default server timezone"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:33
+msgid "Wrong contained type"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:34
+msgid "One or more entries of sequence are not unique."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:35
+msgid "Schema not fully implemented"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:36
+msgid "Schema not provided"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:37
+msgid "The specified URI is not valid."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:38
+msgid "The specified id is not valid."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:39
+msgid "The specified dotted name is not valid."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:40
+msgid "The field is not bound."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:42
+msgid "Required input is missing."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:43
+msgid "Object is of wrong type."
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:44
+msgid "Value is too big"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:45
+msgid "Value is too small"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:46
+msgid "Value is too long"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:47
+msgid "Value is too short"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:48
+msgid "Invalid value"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:49
+msgid "Constraint not satisfied"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:50
+msgid "Not a container"
+msgstr ""
+
+#: ./src/pyams_utils/interfaces/__init__.py:51
+msgid "Not an iterator"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/lock.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,97 @@
+#
+# 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 time
+from threading import local
+
+# import interfaces
+
+# import packages
+from beaker import cache
+
+
+_local = local()
+
+
+def get_locks_cache():
+    """Get locks shared cache"""
+    try:
+        locks_cache = _local.locks_cache
+    except AttributeError:
+        manager = cache.CacheManager(**cache.cache_regions['persistent'])
+        locks_cache = _local.locks_cache = manager.get_cache('PyAMS::locks')
+    return locks_cache
+
+
+class LockException(Exception):
+    """Cache lock exception"""
+
+
+class CacheLock(object):
+    """Beaker based lock
+
+    This lock can be used when you need to get a lot across several processes or even computers.
+    The lock relies on a shared value stored into a shared Beaker cache.
+
+    :param str name: name of the lock to use as shared key
+    :param boolean wait: if *False*, a *LockException* is raised if lock can't be taken; otherwise,
+        application waits until lock is released
+
+    Lock can be used as a context manager.
+    """
+
+    def __init__(self, name, wait=True):
+        self.key = 'PyAMS::lock::{0}'.format(name)
+        self.wait = wait
+        self.has_lock = False
+
+    def __enter__(self):
+        locks_cache = get_locks_cache()
+        while True:
+            test = locks_cache.has_key(self.key)
+            if test:
+                if not self.wait:
+                    raise LockException()
+                else:
+                    time.sleep(0.1)
+            else:
+                locks_cache.set_value(self.key, 1)
+                self.has_lock = True
+                return
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if self.has_lock:
+            get_locks_cache().remove_value(self.key)
+        return False
+
+
+def locked(name, wait=True):
+    """Locked function decorator
+
+    Can be used with any function or method which requires a global shared lock.
+
+    :param str name: name of the lock to use as shared key
+    :param boolean wait: if *False*, a *LockException* is raised if lock can't be taken; otherwise,
+        application waits until lock is released
+    """
+
+    def lock_decorator(func):
+        def wrapper(*args, **kwargs):
+            with CacheLock(name, wait):
+                return func(*args, **kwargs)
+        return wrapper
+
+    return lock_decorator
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/progress.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,147 @@
+#
+# 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
+from threading import local
+
+# import interfaces
+
+# import packages
+from beaker import cache
+from pyams_utils.lock import locked
+from pyramid.httpexceptions import HTTPBadRequest
+from pyramid.view import view_config
+
+
+_local = local()
+
+
+PROGRESS_CACHE_NAME = 'PyAMS::progress'
+PROGRESS_TASKS_CACHE_KEY = 'PyAMS::progress::running_tasks'
+PROGRESS_LOCK_NAME = 'tasks_progress_lock'
+PROGRESS_TASK_KEY = 'PyAMS::progress::task::{0}'
+
+
+def get_tasks_cache():
+    """Get cache storing tasks list"""
+    try:
+        tasks_cache = _local.running_tasks_cache
+    except AttributeError:
+        manager = cache.CacheManager(**cache.cache_regions['persistent'])
+        tasks_cache = _local.running_tasks_cache = manager.get_cache(PROGRESS_CACHE_NAME)
+    return tasks_cache
+
+
+def get_progress_cache():
+    """Get cache storing tasks progress"""
+    try:
+        local_cache = _local.progress_cache
+    except AttributeError:
+        manager = cache.CacheManager(**cache.cache_regions['default'])
+        local_cache = _local.progress_cache = manager.get_cache(PROGRESS_CACHE_NAME)
+    return local_cache
+
+
+def get_running_tasks():
+    """Get list of running tasks"""
+    tasks_cache = get_tasks_cache()
+    return tasks_cache.get_value(PROGRESS_TASKS_CACHE_KEY, createfunc=set)
+
+
+def set_running_tasks(tasks):
+    """Update list of running tasks"""
+    tasks_cache = get_tasks_cache()
+    tasks_cache.set_value(PROGRESS_TASKS_CACHE_KEY, tasks)
+
+
+@locked(name=PROGRESS_LOCK_NAME)
+def init_progress_status(progress_id, owner, label, tags=None, length=None, current=None):
+    """Initialize progress status for given task
+
+    :param str progress_id: task ID
+    :param str owner: user ID associated with this task
+    :param str label: label associated with this task
+    :param tags: list of tags associated with given task
+    :param int length: whole length of the given task, if available
+    :param int current: current position in the whole task length, if available
+    """
+    status = {'status': 'running',
+              'owner': owner,
+              'label': label,
+              'tags': tags,
+              'length': length,
+              'current': current,
+              'started': datetime.utcnow()}
+    # Store task status
+    cache_key = PROGRESS_TASK_KEY.format(progress_id)
+    progress_cache = get_progress_cache()
+    progress_cache.set_value(cache_key, status)
+    # Store task in running tasks list
+    tasks = get_running_tasks()
+    tasks.add(progress_id)
+    set_running_tasks(tasks)
+
+
+@locked(name=PROGRESS_LOCK_NAME)
+def get_progress_status(progress_id):
+    """Get status of given task"""
+    progress_cache = get_progress_cache()
+    cache_key = PROGRESS_TASK_KEY.format(progress_id)
+    try:
+        status = progress_cache.get_value(cache_key)
+    except KeyError:
+        status = {'status': 'unknown'}
+    else:
+        if status.get('status') == 'finished':
+            progress_cache.remove_value(cache_key)
+            tasks = get_running_tasks()
+            if progress_id in tasks:
+                tasks.remove(progress_id)
+    return status
+
+
+@locked(name=PROGRESS_LOCK_NAME)
+def set_progress_status(progress_id, status='running', message=None, length=None, current=None):
+    """Set status of given task
+
+    :param str progress_id: task ID
+    :param str status: new status of the given task
+    :param str message: status message associated with the given task
+    :param int length: whole length of the given task, if available
+    :param int current: current position in the whole task length, if available
+    """
+    progress_cache = get_progress_cache()
+    cache_key = PROGRESS_TASK_KEY.format(progress_id)
+    try:
+        task_status = progress_cache.get_value(cache_key)
+    except KeyError:
+        task_status = {'status': 'unknown'}
+    task_status.update({'status': status,
+                        'message': message,
+                        'length': length,
+                        'current': current})
+    progress_cache.set_value(cache_key, task_status)
+
+
+@view_config(name='get_progress_status.json', renderer='JSON', xhr=True)
+def get_progress_status_view(request):
+    """Get progress status of a given task
+
+    Each submitted task is identified by an ID defined when the task is created
+    """
+    if 'progress_id' not in request.params:
+        raise HTTPBadRequest("Missing argument")
+    return get_progress_status(request.params['progress_id'])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/property.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,118 @@
+#
+# 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 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
+
+
+class classproperty:
+    """Same decorator as property(), but passes obj.__class__ instead of obj to fget/fset/fdel.
+    
+    Original code for property emulation:
+    https://docs.python.org/3.5/howto/descriptor.html#properties
+    """
+    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
+        self.fget = fget
+        self.fset = fset
+        self.fdel = fdel
+        if doc is None and fget is not None:
+            doc = fget.__doc__
+        self.__doc__ = doc
+
+    def __get__(self, obj, objtype=None):
+        if obj is None:
+            return self
+        if self.fget is None:
+            raise AttributeError("Unreadable attribute")
+        return self.fget(obj.__class__)
+
+    def __set__(self, obj, value):
+        if self.fset is None:
+            raise AttributeError("Can't set attribute")
+        self.fset(obj.__class__, value)
+
+    def __delete__(self, obj):
+        if self.fdel is None:
+            raise AttributeError("Can't delete attribute")
+        self.fdel(obj.__class__)
+
+    def getter(self, fget):
+        return type(self)(fget, self.fset, self.fdel, self.__doc__)
+
+    def setter(self, fset):
+        return type(self)(self.fget, fset, self.fdel, self.__doc__)
+
+    def deleter(self, fdel):
+        return type(self)(self.fget, self.fset, fdel, self.__doc__)
+
+
+def classproperty_support(cls):
+    """Class decorator to add metaclass to a class.
+    
+    Metaclass uses to add descriptors to class attributes
+    """
+    class Meta(type):
+        pass
+
+    for name, obj in vars(cls).items():
+        if isinstance(obj, classproperty):
+            setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel))
+
+    class Wrapper(cls, metaclass=Meta):
+        pass
+    return Wrapper
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/protocol/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/protocol/http.py	Wed Dec 05 12:45:56 2018 +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 with proxy support"""
+
+    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 and (len(self.proxy) == 2):
+            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/tcp.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,21 @@
+#
+# Copyright (c) 2008-2018 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 socket
+
+
+def is_port_in_use(port, hostname='localhost'):
+    """Check if given port is already used locally"""
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+        return s.connect_ex((hostname, port)) == 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/protocol/xmlrpc.py	Wed Dec 05 12:45:56 2018 +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 base64
+import http.client
+import http.cookiejar
+import socket
+import urllib.request
+import xmlrpc.client
+
+try:
+    import gzip
+except ImportError:
+    gzip = None #python can be built without zlib/gzip support
+
+# import interfaces
+
+# import packages
+
+
+class XMLRPCCookieAuthTransport(xmlrpc.client.Transport):
+    """An XML-RPC transport handling authentication via cookies"""
+
+    _http_connection = http.client.HTTPConnection
+    verbose = False
+
+    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 request(self, host, handler, request_body, verbose=False):
+        self.verbose = verbose
+        # issue XML-RPC request
+        connection = self.send_request(host, handler, request_body, verbose)
+        # get response
+        return self.get_response(connection, host, handler)
+
+    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]
+
+    def send_request(self, host, handler, request_body, debug):
+        connection = self.make_connection(host)
+        headers = self._extra_headers[:]
+        if debug:
+            connection.set_debuglevel(1)
+        if self.accept_gzip_encoding and gzip:
+            connection.putrequest("POST", handler, skip_accept_encoding=True)
+            headers.append(("Accept-Encoding", "gzip"))
+        else:
+            connection.putrequest("POST", handler)
+        self.send_auth(connection)
+        self.send_content_type(connection)
+        self.send_user_agent(connection)
+        self.send_headers(connection, headers)
+        self.send_content(connection, request_body)
+        return connection
+
+    # override the send_host hook to also send authentication info
+    def send_auth(self, connection):
+        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:
+            creds = base64.encodebytes(("%s:%s" % self.credentials).encode()).strip().decode()
+            auth = 'Basic %s' % creds
+            connection.putheader('Authorization', auth)
+
+    # send content type
+    def send_content_type(self, connection):
+        connection.putheader('Content-Type', 'text/xml')
+
+    # 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 {}).items():
+            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 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)
+            for cookie in self.cookies.make_cookies(cresponse, crequest):
+                if cookie.name.startswith('Set-Cookie'):
+                    cookie.name = cookie.name.split(': ', 1)[1]
+                self.cookies.set_cookie(cookie)
+        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/pygments.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,152 @@
+#
+# Copyright (c) 2008-2018 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'
+
+from fanstatic import get_library_registry
+from persistent import Persistent
+from pygments import highlight
+from pygments.formatters.html import HtmlFormatter
+from pygments.lexers import get_all_lexers, get_lexer_by_name, guess_lexer
+from pygments.styles import get_all_styles
+from pyramid.response import Response
+from pyramid.view import view_config
+from zope.container.contained import Contained
+from zope.interface import Interface, implementer
+from zope.schema import Bool, Choice
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
+
+from pyams_utils.factory import factory_config
+from pyams_utils.fanstatic import ExternalResource
+from pyams_utils.list import unique_iter
+from pyams_utils.vocabulary import vocabulary_config
+
+from pyams_utils import _
+
+
+#
+# Pygments CSS view
+#
+
+for library in get_library_registry().values():
+    break
+else:
+    try:
+        from pyams_skin import library
+    except ImportError:
+        from pyams_default_theme import library
+
+
+pygments_css = ExternalResource(library, 'get-pygments-style.css', resource_type='css')
+
+
+@view_config(name='get-pygments-style.css')
+def get_pygments_style_view(request):
+    style = request.params.get('style', 'default')
+    styles = HtmlFormatter(linenos='inline',
+                           nowrap=False,
+                           cssclass='source',
+                           style=style).get_style_defs()
+    return Response(styles, content_type='text/css')
+
+
+#
+# Pygments lexers
+#
+
+PYGMENTS_LEXERS_VOCABULARY = 'Pygments lexers vocabulary'
+
+
+@vocabulary_config(name=PYGMENTS_LEXERS_VOCABULARY)
+class PygmentsLexersVocabulary(SimpleVocabulary):
+    """Pygments lexers vocabulary"""
+
+    def __init__(self, context):
+        terms = [SimpleTerm('auto', title=_("Automatic detection"))]
+        for name, aliases, filetypes, mimetypes in sorted(unique_iter(get_all_lexers(),
+                                                                      key=lambda x: x[0].lower()),
+                                                          key=lambda x: x[0].lower()):
+            terms.append(SimpleTerm(aliases[0] if len(aliases) > 0 else name,
+                                    title='{0}{1}'.format(name,
+                                                          ' ({})'.format(', '.join(filetypes)) if filetypes else '')))
+        super(PygmentsLexersVocabulary, self).__init__(terms)
+
+
+PYGMENTS_STYLES_VOCABULARY = 'Pygments styles vocabulary'
+
+
+@vocabulary_config(name=PYGMENTS_STYLES_VOCABULARY)
+class PygmentsStylesVocabulary(SimpleVocabulary):
+    """Pygments styles vocabulary"""
+
+    def __init__(self, context):
+        terms = []
+        for name in sorted(get_all_styles()):
+            terms.append(SimpleTerm(name))
+        super(PygmentsStylesVocabulary, self).__init__(terms)
+
+
+#
+# Pygments configuration
+#
+
+class IPygmentsCodeConfiguration(Interface):
+    """Pygments html formatter options"""
+
+    lexer = Choice(title=_("Selected lexer"),
+                   description=_("Lexer used to format source code"),
+                   required=True,
+                   vocabulary=PYGMENTS_LEXERS_VOCABULARY,
+                   default='auto')
+
+    display_linenos = Bool(title=_("Display line numbers?"),
+                           description=_("If 'no', line numbers will be hidden"),
+                           required=True,
+                           default=True)
+
+    disable_wrap = Bool(title=_("Lines wrap?"),
+                        description=_("If 'yes', lines wraps will be enabled; line numbers will not be "
+                                      "displayed if lines wrap is enabled..."),
+                        required=True,
+                        default=False)
+
+    style = Choice(title=_("Color style"),
+                   description=_("Selected color style"),
+                   required=True,
+                   vocabulary=PYGMENTS_STYLES_VOCABULARY,
+                   default='default')
+
+
+@factory_config(provided=IPygmentsCodeConfiguration)
+@implementer(IPygmentsCodeConfiguration)
+class PygmentsCodeRendererSettings(Persistent, Contained):
+    """Pygments code renderer settings"""
+
+    lexer = FieldProperty(IPygmentsCodeConfiguration['lexer'])
+    display_linenos = FieldProperty(IPygmentsCodeConfiguration['display_linenos'])
+    disable_wrap = FieldProperty(IPygmentsCodeConfiguration['disable_wrap'])
+    style = FieldProperty(IPygmentsCodeConfiguration['style'])
+
+
+def render_source(code: str, settings: IPygmentsCodeConfiguration):
+    """Render source with given settings"""
+    if settings.lexer == 'auto':
+        lexer = guess_lexer(code)
+    else:
+        lexer = get_lexer_by_name(settings.lexer)
+    if lexer is not None:
+        formatter = HtmlFormatter(linenos='inline' if settings.display_linenos else None,
+                                  nowrap=settings.disable_wrap,
+                                  cssclass='source',
+                                  style=settings.style)
+        return highlight(code, lexer, formatter)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/registry.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,261 @@
+#
+# 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.
+#
+
+__doc__ = """Registry management package
+
+This package is used to manage *local registry*. A local registry is a *site management* component
+created automatically on application startup by PyAMS_utils package. It can be used to store and register
+components, mainly utilities which are created and configured dynamically by a site administrator; this can include
+SQLAlchemy engines, ZEO connections, and several PyAMS utilities like security manager, medias converter,
+tasks scheduler and many other ones.
+
+See :ref:`zca` to get a brief introduction about using a local registry with PyAMS packages.
+"""
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import logging
+logger = logging.getLogger('PyAMS (utils)')
+
+import threading
+import venusian
+
+# import interfaces
+from pyramid.interfaces import INewRequest
+from zope.component.interfaces import ComponentLookupError, ISite
+from zope.traversing.interfaces import IBeforeTraverseEvent
+
+# import packages
+from pyramid.events import subscriber
+from pyramid.threadlocal import manager, get_current_registry as get_request_registry
+from ZODB.POSException import POSError
+from zope.component.globalregistry import getGlobalSiteManager
+from zope.interface import implementedBy, providedBy
+
+
+class LocalRegistry(threading.local):
+    """Local registry
+
+    The local registry is defined to allow access to persistent utility
+    registered and stored into ZODB.
+    """
+
+    _registry = None
+
+    def get_registry(self):
+        return self._registry
+
+    def set_registry(self, registry):
+        self._registry = registry
+
+local_registry = LocalRegistry()
+
+
+def get_local_registry():
+    """Get local registry
+
+    Local registry is automatically defined while traversing a site manager.
+    """
+    return local_registry.get_registry()
+
+
+def set_local_registry(registry):
+    """Define local registry"""
+    local_registry.set_registry(registry)
+
+
+@subscriber(INewRequest)
+def handle_new_request(event):
+    """New request event subscriber
+
+    Is used to initialize local registry to None for any new request
+    """
+    set_local_registry(None)
+
+
+@subscriber(IBeforeTraverseEvent, context_selector=ISite)
+def handle_site_before_traverse(event):
+    """Before traverse event subscriber
+
+    Define site's local registry when an object implementing ISite is traversed
+    """
+    set_local_registry(event.object.getSiteManager())
+
+
+def get_registries():
+    """Iterator on components registries
+
+    Returns an iterator on current local registry (if any) and registries associated
+    in current thread stack.
+    """
+    registry = local_registry.get_registry()
+    if registry is not None:
+        yield registry
+    for entry in reversed(manager.stack):
+        registry = entry.get('registry')
+        if registry is not None:
+            yield registry
+
+
+def get_global_registry():
+    """Get global registry"""
+    return getGlobalSiteManager()
+
+
+def get_current_registry(context=None):
+    """Get current or global registry
+
+    The function is looking for given request registry.
+    If registry is None, returns the global registry.
+    """
+    registry = get_request_registry(context)
+    if registry is None:
+        registry = get_global_registry()
+    return registry
+
+
+def registered_utilities():
+    """Get utilities registrations as generator
+
+    Iterates over utilities defined in all registries, starting with local ones.
+    """
+    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
+
+    Do a registry lookup for given utility into local registry first, then on each registry
+    associated with current thread stack.
+
+    :param Interface provided: the requested interface
+    :param str name: name of the requested utility
+    :param object default: the default object returned if the requested utility can't be found
+    :return: object; the requested object, or *default* if it can't be found
+    """
+    try:
+        for registry in get_registries():
+            utility = registry.queryUtility(provided, name, default)
+            if utility is not None:
+                return utility
+    except POSError:
+        pass
+    return default
+
+
+def get_utility(provided, name=''):
+    """Get utility registered with given interface
+
+    Do a registry lookup for given utility into local registry first, then on each registry
+    associated with current thread stack.
+
+    :param Interface provided: the requested interface
+    :param str name: name of the requested utility
+    :return: object; the requested object. A *ComponentLookupError* is raised if the utility
+        can't be found.
+    """
+    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) tuples iterator
+
+    Do a registry lookup for matching utilities into local registry first, then on each registry
+    associated with current thread stack.
+    """
+    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
+
+    Do a registry lookup for matching utilities into local registry first, then on each registry
+    associated with current thread stack.
+    """
+    result = []
+    for registry in get_registries():
+        for utilities in registry.getAllUtilitiesRegisteredFor(interface):
+            result.append(utilities)
+    return result
+
+
+class utility_config(object):
+    """Function or class decorator to register a utility in the global registry
+
+    :param str name: default=''; name under which the utility is registered
+    :param Interface provides: the interface for which the utility is registered
+
+    Please note that a single utility can be registered several times (using several annotations), with
+    different names.
+
+    If several utilities are registered for the same interface with the same name, the last registered
+    utility will override the previous ones.
+    """
+
+    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)
+            logger.debug("Registering utility {0} named '{1}' providing {2}".format(
+                str(component) if component else str(factory),
+                settings.get('name', ''),
+                str(provides)))
+            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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,241 @@
+#
+# 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 logging
+logger = logging.getLogger('PyAMS (utils)')
+
+# import interfaces
+from pyams_utils.interfaces import MissingRequestError, ICacheKeyValue
+from pyramid.interfaces import IAuthenticationPolicy, IAuthorizationPolicy
+from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations
+
+# import packages
+from pyams_utils.registry import get_global_registry
+from pyramid.request import Request
+from pyramid.security import Allowed
+from pyramid.threadlocal import get_current_request, get_current_registry
+from zope.interface import alsoProvides
+
+
+_marker = object()
+
+
+class RequestSelector(object):
+    """Interface based request selector
+
+    This selector can be used as a subscriber predicate to define
+    an interface that the event's 'request' attribute must support for the event to be applied::
+
+    .. code-block:: python
+
+        from pyams_utils.interfaces.site import ISiteRoot
+
+        @subscriber(IBeforeTraverseEvent, request_selector=IPyAMSLayer)
+        def before_traverse_event(event):
+            '''This is an event handler for an IPyAMSRequest modification event'''
+    """
+
+    def __init__(self, ifaces, config):
+        if not isinstance(ifaces, (list, tuple, set)):
+            ifaces = (ifaces,)
+        self.interfaces = ifaces
+
+    def text(self):
+        return 'request_selector = %s' % str(self.interfaces)
+
+    phash = text
+
+    def __call__(self, event):
+        for intf in self.interfaces:
+            try:
+                if intf.providedBy(event.request):
+                    return True
+            except (AttributeError, TypeError):
+                if isinstance(event.request, intf):
+                    return True
+        return False
+
+
+def request_property(key=None, prefix=None):
+    """Define a method decorator used to store result into current request's annotations
+
+    If no request is currently running, a new one is created.
+    `key` is a required argument; if None, the key will be the method's object
+
+    :param str key: annotations value key; if *None*, the key will be the method's object; if *key* is a callable
+        object, it will be called to get the actual session key
+    :param prefix: str; prefix to use for session key; if *None*, the prefix will be the property name
+    """
+
+    def request_decorator(func):
+
+        def wrapper(obj, key, *args, **kwargs):
+            request = query_request()
+            if request is not None:
+                if callable(key):
+                    key = key(obj, *args, **kwargs)
+                if not key:
+                    key = prefix or func.__name__
+                    if obj is not request:
+                        key += '::{0}'.format(ICacheKeyValue(obj))
+                    key_args = tuple(filter(lambda x: x is not request, args))
+                    if key_args:
+                        key += '::' + '::'.join((ICacheKeyValue(arg) for arg in key_args))
+                    if kwargs:
+                        key += '::' + \
+                               '::'.join(('{0}={1}'.format(key, ICacheKeyValue(val)) for key, val in kwargs.items()))
+                logger.debug(">>> Looking for request cache key {0}".format(key))
+                data = get_request_data(request, key, _marker)
+                if data is _marker:
+                    logger.debug("<<< no cached value!")
+                    data = func
+                    if callable(data):
+                        data = data(obj, *args, **kwargs)
+                    set_request_data(request, key, data)
+                else:
+                    logger.debug("<<< cached value found!")
+                    logger.debug("  < {0!r}".format(data))
+            else:
+                data = func
+                if callable(data):
+                    data = data(obj, *args, **kwargs)
+            return data
+
+        return lambda x, *args, **kwargs: wrapper(x, key, *args, **kwargs)
+
+    return request_decorator
+
+
+class PyAMSRequest(Request):
+    """Custom request factory
+
+    Used to add 'context' argument to 'effective_principals' method call
+    to be able to get 'roles' principals
+    """
+
+    @request_property(key=None)
+    def has_permission(self, permission, context=None):
+        if context is None:
+            context = self.context
+        try:
+            reg = self.registry
+        except AttributeError:
+            reg = get_current_registry()
+        authn_policy = reg.queryUtility(IAuthenticationPolicy)
+        if authn_policy is None:
+            return Allowed('No authentication policy in use.')
+        authz_policy = reg.queryUtility(IAuthorizationPolicy)
+        if authz_policy is None:
+            raise ValueError('Authentication policy registered without '
+                             'authorization policy')  # should never happen
+        principals = authn_policy.effective_principals(self, context)
+        return authz_policy.permits(context, principals, permission)
+
+
+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 MissingRequestError("No request")
+    return request
+
+
+def query_request():
+    """Query current request
+
+    Returns None if there is no active request"""
+    try:
+        return get_request()
+    except MissingRequestError:
+        return None
+
+
+def check_request(path='/', environ=None, base_url=None, headers=None, POST=None, registry=None, **kwargs):
+    """Get current request, or create a new blank one if missing"""
+    try:
+        return get_request()
+    except MissingRequestError:
+        request = PyAMSRequest.blank(path, environ, base_url, headers, POST, **kwargs)
+        if registry is None:
+            registry = get_current_registry()
+            if registry is None:
+                registry = get_global_registry()
+        request.registry = registry
+        return request
+
+
+def copy_request(request):
+    """Create clone of given request, keeping registry as well"""
+    request = request.copy()
+    if not hasattr(request, 'registry'):
+        registry = get_current_registry()
+        if registry is None:
+            registry = get_global_registry()
+        request.registry = registry
+    return request
+
+
+def get_annotations(request):
+    """Define 'annotations' request property
+
+    This function is automatically defined as a custom request method on package include.
+    """
+    alsoProvides(request, IAttributeAnnotatable)
+    return IAnnotations(request)
+
+
+def get_debug(request):
+    """Define 'debug' request property
+
+    This function is automatically defined as a custom request method on package include.
+    """
+    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
+
+    :param request: the request containing requested data
+    :param str key: request data annotation key
+    :param object default: the default value when data is missing
+    :return: the requested value, or *default*
+    """
+    try:
+        annotations = request.annotations
+    except (TypeError, AttributeError) as e:
+        annotations = get_annotations(request)
+    return annotations.get(key, default)
+
+
+def set_request_data(request, key, value):
+    """Associate data with request
+
+    :param request: the request in which to set data
+    :param str key: request data annotation key
+    :param object value: the value to be set in request annotation
+    """
+    try:
+        annotations = request.annotations
+    except (TypeError, AttributeError) as e:
+        annotations = get_annotations(request)
+    annotations[key] = value
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/schema.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,191 @@
+#
+# 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 string
+
+# import interfaces
+from zope.schema.interfaces import IText, ITextLine, IDecimal, IList, IDict, ITuple, IPassword
+
+# import Zope3 packages
+from persistent.list import PersistentList as PersistentListType
+from persistent.mapping import PersistentMapping
+from zope.interface import implementer
+from zope.schema import Text, TextLine, Decimal, List, Dict, Tuple, Password, ValidationError
+
+# import local packages
+
+from pyams_utils import _
+
+
+#
+# Persistent list field
+#
+
+class IPersistentList(IList):
+    """Persistent list field marker interface"""
+
+
+@implementer(IPersistentList)
+class PersistentList(List):
+    """Persistent list field"""
+
+    _type = PersistentListType
+
+
+#
+# Persistent mapping field
+#
+
+class IPersistentDict(IDict):
+    """Persistent mapping field marker interface"""
+
+
+@implementer(IPersistentDict)
+class PersistentDict(Dict):
+    """Persistent mapping field"""
+
+    _type = PersistentMapping
+
+
+#
+# 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
+
+
+#
+# HTML field
+#
+
+class IHTMLField(IText):
+    """HTML field interface"""
+
+
+@implementer(IHTMLField)
+class HTMLField(Text):
+    """HTML field"""
+
+
+#
+# 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)
+
+
+#
+# Mail address field
+#
+
+class IMailAddressField(ITextLine):
+    """Marker interface for mail address field"""
+
+
+EMAIL_REGEX = re.compile("^[^ @]+@[^ @]+\.[^ @]+$")
+
+
+class InvalidEmail(ValidationError):
+    __doc__ = _("Email address must be entered as « name@domain.name », without '<' and '>' characters")
+
+
+@implementer(IMailAddressField)
+class MailAddressField(TextLine):
+    """Mail address field"""
+
+    def _validate(self, value):
+        super(MailAddressField, self)._validate(value)
+        if not EMAIL_REGEX.match(value):
+            raise InvalidEmail(value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/scripts/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/scripts/zodb.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,45 @@
+#
+# 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 argparse
+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: {0} config_uri".format(sys.argv[0])
+    description = """Check for database upgrade.
+                  Usage: pyams_upgrade production.ini
+                  """
+    parser = argparse.ArgumentParser(usage=usage,
+                                     description=textwrap.dedent(description))
+    parser.add_argument('config_uri', help='Name of configuration file')
+    args = parser.parse_args()
+
+    config_uri = args.config_uri
+    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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,99 @@
+#
+# 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
+
+
+_marker = object()
+
+
+def session_property(app, key=None, prefix=None):
+    """Define a method decorator used to store result into request's session
+
+    If no request is currently running, a new one is created.
+
+    :param str app: application identifier used to prefix session keys
+    :param str key: session's value key; if *None*, the key will be the method's object; if *key* is a callable
+        object, il will be called to get the actual session key
+    :param prefix: str; prefix to use for session key; if *None*, the prefix will be the property name
+    """
+
+    def session_decorator(func):
+
+        def wrapper(obj, app, key, *args, **kwargs):
+            request = check_request()
+            if callable(key):
+                key = key(obj, *args, **kwargs)
+            if not key:
+                key = '{1}::{0!r}'.format(obj, prefix or func.__name__)
+            data = get_session_data(request, app, key, _marker)
+            if data is _marker:
+                data = func
+                if callable(data):
+                    data = data(obj, *args, **kwargs)
+                set_session_data(request, app, key, data)
+            return data
+
+        return lambda x, *args, **kwargs: wrapper(x, app, key, *args, **kwargs)
+
+    return session_decorator
+
+
+def get_session_data(request, app, key, default=None):
+    """Get data associated with current user session
+
+    PyAMS session management is based on :py:mod:`Beaker` package session management.
+
+    :param request: the request from which session is extracted
+    :param str app: application name
+    :param str key: session data key for given application
+    :param default: object; requested session data, or *default* if it can't be found
+
+    .. code-block:: python
+
+        APPLICATION_KEY = 'MyApp'
+        SESSION_KEY = 'MyFunction'
+
+        def my_function(request):
+            return get_session_data(request, APPLICATION_KEY, SESSION_KEY)
+    """
+    session = request.session
+    return session.get('{0}::{1}'.format(app, key), default)
+
+
+def set_session_data(request, app, key, value):
+    """Associate data with current user session
+
+    :param request: the request from which session is extracted
+    :param str app: application name
+    :param str key: session data key for given application
+    :param object value: any object that can be pickled can be stored into user session
+
+    .. code-block:: python
+
+        APPLICATION_KEY = 'MyApp'
+        SESSION_KEY = 'MyFunction'
+
+        def my_function(request):
+            value = {'key1': 'value1', 'key2': 'value2'}
+            set_session_data(request, APPLICATION_KEY, SESSION_KEY, value)
+    """
+    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	Wed Dec 05 12:45:56 2018 +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 pyams_utils.interfaces import PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME, \
+    PYAMS_APPLICATION_FACTORY_KEY, PUBLIC_PERMISSION
+from pyams_utils.interfaces.site import ISiteRoot, ISiteRootFactory, INewLocalSiteCreatedEvent, ISiteUpgradeEvent, \
+    ISiteGenerations, SITE_GENERATIONS_KEY, IConfigurationManager
+from zope.component.interfaces import IPossibleSite, ObjectEvent
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent.dict import PersistentDict
+from pyams_utils.adapter import adapter_config, ContextAdapter, get_annotation_adapter
+from pyams_utils.registry import get_utilities_for, query_utility
+from pyramid.exceptions import NotFound
+from pyramid.path import DottedNameResolver
+from pyramid.security import Allow, Everyone, ALL_PERMISSIONS
+from pyramid.threadlocal import 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
+
+    A site root can be used as base application root in your ZODB.
+    It's also site root responsibility to manage your local site manager.
+
+    BaseSiteRoot defines a basic ACL which gives all permissions to system administrator,
+    and 'public' permission to everyone. But this ACL is generally overriden in subclasses
+    which also inherit from :class:`pyams_security.security.ProtectedObject`.
+    """
+
+    __acl__ = [(Allow, 'system:admin', ALL_PERMISSIONS),
+               (Allow, Everyone, {PUBLIC_PERMISSION})]
+
+    config_klass = None
+
+
+@adapter_config(name='etc', context=ISiteRoot, provides=ITraversable)
+class SiteRootEtcTraverser(ContextAdapter):
+    """Site root ++etc++ namespace traverser
+
+    Gives access to local site manager from */++etc++site* URL
+    """
+
+    def traverse(self, name, furtherpath=None):
+        if name == 'site':
+            return self.context.getSiteManager()
+        raise NotFound
+
+
+@implementer(INewLocalSiteCreatedEvent)
+class NewLocalSiteCreatedEvent(ObjectEvent):
+    """New local site creation event"""
+
+
+def site_factory(request):
+    """Application site factory
+
+    On application startup, this factory checks configuration to get application name and
+    load it from the ZODB; if the application can't be found, configuration is scanned to
+    get application factory, create a new one and create a local site manager.
+    """
+    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 = request.registry.queryUtility(ISiteRootFactory, default=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 named utilities providing
+    :py:class:`ISiteGenerations <pyams_utils.interfaces.site.ISiteGenerations>` interface.
+
+    Current site generations are stored into annotations for each generation adapter.
+    """
+    application = site_factory(request)
+    if application is not None:
+        try:
+            hooks.setSite(application)
+            generations = get_annotation_adapter(application, SITE_GENERATIONS_KEY, PersistentDict,
+                                                 notify=False, locate=False)
+            for name, utility in sorted(get_utilities_for(ISiteGenerations),
+                                        key=lambda x: x[1].order):
+                if not name:
+                    name = '.'.join((utility.__module__, utility.__class__.__name__))
+                current = generations.get(name)
+                if not current:
+                    print("Upgrading {0} to generation {1}...".format(name, utility.generation))
+                elif 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
+
+
+def check_required_utilities(site, utilities):
+    """Utility function to check for required utilities
+
+    :param object site: the site manager into which configuration may be checked
+    :param tuple utilities: each element of the tuple is another tuple made of the utility interface,
+        the utility registration name, the utility factory and the object name when creating the utility, as in:
+
+    .. code-block:: python
+
+        REQUIRED_UTILITIES = ((ISecurityManager, '', SecurityManager, 'Security manager'),
+                              (IPrincipalAnnotationUtility, '', PrincipalAnnotationUtility, 'User profiles'))
+    """
+    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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,69 @@
+#
+# 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_': 'en'})
+    >>> request.locale_name
+    'en'
+    >>> 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'
+    >>> request = DummyRequest(params={'_LOCALE_': 'fr'})
+    >>> request.locale_name
+    'fr'
+    >>> 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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,116 @@
+#
+# 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('(.+)\((.+)\)', re.MULTILINE | re.DOTALL)
+ARGUMENTS_EXPRESSION = re.compile('[^(,)]+')
+
+
+def render_extension(econtext, name):
+    """TALES extension renderer
+
+    See :ref:`tales` for complete description.
+    """
+
+    def get_value(econtext, arg):
+        """Extract argument value from context
+
+        Extension expression language is quite simple. Values can be given as
+        positioned strings, integers or named arguments of the same types.
+        """
+        arg = arg.strip()
+        if arg.startswith('"') or arg.startswith("'"):
+            # may be a quoted string...
+            return arg[1:-1]
+        if '=' in arg:
+            key, value = arg.split('=', 1)
+            value = get_value(econtext, value)
+            return {key.strip(): value}
+        try:
+            arg = int(arg)  # check integer value
+        except ValueError:
+            args = arg.split('.')
+            result = econtext.get(args.pop(0))
+            for arg in args:
+                result = getattr(result, arg)
+            return result
+        else:
+            return arg
+
+    name = name.strip()
+    context = econtext.get('context')
+    request = econtext.get('request')
+    view = econtext.get('view')
+
+    args, kwargs = [], {}
+    func_match = FUNCTION_EXPRESSION.match(name)
+    if func_match:
+        name, arguments = func_match.groups()
+        for arg in map(lambda x: get_value(econtext, x), ARGUMENTS_EXPRESSION.findall(arguments)):
+            if isinstance(arg, dict):
+                kwargs.update(arg)
+            else:
+                args.append(arg)
+
+    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(*args, **kwargs)
+
+
+class ExtensionExpr(ContextExprMixin, StringExpr):
+    """tales: TALES expression
+
+    This expression can be used to call a custom named adapter providing ITALESExtension interface.
+    """
+
+    transform = Symbol(render_extension)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/tests/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/tests/test_utilsdocs.py	Wed Dec 05 12:45:56 2018 +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	Wed Dec 05 12:45:56 2018 +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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,203 @@
+#
+# 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 html
+
+import docutils.core
+from pyramid.interfaces import IRequest
+from zope.interface import Interface
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
+
+from pyams_utils.adapter import ContextRequestAdapter, ContextRequestViewAdapter, adapter_config
+from pyams_utils.interfaces.tales import ITALESExtension
+from pyams_utils.interfaces.text import IHTMLRenderer
+from pyams_utils.request import check_request
+from pyams_utils.vocabulary import vocabulary_config
+
+
+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 this value
+
+    :param str text: initial text
+    :param integer length: maximum length of resulting text
+    :param integer 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='truncate', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class TruncateCharsTalesExtension(ContextRequestViewAdapter):
+    """extension:truncate(value, length, max) TALES expression
+
+    Truncates a sentence if it is longer than the specified 'length' characters.
+    Truncated strings will end with an ellipsis character (“…”)
+    See also 'get_text_start'
+    """
+
+    @staticmethod
+    def render(value, length=50, max=0):
+        if not value:
+            return ''
+        return get_text_start(value, length, max=max)
+
+
+@adapter_config(name='raw', context=(str, IRequest), provides=IHTMLRenderer)
+class BaseHTMLRenderer(ContextRequestAdapter):
+    """Raw text HTML renderer
+
+    This renderer renders input text 'as is', mainly for use in a <pre> tag.
+    """
+
+    def render(self, **kwargs):
+        return self.context
+
+
+@adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer)
+class TextRenderer(BaseHTMLRenderer):
+    """Basic text HTML renderer
+
+    This renderer only replace newlines with HTML breaks.
+    """
+
+    def render(self, **kwargs):
+        return html.escape(self.context).replace('\n', '<br />\n')
+
+
+@adapter_config(name='js', context=(str, IRequest), provides=IHTMLRenderer)
+class JsRenderer(BaseHTMLRenderer):
+    """Custom Javascript HTML renderer
+
+    This renderer replaces single quotes with escaped ones
+    """
+
+    def render(self, **kwargs):
+        return self.context.replace("'", "\\'")
+
+
+@adapter_config(name='rest', context=(str, IRequest), provides=IHTMLRenderer)
+class ReStructuredTextRenderer(BaseHTMLRenderer):
+    """reStructuredText HTML renderer
+
+    This renderer is using *docutils* to render HTML output.
+    """
+
+    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
+
+    Renderer name can be any registered HTML renderer adapter
+    """
+    request = check_request()
+    registry = request.registry
+    renderer = registry.queryMultiAdapter((text, request), IHTMLRenderer, name=renderer)
+    if renderer is not None:
+        return renderer.render()
+
+
+empty_marker = object()
+
+
+@adapter_config(name='html', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class HTMLTalesExtension(ContextRequestViewAdapter):
+    """*extension:html* TALES expression
+
+    If first *context* argument of the renderer is an object for which an :py:class:`IHTMLRenderer`
+    adapter can be found, this adapter is used to render the context to HTML; if *context* is a string,
+    it is converted to HTML using the renderer defined as second parameter; otherwise, context is just
+    converted to string using the :py:func:`str` function.
+    """
+
+    def render(self, context=empty_marker, renderer='text'):
+        if context is empty_marker:
+            context = self.context
+        if not context:
+            return ''
+        registry = self.request.registry
+        adapter = registry.queryMultiAdapter((context, self.request, self.view), IHTMLRenderer)
+        if adapter is None:
+            adapter = registry.queryMultiAdapter((context, self.request), IHTMLRenderer)
+        if adapter is not None:
+            return adapter.render()
+        elif isinstance(context, str):
+            return text_to_html(context, renderer)
+        else:
+            return str(context)
+
+
+@vocabulary_config(name='PyAMS HTML renderers')
+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)
+
+
+@adapter_config(name='br', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class BrTalesExtension(ContextRequestViewAdapter):
+    """extension:br(value, class) TALES expression
+
+    This expression can be used to context a given character ('|' by default) into HTML
+    breaks with given CSS class.
+    """
+
+    @staticmethod
+    def render(value, css_class='', character='|', start_tag=None, end_tag=None):
+        if not value:
+            return ''
+        br = '<br {0} />'.format('class="{0}"'.format(css_class) if css_class else '')
+        elements = value.split(character)
+        if start_tag:
+            elements[0] = '<{0}>{1}</{0}>'.format(start_tag, elements[0])
+        if end_tag:
+            elements[-1] = '<{0}>{1}</{0}>'.format(end_tag, elements[-1])
+        return br.join(elements)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/timezone/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,90 @@
+#
+# 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):
+    """Convert datetime to local timezone
+
+    :param datetime value: input datetime; value is assumed to be in GMT if no timezone is given
+    """
+    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):
+    """Convert datetime to GMT
+
+    Value is assumed to be in GMT if no timezone is given
+    """
+    if not value:
+        return None
+    if not isinstance(value, datetime):
+        return value
+    if not value.tzinfo:
+        value = GMT.localize(value)
+    return value.astimezone(GMT)
+
+
+def localgmtime(value):
+    """Convert datetime to GMT
+
+    Value is assumed to be in server timezone if none is given
+    """
+    if not value:
+        return None
+    if not isinstance(value, datetime):
+        return value
+    if not value.tzinfo:
+        value = tzinfo().localize(value)
+    return value.astimezone(GMT)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/timezone/utility.py	Wed Dec 05 12:45:56 2018 +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 pyams_utils.interfaces.site import ISiteGenerations
+from pyams_utils.interfaces.timezone import IServerTimezone
+
+# import packages
+from persistent import Persistent
+from pyams_utils.registry import utility_config
+from pyams_utils.site import check_required_utilities
+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'),)
+
+
+@utility_config(name='PyAMS timezone', provides=ISiteGenerations)
+class TimezoneGenerationsChecker(object):
+    """Timezone generations checker"""
+
+    order = 10
+    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	Wed Dec 05 12:45:56 2018 +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 pytz
+
+# import interfaces
+
+# import packages
+from pyams_utils.vocabulary import vocabulary_config
+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
+
+
+@vocabulary_config(name='PyAMS timezones')
+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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/traversing.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,216 @@
+#
+# 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.traversing import IPathElements
+from pyramid.interfaces import VH_ROOT_KEY
+from zope.intid.interfaces import IIntIds
+from zope.location.interfaces import IContained
+from zope.traversing.interfaces import ITraversable, BeforeTraverseEvent
+
+# import packages
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
+from pyramid.compat import decode_path_info, is_nonstr_iter
+from pyramid.exceptions import URLDecodeError, NotFound
+from pyramid.location import lineage
+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:
+                if ob is not root:
+                    request.registry.notify(BeforeTraverseEvent(ob, request))
+
+                if segment[:2] == view_selector:
+                    # check for view name prefixed by '@@'
+                    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}
+
+                elif segment[:2] == ns_selector:
+                    # check for namespace prefixed by '++'
+                    # when a namespace is detected, named "ITraversable" multi-adapters are searched for
+                    # context and request, for context and for request, sequentially; a NotFound exception is
+                    # raised if traverser can't be found, otherwise it's "traverse" method is called to get new
+                    # context
+                    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
+
+        if ob is not root:
+            request.registry.notify(BeforeTraverseEvent(ob, request))
+
+        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, condition=None):
+    """Get first parent of the context that implements given interface
+
+    :param object context: base element
+    :param Interface interface: the interface that parend should implement
+    :param boolean allow_context: if 'True' (the default), traversing is done starting with context; otherwise,
+        traversing is done starting from context's parent
+    :param callable condition: an optional function that should return a 'True' result when called with parent
+        as first argument
+    """
+    if allow_context:
+        parent = context
+    else:
+        parent = getattr(context, '__parent__', None)
+    while parent is not None:
+        if interface.providedBy(parent):
+            target = interface(parent)
+            if (not condition) or condition(target):
+                return target
+        parent = getattr(parent, '__parent__', None)
+    return None
+
+
+@adapter_config(context=IContained, provides=IPathElements)
+class PathElementsAdapter(ContextAdapter):
+    """Contained object path elements adapter
+
+    This interface is intended to be used inside a keyword index to
+    be able to search object based on a given parent
+    """
+
+    @property
+    def parents(self):
+        intids = query_utility(IIntIds)
+        if intids is None:
+            return []
+        return [intids.register(parent) for parent in lineage(self.context)]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/unicode.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,243 @@
+#
+# 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=' ', remove_punctuation=True, keep_chars='_-.'):
+    """Remove extended characters and diacritics from string and replace them with 'basic' ones
+    
+    :param str s: text to be cleaned.
+    :param boolean escape_slashes: if True, slashes are also converted
+    :param boolean force_lower: if True, result is automatically converted to lower case
+    :param str spaces: character used to replace spaces
+    :param boolean remove_punctuation: if True, all punctuation characters are removed
+    :param str keep_chars: characters which may be kept in the input string
+    :return: text without diacritics or special characters
+
+    >>> from pyams_utils.unicode import translate_string
+    >>> input = 'Ceci est un test en Français !!!'
+    >>> translate_string(input)
+    'ceci est un test en francais'
+    >>> translate_string(input, force_lower=False)
+    'Ceci est un test en Francais'
+    >>> translate_string(input, spaces='-')
+    'ceci-est-un-test-en-francais'
+    >>> translate_string(input, remove_punctuation=False)
+    'ceci est un test en francais !!!'
+    >>> translate_string(input, keep_chars='!')
+    'ceci est un test en francais !!!'
+    """
+    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)
+    if remove_punctuation:
+        punctuation = ''.join(filter(lambda x: x not in keep_chars, string.punctuation))
+        s = ''.join(filter(lambda x: x not in punctuation, s))
+    if force_lower:
+        s = s.lower()
+    s = s.strip()
+    if spaces != ' ':
+        s = s.replace(' ', spaces)
+    return s
+
+
+def nvl(value, default=''):
+    """Get specified value, or an empty string if value is empty
+    
+    :param object value: value to be checked
+    :param object default: default value to be returned if value is *false*
+    :return: input value, or *default* if value is *false*
+
+    >>> from pyams_utils.unicode import nvl
+    >>> nvl(None)
+    ''
+    >>> nvl('foo')
+    'foo'
+    >>> nvl(False, 'bar')
+    'bar'
+    """
+    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 str/bytes value: the input to be checked
+    :param default: str; default value
+    :param encoding: str; encoding name to use for conversion
+    :return: str; value, or *default* if value is empty, converted to unicode
+
+    >>> from pyams_utils.unicode import uninvl
+    >>> uninvl('String value')
+    'String value'
+    >>> uninvl(b'String value')
+    'String value'
+    >>> uninvl(b'Cha\\xc3\\xaene accentu\\xc3\\xa9e')
+    'Chaîne accentuée'
+    >>> uninvl(b'Cha\\xeene accentu\\xe9e', 'latin1')
+    'Chaîne accentuée'
+    """
+    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 dict value: input mapping of strings which may be converted to unicode
+    :return: dict; a new mapping with each value converted to unicode
+
+    >>> from pyams_utils.unicode import unidict
+    >>> unidict({'input': b'Cha\\xc3\\xaene accentu\\xc3\\xa9e'})
+    {'input': 'Chaîne accentuée'}
+    >>> unidict({'input': b'Cha\\xeene accentu\\xe9e'}, 'latin1')
+    {'input': 'Chaîne accentuée'}
+    """
+    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 list value: input list of strings which may be converted to unicode
+    :return: list; a new list with each value converted to unicode
+
+    >>> from pyams_utils.unicode import unilist
+    >>> unilist([b'Cha\\xc3\\xaene accentu\\xc3\\xa9e'])
+    ['Chaîne accentuée']
+    >>> unilist([b'Cha\\xeene accentu\\xe9e'], 'latin1')
+    ['Chaîne accentuée']
+    """
+    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
+
+    :param str value: the value to encode
+    :param str encoding: selected encoding
+    :return: bytes; value encoded to bytes if input is a string, original value otherwise
+
+    >>> from pyams_utils.unicode import encode
+    >>> encode('Chaîne accentuée')
+    b'Cha\\xc3\\xaene accentu\\xc3\\xa9e'
+    >>> encode('Chaîne accentuée', 'latin1')
+    b'Cha\\xeene accentu\\xe9e'
+    """
+    return value.encode(encoding) if isinstance(value, str) else value
+
+
+def utf8(value):
+    """Encode given unicode value to UTF-8 encoded bytes
+
+    :param str value: the value to encode to utf-8
+    :return: bytes; value encoded to bytes if input is a string, original value otherwise
+
+    >>> from pyams_utils.unicode import utf8
+    >>> utf8('Chaîne accentuée')
+    b'Cha\\xc3\\xaene accentu\\xc3\\xa9e'
+    """
+    return encode(value, 'utf-8')
+
+
+def decode(value, encoding='utf-8'):
+    """Decode given bytes value to unicode with given encoding
+
+    :param bytes value: the value to decode
+    :param str encoding: selected encoding
+    :return: str; value decoded to unicode string if input is a bytes, original value otherwise
+
+    >>> from pyams_utils.unicode import decode
+    >>> decode(b'Cha\\xc3\\xaene accentu\\xc3\\xa9e')
+    'Chaîne accentuée'
+    >>> decode(b'Cha\\xeene accentu\\xe9e', 'latin1')
+    'Chaîne accentuée'
+    """
+    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	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,165 @@
+#
+# 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 pyramid.encode import url_quote, urlencode
+from pyramid.url import QUERY_SAFE, resource_url
+from zope.interface import Interface
+
+from pyams_utils.adapter import ContextRequestAdapter, ContextRequestViewAdapter, adapter_config
+from pyams_utils.interfaces.tales import ITALESExtension
+from pyams_utils.interfaces.url import DISPLAY_CONTEXT, ICanonicalURL, IRelativeURL
+from pyams_utils.unicode import translate_string
+
+
+def generate_url(title):
+    """Generate an SEO-friendly content URL from it's title
+
+    The original title is translated to remove accents, converted to lowercase, and words
+    shorter than three characters are removed; terms are joined by hyphens.
+    """
+    return '-'.join(filter(lambda x: len(x) > 2,
+                           translate_string(title, escape_slashes=True, force_lower=True, spaces='-',
+                                            remove_punctuation=True, keep_chars='-').split('-')))
+
+
+#
+# Request display context
+#
+
+def get_display_context(request):
+    return request.annotations.get(DISPLAY_CONTEXT, request.context)
+
+
+#
+# Absolute URLs management
+#
+
+def absolute_url(context, request, view_name=None, query=None):
+    """Get resource absolute_url
+
+    :param object context: the persistent object for which absolute URL is required
+    :param request: the request on which URL is based
+    :param str view_name: an optional view name to add to URL
+    :param str/dict query: an optional URL arguments string or mapping
+
+    This absolute URL function is based on default Pyramid's :py:func:`resource_url` function, but
+    add checks to remove some double slashes, and add control on view name when it begins with a '#'
+    character which is used by MyAMS.js framework.
+    """
+
+    # if we pass a string to absolute_url(), argument is returned as-is!
+    if isinstance(context, str):
+        return context
+
+    # if we have several parents without name in the lineage, the resource URL contains a double slash
+    # which generates "NotFound" exceptions; so we replace it with a single slash...
+    result = resource_url(context, request).replace('//', '/').replace(':/', '://')
+    if result.endswith('/'):
+        result = result[:-1]
+    if view_name:
+        if view_name.startswith('#'):
+            result += view_name
+        else:
+            result += '/' + view_name
+    if query:
+        qs = ''
+        if isinstance(query, str):
+            qs = '?' + url_quote(query, QUERY_SAFE)
+        elif query:
+            qs = '?' + urlencode(query, doseq=True)
+        result += qs
+    return result
+
+
+@adapter_config(name='absolute_url', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class AbsoluteUrlTalesExtension(ContextRequestViewAdapter):
+    """extension:absolute_url(context, view_name) TALES extension
+
+    A PyAMS TALES extension used to get access to an object URL from a page template.
+    """
+
+    def render(self, context=None, view_name=None):
+        if context is None:
+            context = self.context
+        return absolute_url(context, self.request, view_name)
+
+
+#
+# Canonical URLs management
+#
+
+def canonical_url(context, request, view_name=None, query=None):
+    """Get resource canonical URL"""
+
+    # if we pass a string to canonical_url(), argument is returned as-is!
+    if isinstance(context, str):
+        return context
+
+    url_adapter = request.registry.queryMultiAdapter((context, request), ICanonicalURL)
+    if url_adapter is None:
+        url_adapter = request.registry.queryAdapter(context, ICanonicalURL)
+
+    if url_adapter is not None:
+        return url_adapter.get_url(view_name, query)
+    else:
+        return absolute_url(context, request, view_name, query)
+
+
+@adapter_config(name='canonical_url', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class CanonicalUrlTalesExtension(ContextRequestViewAdapter):
+    """extension:canonical_url(context, view_name) TALES extension
+
+    A PyAMS TALES extension used to get access to an object's canonical URL from a page template.
+    """
+
+    def render(self, context=None, view_name=None):
+        if context is None:
+            context = self.context
+        return canonical_url(context, self.request, view_name)
+
+
+#
+# Relative URLs management
+#
+
+@adapter_config(context=(Interface, Interface), provides=IRelativeURL)
+class DefaultRelativeURLAdapter(ContextRequestAdapter):
+    """Default relative URL adapter"""
+
+    def get_url(self, display_context=None, view_name=None, query=None):
+        return absolute_url(self.context, self.request, view_name, query)
+
+
+def relative_url(context, request, display_context=None, view_name=None, query=None):
+    """Get resource URL relative to given context"""
+    if display_context is None:
+        display_context = request.annotations.get(DISPLAY_CONTEXT, request.context)
+    adapter = request.registry.getMultiAdapter((context, request), IRelativeURL)
+    return adapter.get_url(display_context, view_name, query)
+
+
+@adapter_config(name='relative_url', context=(Interface, Interface, Interface), provides=ITALESExtension)
+class RelativeUrlTalesExtension(ContextRequestViewAdapter):
+    """extension:relative_url(context, view_name, query) TALES extension
+
+    A PyAMS TALES extension used to get an object's relative URL based on current request display context.
+    """
+
+    def render(self, context=None, view_name=None, query=None):
+        if context is None:
+            context = self.context
+        return relative_url(context, self.request, view_name=view_name, query=query)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/vocabulary.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,95 @@
+#
+# 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 logging
+logger = logging.getLogger('PyAMS (utils)')
+
+import venusian
+
+# import interfaces
+from zope.schema.interfaces import IVocabularyFactory
+
+# import packages
+from zope.interface import directlyProvides
+from zope.schema.vocabulary import getVocabularyRegistry
+
+
+class vocabulary_config:
+    """Class decorator to define a vocabulary
+
+    :param str name: name of the registered vocabulary
+
+    This is, for example, how a vocabulary of registered ZEO connections utilities is created:
+
+    .. code-block:: python
+
+        from pyams_utils.interfaces.zeo import IZEOConnection
+
+        from pyams_utils.registry import get_utilities_for
+        from pyams_utils.vocabulary import vocabulary_config
+        from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
+
+        @vocabulary_config(name='PyAMS ZEO connections')
+        class ZEOConnectionVocabulary(SimpleVocabulary):
+            '''ZEO connections vocabulary'''
+
+            def __init__(self, context=None):
+                terms = [SimpleTerm(name, title=util.name) for name, util in get_utilities_for(IZEOConnection)]
+                super(ZEOConnectionVocabulary, self).__init__(terms)
+
+    You can then use such a vocabulary in any schema field:
+
+    .. code-block:: python
+
+        from zope.interface import Interface
+        from zope.schema import Choice
+
+        class MySchema(Interface):
+            '''Custom schema interface'''
+
+            zeo_connection_name = Choice(title='ZEO connection name',
+                                         description='Please select a registered ZEO connection',
+                                         vocabulary='PyAMS ZEO connections',
+                                         required=False)
+    """
+
+    venusian = venusian
+
+    def __init__(self, name, **settings):
+        self.name = name
+        self.__dict__.update(settings)
+
+    def __call__(self, wrapped):
+        settings = self.__dict__.copy()
+        depth = settings.pop('_depth', 0)
+
+        def callback(context, name, ob):
+            logger.debug('Registering class {0} as vocabulary with name "{1}"'.format(str(ob), self.name))
+            directlyProvides(ob, IVocabularyFactory)
+            getVocabularyRegistry().register(self.name, ob)
+
+        info = self.venusian.attach(wrapped, callback, category='pyams_vocabulary',
+                                    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/widget/__init__.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,1 @@
+#
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/widget/decimal.py	Wed Dec 05 12:45:56 2018 +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 decimal
+
+from z3c.form.converter import BaseDataConverter, FormatterValidationError
+from z3c.form.interfaces import IWidget, IDataConverter
+
+from pyams_utils.adapter import adapter_config
+from pyams_utils.schema import IDottedDecimalField
+
+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	Wed Dec 05 12:45:56 2018 +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 wsgi_environ_cache(*names):
+    """Wrap a function/method to cache its result for call into request.environ
+
+    :param [string...] names: keys to cache into environ; len(names) must
+        be equal to the result's length or scalar
+    """
+    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/zodb.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,312 @@
+#
+# 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.site import IOptionalUtility
+from pyams_utils.interfaces.zeo import IZEOConnection
+from transaction.interfaces import ITransactionManager
+from ZODB.interfaces import IConnection
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config
+from pyams_utils.registry import get_utilities_for, get_global_registry
+from pyams_utils.vocabulary import vocabulary_config
+from pyramid.events import subscriber
+from pyramid_zodbconn import get_uris, db_from_uri
+from ZEO import DB
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.schema import getFieldNames
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm
+
+
+@adapter_config(context=IPersistent, provides=IConnection)
+def persistent_connection(obj):
+    """An adapter which gets a ZODB connection from 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 persistent_transaction_manager(obj):
+    conn = IConnection(obj)  # typically this will be
+                             # zope.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.
+
+
+#
+# ZEO connection management
+#
+
+@implementer(IZEOConnection)
+class ZEOConnection(object):
+    """ZEO connection object
+
+    This object can be used to store all settings to be able to open a ZEO connection.
+    Note that this class is required only for tasks specifically targeting a ZEO database connection (like a ZEO
+    packer scheduler task); for generic ZODB operations, just use a :class:`ZODBConnection` class defined through
+    Pyramid's configuration file.
+
+    Note that a ZEO connection object is a context manager, so you can use it like this:
+
+    .. code-block:: python
+
+        from pyams_utils.zodb import ZEOConnection
+
+        def my_method(zeo_settings):
+            zeo_connection = ZEOConnection()
+            zeo_connection.update(zeo_settings)
+            with zeo_connection as root:
+                # *root* is then the ZODB root object
+                # do whatever you want with ZEO connection,
+                # which is closed automatically
+    """
+
+    _storage = None
+    _db = None
+    _connection = None
+
+    name = FieldProperty(IZEOConnection['name'])
+    server_name = FieldProperty(IZEOConnection['server_name'])
+    server_port = FieldProperty(IZEOConnection['server_port'])
+    storage = FieldProperty(IZEOConnection['storage'])
+    username = FieldProperty(IZEOConnection['username'])
+    password = FieldProperty(IZEOConnection['password'])
+    server_realm = FieldProperty(IZEOConnection['server_realm'])
+    blob_dir = FieldProperty(IZEOConnection['blob_dir'])
+    shared_blob_dir = FieldProperty(IZEOConnection['shared_blob_dir'])
+
+    def get_settings(self):
+        """Get mapping of all connection settings
+
+        These settings can be converted to JSON and sent to another process, for example
+        via a ØMQ connection.
+
+        :return: dict
+        """
+        result = {}
+        for name in getFieldNames(IZEOConnection):
+            result[name] = getattr(self, name)
+        return result
+
+    def update(self, settings):
+        """Update connection properties with settings as *dict*
+
+        :param dict settings: typically extracted via the :py:meth:`get_settings` method from
+            another process
+        """
+        names = getFieldNames(IZEOConnection)
+        for key, value in settings.items():
+            if key in names:
+                setattr(self, key, value)
+
+    def get_connection(self, wait_timeout=30, get_storage=False):
+        """Create ZEO client connection from current settings
+
+        :param boolean wait_timeout: connection timeout, in seconds
+        :param boolean get_storage: if *True*, the method should return a tuple containing
+            storage and DB objects; otherwise only DB object is returned
+        :return: tuple containing ZEO client storage and DB object (if *get_storage* argument is
+            set to *True*), or only DB object otherwise
+        """
+        db = DB((self.server_name, self.server_port),
+                storage=self.storage,
+                username=self.username or '',
+                password=self.password or '',
+                realm=self.server_realm,
+                blob_dir=self.blob_dir,
+                shared_blob_dir=self.shared_blob_dir,
+                wait_timeout=wait_timeout)
+        return (db.storage, db) if get_storage else db
+
+    @property
+    def connection(self):
+        return self._connection
+
+    # Context manager methods
+    def __enter__(self):
+        self._storage, self._db = self.get_connection(get_storage=True)
+        self._connection = self._db.open_then_close_db_when_connection_closes()
+        return self._connection.root()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        if self._connection is not None:
+            self._connection.close()
+        if self._storage is not None:
+            self._storage.close()
+
+
+@implementer(IOptionalUtility, IAttributeAnnotatable)
+class ZEOConnectionUtility(ZEOConnection, Persistent, Contained):
+    """Persistent ZEO connection utility"""
+
+
+@subscriber(IObjectAddedEvent, context_selector=IZEOConnection)
+def handle_added_connection(event):
+    """Register new ZEO connection when added"""
+    manager = event.newParent
+    manager.registerUtility(event.object, IZEOConnection, name=event.object.name)
+
+
+@subscriber(IObjectRemovedEvent, context_selector=IZEOConnection)
+def handle_removed_connection(event):
+    """Un-register ZEO connection when deleted"""
+    manager = event.oldParent
+    manager.unregisterUtility(event.object, IZEOConnection, name=event.object.name)
+
+
+@vocabulary_config(name='PyAMS ZEO connections')
+class ZEOConnectionVocabulary(SimpleVocabulary):
+    """ZEO connections vocabulary"""
+
+    def __init__(self, context=None):
+        terms = [SimpleTerm(name, title=util.name) for name, util in get_utilities_for(IZEOConnection)]
+        super(ZEOConnectionVocabulary, self).__init__(terms)
+
+
+def get_connection_from_settings(settings=None):
+    """Load connection matching registry settings"""
+    if settings is None:
+        settings = get_global_registry().settings
+    for name, uri in get_uris(settings):
+        db = db_from_uri(uri, name, {})
+        return db.open()
+
+
+class ZODBConnection(object):
+    """ZODB connection wrapper
+
+    Connections are extracted from Pyramid's settings file in *zodbconn.uri* entries.
+
+    Note that a ZODB connection object is a context manager, so you can use it like this:
+
+    .. code-block:: python
+
+        from pyams_utils.zodb import ZODBConnection
+
+        def my_method(zodb_name):
+            zodb_connection = ZODBConnection(zodb_name)
+            with zodb_connection as root:
+                # *root* is then the ZODB root object
+                # do whatever you want with ZODB connection,
+                # which is closed automatically
+    """
+
+    def __init__(self, name='', settings=None):
+        self.name = name or ''
+        if not settings:
+            settings = get_global_registry().settings
+        self.settings = settings
+
+    _connection = None
+    _db = None
+    _storage = None
+
+    @property
+    def connection(self):
+        return self._connection
+
+    @property
+    def db(self):
+        return self._db
+
+    @property
+    def storage(self):
+        return self._storage
+
+    def get_connection(self):
+        """Load named connection matching registry settings"""
+        for name, uri in get_uris(self.settings):
+            if name == self.name:
+                db = db_from_uri(uri, name, {})
+                connection = self._connection = db.open()
+                self._db = connection.db()
+                self._storage = self.db.storage
+                return connection
+
+    def close(self):
+        self._connection.close()
+        self._db.close()
+        self._storage.close()
+
+    # Context manager methods
+    def __enter__(self):
+        connection = self.get_connection()
+        return connection.root()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.close()
+
+
+@vocabulary_config(name='PyAMS ZODB connections')
+class ZODBConnectionVocabulary(SimpleVocabulary):
+    """ZODB connections vocabulary"""
+
+    def __init__(self, context=None):
+        settings = get_global_registry().settings
+        terms = [SimpleTerm(name, title=name) for name, uri in get_uris(settings)]
+        super(ZODBConnectionVocabulary, self).__init__(terms)
+
+
+volatile_marker = object()
+
+
+class volatile_property:
+    """Property decorator to define volatile attributes into persistent classes"""
+
+    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, inst, cls):
+        if inst is None:
+            return self
+        attrname = '_v_{0}'.format(self.__name__)
+        value = getattr(inst, attrname, volatile_marker)
+        if value is volatile_marker:
+            value = self.fget(inst)
+            setattr(inst, attrname, value)
+        return value
+
+    def __delete__(self, inst):
+        attrname = '_v_{0}'.format(self.__name__)
+        if hasattr(inst, attrname):
+            delattr(inst, attrname)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tox.ini	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,11 @@
+# tox (https://tox.readthedocs.io/) is a tool for running tests
+# in multiple virtualenvs. This configuration file will run the
+# test suite on all supported python versions. To use it, "pip install tox"
+# and then run "tox" from this directory.
+
+[tox]
+envlist = py34, py35, py36, pypy
+
+[testenv]
+commands = python setup.py test
+deps = -c http://download.ztfy.org/pyams/requirements.txt