--- /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 < ò > entity.<br /></p></div>'''
+ >>> html_to_text(html)
+ 'Header\\nThis is an < ò > entity.\\n\\n'
+
+ >>> html = '''<div><p>Header</p><p>This is an < ò > 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…'
+ >>> get_text_start('This is a long string', 20)
+ 'This is a long…'
+ >>> 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] + '…'
+ 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