# HG changeset patch # User Thierry Florac # Date 1544010356 -3600 # Node ID c8e21d7dd6850a228266eb8ca15c6d7fe42b0dbd Rebuild repository after corruption diff -r 000000000000 -r c8e21d7dd685 .hgignore --- /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$ diff -r 000000000000 -r c8e21d7dd685 .hgtags --- /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 diff -r 000000000000 -r c8e21d7dd685 LICENSE --- /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. diff -r 000000000000 -r c8e21d7dd685 MANIFEST.in --- /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 *.*~ diff -r 000000000000 -r c8e21d7dd685 bootstrap.py --- /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) diff -r 000000000000 -r c8e21d7dd685 buildout.cfg --- /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 diff -r 000000000000 -r c8e21d7dd685 docs/HISTORY.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 docs/README.txt --- /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 `_ diff -r 000000000000 -r c8e21d7dd685 setup.py --- /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 +# 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 + """, + ) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/PKG-INFO --- /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 `_ + + + 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/SOURCES.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/dependency_links.txt --- /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 @@ + diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/entry_points.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/namespace_packages.txt --- /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 @@ + diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/not-zip-safe --- /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 @@ + diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/requires.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils.egg-info/top_level.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/__init__.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/adapter.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/attr.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/cache.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/configure.zcml --- /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 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/container.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/context.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/data.py --- /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 +# 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:: + +
...
+ +After data initialization by **MyAMS.js**, the following code will be converted to:: + +
...
+""" + +__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:: + +
...
+ """ + + 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:: + +
...
+ """ + + 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:: + +
...
+ """ + + def render(self, params=None): + """See `pyams_utils.interfaces.tales.ITALESExtension` interface""" + return self.request.annotations.get(params) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/date.py --- /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 +# 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=) + """ + 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/decorator.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/dict.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/doctests/README.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/doctests/dates.txt --- /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=) + +'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=) + >>> 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=) + +'gmtime' function can be used to convert a datetime to GMT: + + >>> timezone.gmtime(now) + datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/doctests/request.txt --- /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 + + +If a new request is created "from scratch", it's registry is assigned to global registry: + + >>> request.registry + + +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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/doctests/unicode.txt --- /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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/encoding.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/factory.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/fanstatic.py --- /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 +# 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 '' % (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 + +
+ """ + + 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 + + + """ + + def render(self, resource): + library, resource_name = resource.split(':') + resolver = DottedNameResolver() + module = resolver.maybe_resolve(library) + resource = getattr(module, resource_name) + resource.need() + return '' diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/html.py --- /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 +# 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 = '''

This is a HTML text part.

''' + >>> html_to_text(html) + 'This is a HTML text part.\\n' + + >>> html = '''

This is text with french accents: é à è ù

''' + >>> html_to_text(html) + 'This is text with french accents: é à è ù\\n' + + HTML parser should handle entities correctly: + + >>> html = '''

Header

This is an < ò > entity.

''' + >>> html_to_text(html) + 'Header\\nThis is an < ò > entity.\\n\\n' + + >>> html = '''

Header

This is an < ò > entity.

''' + >>> 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/i18n.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/include.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/inherit.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/__init__.py --- /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 +# 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. + """ diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/data.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/inherit.py --- /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 +# 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") diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/intids.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/site.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/size.py --- /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 +# 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") diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/tales.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/text.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/timezone.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/traversing.py --- /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 +# 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()) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/tree.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/url.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/interfaces/zeo.py --- /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 +# 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""" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/intids.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/json.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/list.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.mo Binary file src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.mo has changed diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.po --- /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 , 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 \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" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/locales/pyams_utils.pot --- /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 , 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 \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 "" diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/lock.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/progress.py --- /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 +# 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']) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/property.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/protocol/__init__.py --- /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 @@ +# diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/protocol/http.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/protocol/tcp.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/protocol/xmlrpc.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/pygments.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/registry.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/request.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/schema.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/scripts/__init__.py --- /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 @@ +# diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/scripts/zodb.py --- /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 +# 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() diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/session.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/site.py --- /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 +# 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 ` 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/size.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/tales.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/tests/__init__.py --- /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 @@ + diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/tests/test_utilsdocs.py --- /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 +# 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') + diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/tests/test_utilsdocstrings.py --- /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 +# 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') diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/text.py --- /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 +# 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
 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', '
\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 = '
'.format('class="{0}"'.format(css_class) if css_class else '') + elements = value.split(character) + if start_tag: + elements[0] = '<{0}>{1}'.format(start_tag, elements[0]) + if end_tag: + elements[-1] = '<{0}>{1}'.format(end_tag, elements[-1]) + return br.join(elements) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/timezone/__init__.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/timezone/utility.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/timezone/vocabulary.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/traversing.py --- /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 +# 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)] diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/unicode.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/url.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/vocabulary.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/widget/__init__.py --- /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 @@ +# diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/widget/decimal.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/wsgi.py --- /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 +# 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 diff -r 000000000000 -r c8e21d7dd685 src/pyams_utils/zodb.py --- /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 +# 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) diff -r 000000000000 -r c8e21d7dd685 tox.ini --- /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