# HG changeset patch # User Thierry Florac # Date 1424303208 -3600 # Node ID 3f89629b9e548cb4390fa71f586d5dd407e1ea3f # Parent 16d47bd81d84de43a6dda1bb827c81b4892bcbbc First commit diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,194 @@ +Metadata-Version: 1.1 +Name: pyams-utils +Version: 0.1.0 +Summary: Utility functions and classes for PyAMS +Home-page: http://www.ztfy.org +Author: Thierry Florac +Author-email: tflorac@ulthar.net +License: ZPL +Description: =================== + pyams_utils package + =================== + + .. contents:: + + What is pyams_utils ? + ===================== + + pyams_utils is a set of classes and functions which can be used to provide many small services and + handle common operations in the context of a Pyramid application. + + Internal sub-packages include : + - date : convert dates to unicode ISO format, parse ISO datetime, convert date to datetime + - request : get current request, get request annotations, get and set request data via annotations + - timezone : convert datetime to a given timezone ; provides a server default timezone utility + - traversing : get object parents until a given interface is implemented + - unicode : convert any text to unicode for easy storage + - protocol : utility functions and modules for several nerwork protocols + - catalog : TextIndexNG index for Zope catalog and hurry.query "Text" query item + - text : simple text operations and text to HTML conversion + - html : HTML parser and HTML to text converter + - file : file upload data converter + - tal : text and HTML conversions for use from within TAL + + + How to use pyams_utils ? + ======================== + + A set of pyams_utils usages are given as doctests in pyams_utils/doctests/README.txt + + + Changelog + ========= + + 0.4.11 + ------ + - added "encode" and "decode" functions in "ztfy.utils.unicode" module (with updated doctests) + + 0.4.10 + ------ + - added configuration directives to remove static dependencies with "ztfy.skin" and "zopyx.txng3.core" + - updated Buildout's bootstrap + + 0.4.9 + ----- + - added "ztfy.utils.decorator" module with "@deprecated" decorator + + 0.4.8 + ----- + - remove security proxy in ITransactionManager adapter + + 0.4.7 + ----- + - added new "component" registry utility module + - added date and datetime display formats + - added "keep_chars" argument in translateString function + + 0.4.6 + ----- + - added ISet interface with permissions on zc.set.Set class + + 0.4.5 + ----- + - make current participation request optional in "indexObject()" function + + 0.4.4 + ----- + - added "allow_none" and "headers" arguments in XML-RPC "getClient()" methods + + 0.4.3 + ----- + - change test for date types in tztime/gmtime functions (because datetime + inherits from date!) + + 0.4.2 + ----- + - small correction in getHumanSize() function + - added dates formatting functions + - added check between date and datetime types in timezone module + + 0.4.1 + ----- + - use request locale formatter in getHumanSize function + + 0.4.0 + ----- + - move custom schema fields widgets to ZTFY.skin package + + 0.3.14 + ------ + - added legend on ZEO connection properties edit form + - force usage of "escapeSlashes" argument when checking new content name + + 0.3.13 + ------ + - added ZEO connection interface, utility and tools + - added "ztfy.utils.container" utility module + - added a persistent utility to store ZEO connection settings + - added "TextLine list" schema field and widget + - added request and session cached properties + - added Python 2.7 compatibility code and timeout parameter to XML-RPC + protocol helper + - changed request "data:" TAL namespace to basic HTTP request so it can be used + in views called via JSON-RPC or XML-RPC + + 0.3.12 + ------ + - updated package source layout + + 0.3.11 + ------ + - added dotted decimal schema field, not handling locales :-/ + + 0.3.10 + ------ + - upgraded for ztfy.jqueryui 0.6.0 + - added Color schema field and widget + - added StringLine schema field + - added "text:translate" TAL adapter + - moved ITransactionManager adapter from ztfy.scheduler package + + 0.3.9 + ----- + - added HTTP client based on httplib2, handling authentication and proxies + + 0.3.8 + ----- + - corrected encodings vocabulary + + 0.3.7 + ----- + - added encodings vocabulary + + 0.3.6 + ----- + - corrected code and translations in MissingPrincipal class + - added permissions on TextIndexNG index + + 0.3.5 + ----- + - re-add IList and IDict interfaces forgotten from bad merge :-( + + 0.3.4 + ----- + - better check for missing requests + + 0.3.3 + ----- + - Added "fanstatic:" TALES expression + + 0.3.2 + ----- + - Mark ztfy.utils.security functions and classes as deprecated + + 0.3.1 + ----- + - Updated signature in ztfy.utils.catalog.index to match last hurry.query release + + 0.3 + --- + - Switched to ZTK-1.1.2 and Python 2.6 + - Added "getAge" function in date module + - Added session module and TALES adapter to get/set session values + - Check None value in catalog.getObjectId(...) and catalog.getObject(...) methods + + 0.2.1 + ----- + - Added 'site.locateAndRegister' facility function + - Update ServerTimezoneUtility parent classes + + 0.2 + --- + - Added 'data' namespace to access request data + + 0.1 + --- + - Initial release + +Keywords: Pyramid PyAMS utilities +Platform: UNKNOWN +Classifier: License :: OSI Approved :: Zope Public License +Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python +Classifier: Framework :: Zope3 +Classifier: Topic :: Software Development :: Libraries :: Python Modules diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,67 @@ +MANIFEST.in +setup.py +docs/HISTORY.txt +docs/README.txt +src/pyams_utils/__init__.py +src/pyams_utils/adapter.py +src/pyams_utils/attr.py +src/pyams_utils/configure.zcml +src/pyams_utils/context.py +src/pyams_utils/data.py +src/pyams_utils/date.py +src/pyams_utils/decorator.py +src/pyams_utils/encoding.py +src/pyams_utils/html.py +src/pyams_utils/i18n.py +src/pyams_utils/list.py +src/pyams_utils/property.py +src/pyams_utils/registry.py +src/pyams_utils/request.py +src/pyams_utils/schema.py +src/pyams_utils/session.py +src/pyams_utils/site.py +src/pyams_utils/size.py +src/pyams_utils/tales.py +src/pyams_utils/text.py +src/pyams_utils/traversing.py +src/pyams_utils/unicode.py +src/pyams_utils/url.py +src/pyams_utils/wsgi.py +src/pyams_utils/zodb.py +src/pyams_utils.egg-info/PKG-INFO +src/pyams_utils.egg-info/SOURCES.txt +src/pyams_utils.egg-info/dependency_links.txt +src/pyams_utils.egg-info/entry_points.txt +src/pyams_utils.egg-info/namespace_packages.txt +src/pyams_utils.egg-info/not-zip-safe +src/pyams_utils.egg-info/requires.txt +src/pyams_utils.egg-info/top_level.txt +src/pyams_utils/browser/__init__.py +src/pyams_utils/browser/configure.zcml +src/pyams_utils/browser/decimal.py +src/pyams_utils/doctests/README.txt +src/pyams_utils/interfaces/__init__.py +src/pyams_utils/interfaces/data.py +src/pyams_utils/interfaces/site.py +src/pyams_utils/interfaces/size.py +src/pyams_utils/interfaces/tales.py +src/pyams_utils/interfaces/text.py +src/pyams_utils/interfaces/timezone.py +src/pyams_utils/locales/pyams_utils.pot +src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.mo +src/pyams_utils/locales/fr/LC_MESSAGES/pyams_utils.po +src/pyams_utils/protocol/__init__.py +src/pyams_utils/protocol/http.py +src/pyams_utils/protocol/xmlrpc.py +src/pyams_utils/scripts/__init__.py +src/pyams_utils/scripts/zodb.py +src/pyams_utils/tests/__init__.py +src/pyams_utils/tests/test_utilsdocs.py +src/pyams_utils/tests/test_utilsdocstrings.py +src/pyams_utils/timezone/__init__.py +src/pyams_utils/timezone/configure.zcml +src/pyams_utils/timezone/utility.py +src/pyams_utils/timezone/vocabulary.py +src/pyams_utils/zmi/__init__.py +src/pyams_utils/zmi/configure.zcml +src/pyams_utils/zmi/timezone.py \ No newline at end of file diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,5 @@ + + # -*- Entry points: -*- + [console_scripts] + pyams_upgrade = pyams_utils.scripts.zodb:upgrade_site + \ No newline at end of file diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,27 @@ +setuptools +babel +chameleon +docutils +httplib2 +persistent +pyramid +pyramid_zodbconn +pysocks +pytz +transaction +z3c.form +z3c.pt +z3c.ptcompat +ZODB +zope.annotation +zope.component +zope.container +zope.datetime +zope.interface +zope.schema +zope.security +zope.site +zope.traversing + +[test] +pyramid_zcml diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ +pyams_utils diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,64 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages +from chameleon import PageTemplateFile +from pyams_utils.context import ContextSelector +from pyams_utils.request import get_annotations, get_debug +from pyams_utils.site import site_factory +from pyams_utils.tales import ExtensionExpr +from pyams_utils.traversing import NamespaceTraverser +from z3c.pt.pagetemplate import PageTemplateFile as Z3cPageTemplateFile + +from pyramid.i18n import TranslationStringFactory +_ = TranslationStringFactory('pyams_utils') + + +def includeme(config): + """pyams_utils features include""" + + # add translations + config.add_translation_dirs('pyams_utils:locales') + + # define root factory + config.set_root_factory(site_factory) + + # add request annotations + config.add_request_method(get_annotations, 'annotations', reify=True) + config.add_request_method(get_debug, 'debug', reify=True) + + # add traverser handling namespaces via "++ns++(options)" URLs + config.add_traverser(NamespaceTraverser) + + # add custom subscriber predicate to filter events via supported interface(s) + config.add_subscriber_predicate('context_selector', ContextSelector) + + # load registry components + try: + import pyams_zmi + except ImportError: + config.scan(ignore='pyams_utils.zmi') + else: + config.scan() + + if hasattr(config, 'load_zcml'): + config.load_zcml('configure.zcml') + + PageTemplateFile.expression_types['extension'] = ExtensionExpr + Z3cPageTemplateFile.expression_types['extension'] = ExtensionExpr diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/adapter.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/adapter.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,96 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import venusian + +# import interfaces + +# import packages +from zope.interface import implementedBy + + +class ContextAdapter(object): + """Context adapter""" + + def __init__(self, context): + self.context = context + + +class ContextRequestAdapter(object): + """Context + request adapter""" + + def __init__(self, context, request): + self.context = context + self.request = request + + +class ContextRequestViewAdapter(object): + """Context + request + view adapter""" + + def __init__(self, context, request, view): + self.context = context + self.request = request + self.view = view + + +class adapter_config(object): + """Function or class decorator to declare an adapter""" + + venusian = venusian + + def __init__(self, **settings): + if 'for_' in settings: + if settings.get('context') is None: + settings['context'] = settings.pop('for_') + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + depth = settings.pop('_depth', 0) + + def callback(context, name, ob): + adapts = settings.get('context') + if adapts is None: + adapts = getattr(ob, '__component_adapts__', None) + if adapts is None: + raise TypeError("No for argument was provided for %r and " + "can't determine what the factory adapts." % ob) + if not isinstance(adapts, tuple): + adapts = (adapts,) + + provides = settings.get('provides') + if provides is None: + intfs = list(implementedBy(ob)) + if len(intfs) == 1: + provides = intfs[0] + if provides is None: + raise TypeError("Missing 'provided' argument") + + config = context.config.with_package(info.module) + config.registry.registerAdapter(ob, adapts, provides, settings.get('name', '')) + + info = self.venusian.attach(wrapped, callback, category='pyams_adapter', + depth=depth + 1) + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/attr.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/attr.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,35 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from zope.traversing.interfaces import ITraversable + +# import packages +from pyams_utils.adapter import ContextAdapter, adapter_config +from pyramid.exceptions import NotFound +from zope.interface import Interface + + +@adapter_config(name='attr', context=Interface, provides=ITraversable) +class AttributeTraverser(ContextAdapter): + """++attr++ namespace traverser""" + + def traverse(self, name, furtherpath=None): + try: + return getattr(self.context, name) + except AttributeError: + raise NotFound diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/configure.zcml Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/context.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/context.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages + + +class ContextSelector(object): + """Interface based context selector + + This selector can be used on any subscriber to define + interfaces that the context must support for the event + to be applied + """ + + def __init__(self, ifaces, config): + if not isinstance(ifaces, (list, tuple)): + ifaces = (ifaces,) + self.interfaces = ifaces + + def text(self): + return 'context_selector = %s' % str(self.interfaces) + + phash = text + + def __call__(self, event): + for intf in self.interfaces: + try: + if intf.providedBy(event.object): + return True + except (AttributeError, TypeError): + if isinstance(event.object, intf): + return True + return False diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/data.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,64 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import json + +# import interfaces +from pyams_utils.interfaces.data import IObjectData, IObjectDataRenderer +from pyams_utils.interfaces.tales import ITALESExtension +from pyramid.interfaces import IRequest +from zope.publisher.interfaces.browser import IBrowserRequest + +# import packages +from pyams_utils.adapter import ContextAdapter, ContextRequestViewAdapter, adapter_config +from zope.interface import Interface + + +@adapter_config(context=IObjectData, provides=IObjectDataRenderer) +class ObjectDataRenderer(ContextAdapter): + """Object data JSON renderer""" + + def get_object_data(self): + data = IObjectData(self.context) + return json.dumps(data.object_data) if data is not None else None + + +@adapter_config(name='object_data', context=(Interface, Interface, Interface), provides=ITALESExtension) +class ObjectDataExtension(ContextRequestViewAdapter): + """extension:object_data TALES extension""" + + def render(self, context=None): + if context is None: + context = self.context + renderer = IObjectDataRenderer(context, None) + if renderer is not None: + return renderer.get_object_data() + + +@adapter_config(name='request_data', context=(Interface, IRequest, Interface), provides=ITALESExtension) +class PyramidRequestDataExtension(ContextRequestViewAdapter): + """extension:request_data TALES extension for Pyramid request""" + + def render(self, params=None): + return self.request.annotations.get(params) + + +@adapter_config(name='request_data', context=(Interface, IBrowserRequest, Interface), provides=ITALESExtension) +class BrowserRequestDataExtension(ContextRequestViewAdapter): + """extension:request_data TALES extension for Zope browser request""" + + def render(self, params=None): + return self.request.annotations.get(params) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/date.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/date.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,194 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard packages +from datetime import datetime + +# import interfaces + +# import packages +from pyams_utils.request import check_request +from pyams_utils.timezone import gmtime, tztime +from zope.datetime import parseDatetimetz + +from pyams_utils import _ + + +def unidate(value): + """Get specified date converted to unicode ISO format + + Dates are always assumed to be stored in GMT timezone + + @param value: input date to convert to unicode + @type value: date or datetime + @return: input date converted to unicode + @rtype: unicode + """ + if value is not None: + value = gmtime(value) + return value.isoformat('T') + return None + + +def parse_date(value): + """Get date specified in unicode ISO format to Python datetime object + + Dates are always assumed to be stored in GMT timezone + + @param value: unicode date to be parsed + @type value: unicode + @return: the specified value, converted to datetime + @rtype: datetime + """ + if value is not None: + return gmtime(parseDatetimetz(value)) + return None + + +def date_to_datetime(value): + """Get datetime value converted from a date or datetime object + + @param value: a date or datetime value to convert + @type value: date or datetime + @return: input value converted to datetime + @rtype: datetime + """ + if type(value) is datetime: + return value + return datetime(value.year, value.month, value.day) + + +SH_DATE_FORMAT = _("%d/%m/%Y") +SH_DATETIME_FORMAT = _("%d/%m/%Y - %H:%M") + +EXT_DATE_FORMAT = _("on %d/%m/%Y") +EXT_DATETIME_FORMAT = _("on %d/%m/%Y at %H:%M") + + +def format_date(value, format=EXT_DATE_FORMAT, request=None): + """Format given date with the given format""" + if request is None: + request = check_request() + localizer = request.localizer + return datetime.strftime(tztime(value), localizer.translate(format)) + + +def format_datetime(value, format=EXT_DATETIME_FORMAT, request=None): + """Format given datetime with the given format""" + return format_date(value, format, request) + + +def get_age(value): + """Get age of a given datetime (including timezone) compared to current datetime (in UTC) + + @param value: a datetime value, including timezone + @type value: datetime + @return: string representing value age + @rtype: gettext translated string + """ + request = check_request() + translate = request.localizer.translate + now = gmtime(datetime.utcnow()) + delta = now - value + if delta.days > 60: + return translate(_("%d months ago")) % int(round(delta.days * 1.0 / 30)) + elif delta.days > 10: + return translate(_("%d weeks ago")) % int(round(delta.days * 1.0 / 7)) + elif delta.days > 2: + return translate(_("%d days ago")) % delta.days + elif delta.days == 2: + return translate(_("the day before yesterday")) + elif delta.days == 1: + return translate(_("yesterday")) + else: + hours = int(round(delta.seconds * 1.0 / 3600)) + if hours > 1: + return translate(_("%d hours ago")) % hours + elif delta.seconds > 300: + return translate(_("%d minutes ago")) % int(round(delta.seconds * 1.0 / 60)) + else: + return translate(_("less than 5 minutes ago")) + + +def get_duration(v1, v2=None, request=None): + """Get delta as string between two dates + + >>> from datetime import datetime + >>> from pyams_utils.date import get_duration + >>> from pyramid.testing import DummyRequest + >>> request = DummyRequest() + >>> date1 = datetime(2015, 1, 1) + >>> date2 = datetime(2014, 3, 1) + >>> get_duration(date1, date2, request) + '10 months' + + Dates order is not important: + >>> get_duration(date2, date1, request) + '10 months' + >>> date2 = datetime(2014, 11, 10) + >>> get_duration(date1, date2, request) + '7 weeks' + >>> date2 = datetime(2014, 12, 26) + >>> get_duration(date1, date2, request) + '6 days' + + For durations lower than 2 days, duration also display hours: + >>> date1 = datetime(2015, 1, 1) + >>> date2 = datetime(2015, 1, 2, 15, 10, 0) + >>> get_duration(date1, date2, request) + '1 day and 15 hours' + >>> date2 = datetime(2015, 1, 2) + >>> get_duration(date1, date2, request) + '24 hours' + >>> date2 = datetime(2015, 1, 1, 13, 12) + >>> get_duration(date1, date2, request) + '13 hours' + >>> date2 = datetime(2015, 1, 1, 1, 15) + >>> get_duration(date1, date2, request) + '75 minutes' + >>> date2 = datetime(2015, 1, 1, 0, 0, 15) + >>> get_duration(date1, date2, request) + '15 seconds' + """ + if v2 is None: + v2 = datetime.utcnow() + assert isinstance(v1, datetime) and isinstance(v2, datetime) + if request is None: + request = check_request() + translate = request.localizer.translate + v1, v2 = min(v1, v2), max(v1, v2) + delta = v2 - v1 + if delta.days > 60: + return translate(_("%d months")) % int(round(delta.days * 1.0 / 30)) + elif delta.days > 10: + return translate(_("%d weeks")) % int(round(delta.days * 1.0 / 7)) + elif delta.days >= 2: + return translate(_("%d days")) % delta.days + else: + hours = int(round(delta.seconds * 1.0 / 3600)) + if delta.days == 1: + if hours == 0: + return translate(_("24 hours")) + else: + return translate(_("%d day and %d hours")) % (delta.days, hours) + else: + if hours > 2: + return translate(_("%d hours")) % hours + else: + minutes = int(round(delta.seconds * 1.0 / 60)) + if minutes > 2: + return translate(_("%d minutes")) % minutes + else: + return translate(_("%d seconds")) % delta.seconds diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/decorator.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/decorator.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,47 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import functools +import warnings + +# import interfaces + +# import packages + + +def deprecated(*msg): + """This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + """ + + def decorator(func): + + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.warn_explicit("Function %s is deprecated. %s" % (func.__name__, message), + category=DeprecationWarning, + filename=func.func_code.co_filename, + lineno=func.func_code.co_firstlineno + 1) + return func(*args, **kwargs) + return new_func + + if len(msg) == 1 and callable(msg[0]): + message = u'' + return decorator(msg[0]) + else: + message = msg[0] + return decorator diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/doctests/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/doctests/README.txt Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,135 @@ +================== +ZTFY.utils package +================== + +Introduction +------------ + +This package is composed of a set of utility functions, which in complement with zope.app.zapi +package can make Zope management easier. + + +Unicode functions +----------------- + +While working with extended characters sets containing accentuated characters, it's necessary to +conversing strings to UTF8 so that they can be used without any conversion problem. + + >>> from pyams_utils import unicode + +'translate_string' is a utility function which can be used, for example, to generate an object's id +without space and with accentuated characters converted to their unaccentuated version: + + >>> sample = 'Mon titre accentué' + >>> unicode.translate_string(sample) + 'mon titre accentue' + +Results are lower-cased by default ; this can be avoided be setting the 'force_lower' argument +to False: + + >>> unicode.translate_string(sample, force_lower=False) + 'Mon titre accentue' + >>> unicode.translate_string(sample, force_lower=True, spaces='-') + 'mon-titre-accentue' + +If input string can contain 'slashes' (/) or 'backslashes' (\), they are normally removed ; +by using the 'escape_slashes' parameter, the input string is splitted and only the last element is +returned ; this is handy to handle filenames on Windows platform: + + >>> sample = 'Autre / chaîne / accentuée' + >>> unicode.translate_string(sample) + 'autre chaine accentuee' + >>> unicode.translate_string(sample, escape_slashes=True) + 'accentuee' + >>> sample = 'C:\\Program Files\\My Application\\test.txt' + >>> unicode.translate_string(sample) + 'cprogram filesmy applicationtest.txt' + >>> unicode.translate_string(sample, escape_slashes=True) + 'test.txt' + +To remove remaining spaces or convert them to another character, you can use the "spaces" parameter +which can contain any string to be used instead of initial spaces: + + >>> sample = 'C:\\Program Files\\My Application\\test.txt' + >>> unicode.translate_string(sample, spaces=' ') + 'cprogram filesmy applicationtest.txt' + >>> unicode.translate_string(sample, spaces='-') + 'cprogram-filesmy-applicationtest.txt' + +Spaces replacement is made in the last step, so using it with "escape_slashes" parameter only affects +the final result: + + >>> unicode.translate_string(sample, escape_slashes=True, spaces='-') + 'test.txt' + +Unicode module also provides encoding and decoding functions: + + >>> var = b'Cha\xeene accentu\xe9e' + >>> unicode.decode(var, 'latin1') + 'Chaîne accentuée' + >>> unicode.encode(unicode.decode(var, 'latin1'), 'latin1') == var + True + + >>> utf = 'Chaîne accentuée' + >>> unicode.encode(utf, 'latin1') + b'Cha\xeene accentu\xe9e' + >>> unicode.decode(unicode.encode(utf, 'latin1'), 'latin1') == utf + True + + +Dates functions +--------------- + +Dates functions are used to convert dates from/to string representation: + + >>> from datetime import datetime + >>> from pyams_utils import date + >>> now = datetime.fromtimestamp(1205000000) + >>> now + datetime.datetime(2008, 3, 8, 19, 13, 20) + +You can get an unicode representation of a date in ASCII format using 'unidate' fonction ; date is +converted to GMT: + + >>> udate = date.unidate(now) + >>> udate + '2008-03-08T19:13:20+00:00' + +'parse_date' can be used to convert ASCII format into datetime: + + >>> ddate = date.parse_date(udate) + >>> ddate + datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=) + +'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 game me headaches at first. I finally concluded that the best way (for me !) to handle +TZ data was to store every datetime value in GMT timezone. +As far as I know, there is no easy way to know the user's timezone from his request settings. So you can: +- store this timezone in user's profile, +- define a static server's timezone +- create and register a ServerTimezoneUtility to handle server default timezone. + +My current default user's timezone is set to 'Europe/Paris' ; you should probably update this setting in +'timezone.py' if you are located elsewhere. + + >>> from pyams_utils import timezone + >>> timezone.tztime(ddate) + datetime.datetime(2008, 3, 8, 19, 13, 20, tzinfo=) + +'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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/encoding.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/encoding.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,153 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from zope.schema.interfaces import IVocabularyFactory, IChoice + +# import packages +from pyams_utils.request import check_request +from zope.interface import provider, implementer +from zope.schema import Choice +from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary, getVocabularyRegistry + +from pyams_utils import _ + + +ENCODINGS = { + 'ascii': _('English (ASCII)'), + 'big5': _('Traditional Chinese (big5)'), + 'big5hkscs': _('Traditional Chinese (big5hkscs)'), + 'cp037': _('English (cp037)'), + 'cp424': _('Hebrew (cp424)'), + 'cp437': _('English (cp437)'), + 'cp500': _('Western Europe (cp500)'), + 'cp720': _('Arabic (cp720)'), + 'cp737': _('Greek (cp737)'), + 'cp775': _('Baltic languages (cp775)'), + 'cp850': _('Western Europe (cp850)'), + 'cp852': _('Central and Eastern Europe (cp852)'), + 'cp855': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)'), + 'cp856': _('Hebrew (cp856)'), + 'cp857': _('Turkish (cp857)'), + 'cp858': _('Western Europe (cp858)'), + 'cp860': _('Portuguese (cp860)'), + 'cp861': _('Icelandic (cp861)'), + 'cp862': _('Hebrew (cp862)'), + 'cp863': _('Canadian (cp863)'), + 'cp864': _('Arabic (cp864)'), + 'cp865': _('Danish, Norwegian (cp865)'), + 'cp866': _('Russian (cp866)'), + 'cp869': _('Greek (cp869)'), + 'cp874': _('Thai (cp874)'), + 'cp875': _('Greek (cp875)'), + 'cp932': _('Japanese (cp932)'), + 'cp949': _('Korean (cp949)'), + 'cp950': _('Traditional Chinese (cp950)'), + 'cp1006': _('Urdu (cp1006)'), + 'cp1026': _('Turkish (cp1026)'), + 'cp1140': _('Western Europe (cp1140)'), + 'cp1250': _('Central and Eastern Europe (cp1250)'), + 'cp1251': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)'), + 'cp1252': _('Western Europe (cp1252)'), + 'cp1253': _('Greek (cp1253)'), + 'cp1254': _('Turkish (cp1254)'), + 'cp1255': _('Hebrew (cp1255)'), + 'cp1256': _('Arabic (cp1256)'), + 'cp1257': _('Baltic languages (cp1257)'), + 'cp1258': _('Vietnamese (cp1258)'), + 'euc_jp': _('Japanese (euc_jp)'), + 'euc_jis_2004': _('Japanese (euc_jis_2004)'), + 'euc_jisx0213': _('Japanese (euc_jisx0213)'), + 'euc_kr': _('Korean (euc_kr)'), + 'gb2312': _('Simplified Chinese (gb2312)'), + 'gbk': _('Unified Chinese (gbk)'), + 'gb18030': _('Unified Chinese (gb18030)'), + 'hz': _('Simplified Chinese (hz)'), + 'iso2022_jp': _('Japanese (iso2022_jp)'), + 'iso2022_jp_1': _('Japanese (iso2022_jp_1)'), + 'iso2022_jp_2': _('Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)'), + 'iso2022_jp_2004': _('Japanese (iso2022_jp_2004)'), + 'iso2022_jp_3': _('Japanese (iso2022_jp_3)'), + 'iso2022_jp_ext': _('Japanese (iso2022_jp_ext)'), + 'iso2022_kr': _('Korean (iso2022_kr)'), + 'latin_1': _('West Europe (latin_1)'), + 'iso8859_2': _('Central and Eastern Europe (iso8859_2)'), + 'iso8859_3': _('Esperanto, Maltese (iso8859_3)'), + 'iso8859_4': _('Baltic languages (iso8859_4)'), + 'iso8859_5': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)'), + 'iso8859_6': _('Arabic (iso8859_6)'), + 'iso8859_7': _('Greek (iso8859_7)'), + 'iso8859_8': _('Hebrew (iso8859_8)'), + 'iso8859_9': _('Turkish (iso8859_9)'), + 'iso8859_10': _('Nordic languages (iso8859_10)'), + 'iso8859_13': _('Baltic languages (iso8859_13)'), + 'iso8859_14': _('Celtic languages (iso8859_14)'), + 'iso8859_15': _('Western Europe (iso8859_15)'), + 'iso8859_16': _('South-Eastern Europe (iso8859_16)'), + 'johab': _('Korean (johab)'), + 'koi8_r': _('Russian (koi8_r)'), + 'koi8_u': _('Ukrainian (koi8_u)'), + 'mac_cyrillic': _('Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)'), + 'mac_greek': _('Greek (mac_greek)'), + 'mac_iceland': _('Icelandic (mac_iceland)'), + 'mac_latin2': _('Central and Eastern Europe (mac_latin2)'), + 'mac_roman': _('Western Europe (mac_roman)'), + 'mac_turkish': _('Turkish (mac_turkish)'), + 'ptcp154': _('Kazakh (ptcp154)'), + 'shift_jis': _('Japanese (shift_jis)'), + 'shift_jis_2004': _('Japanese (shift_jis_2004)'), + 'shift_jisx0213': _('Japanese (shift_jisx0213)'), + 'utf_32': _('all languages (utf_32)'), + 'utf_32_be': _('all languages (utf_32_be)'), + 'utf_32_le': _('all languages (utf_32_le)'), + 'utf_16': _('all languages (utf_16)'), + 'utf_16_be': _('all languages (BMP only - utf_16_be)'), + 'utf_16_le': _('all languages (BMP only - utf_16_le)'), + 'utf_7': _('all languages (utf_7)'), + 'utf_8': _('all languages (utf_8)'), + 'utf_8_sig': _('all languages (utf_8_sig)'), +} + + +@provider(IVocabularyFactory) +class EncodingsVocabulary(SimpleVocabulary): + + def __init__(self, terms, *interfaces): + request = check_request() + translate = request.localizer.translate + terms = [SimpleTerm(v, title=translate(t)) for v, t in ENCODINGS.items()] + terms.sort(key=lambda x: x.title) + super(EncodingsVocabulary, self).__init__(terms, *interfaces) + +getVocabularyRegistry().register('PyAMS encodings', EncodingsVocabulary) + + +class IEncodingField(IChoice): + """Encoding field interface""" + + +@implementer(IEncodingField) +class EncodingField(Choice): + """Encoding field""" + + def __init__(self, vocabulary='PyAMS encodings', **kw): + if 'values' in kw: + del kw['values'] + if 'source' in kw: + del kw['source'] + kw['vocabulary'] = vocabulary + super(EncodingField, self).__init__(**kw) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/html.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/html.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,124 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from html.parser import HTMLParser + +# import interfaces + +# import packages + + +class MyHTMLParser(HTMLParser): + """HTML parser""" + data = '' + entitydefs = {'amp': '&', 'lt': '<', 'gt': '>', + 'nbsp': ' ', + 'apos': "'", 'quot': '"', + 'Agrave': 'À', 'Aacute': 'A', 'Acirc': 'Â', 'Atilde': 'A', 'Auml': 'Ä', 'Aring': 'A', + 'AElig': 'AE', + 'Ccedil': 'Ç', + 'Egrave': 'É', 'Eacute': 'È', 'Ecirc': 'Ê', 'Euml': 'Ë', + 'Igrave': 'I', 'Iacute': 'I', 'Icirc': 'I', 'Iuml': 'I', + 'Ntilde': 'N', + 'Ograve': 'O', 'Oacute': 'O', 'Ocirc': 'Ô', 'Otilde': 'O', 'Ouml': 'Ö', 'Oslash': 'O', + 'Ugrave': 'Ù', 'Uacute': 'U', 'Ucirc': 'Û', 'Uuml': 'Ü', + 'Yacute': 'Y', + 'THORN': 'T', + 'agrave': 'à', 'aacute': 'a', 'acirc': 'â', 'atilde': 'a', 'auml': 'ä', 'aring': 'a', 'aelig': 'ae', + 'ccedil': 'ç', + 'egrave': 'è', 'eacute': 'é', 'ecirc': 'ê', 'euml': 'ë', + 'igrave': 'i', 'iacute': 'i', 'icirc': 'î', 'iuml': 'ï', + 'ntilde': 'n', + 'ograve': 'o', 'oacute': 'o', 'ocirc': 'ô', 'otilde': 'o', 'ouml': 'ö', 'oslash': 'o', + 'ugrave': 'ù', 'uacute': 'u', 'ucirc': 'û', 'uuml': 'ü', + 'yacute': 'y', + 'thorn': 't', + 'yuml': 'ÿ'} + + charrefs = {34: '"', 38: '&', 39: "'", + 60: '<', 62: '>', + 192: 'À', 193: 'A', 194: 'Â', 195: 'A', 196: 'Ä', 197: 'A', + 198: 'AE', + 199: 'Ç', + 200: 'È', 201: 'É', 202: 'Ê', 203: 'Ë', + 204: 'I', 205: 'I', 206: 'Î', 207: 'Ï', + 208: 'D', + 209: 'N', + 210: 'O', 211: 'O', 212: 'Ô', 213: 'O', 214: 'Ö', 216: 'O', + 215: 'x', + 217: 'Ù', 218: 'U', 219: 'Û', 220: 'Ü', + 221: 'Y', 222: 'T', + 223: 'sz', + 224: 'à', 225: 'a', 226: 'â', 227: 'a', 228: 'ä', 229: 'a', + 230: 'ae', + 231: 'ç', + 232: 'è', 233: 'é', 234: 'ê', 235: 'ë', + 236: 'i', 237: 'i', 238: 'î', 239: 'ï', + 240: 'e', + 241: 'n', + 242: 'o', 243: 'o', 244: 'ô', 245: 'o', 246: 'ö', 248: 'o', + 249: 'ù', 250: 'u', 251: 'û', 252: 'ü', + 253: 'y', 255: 'ÿ'} + + def handle_data(self, data): + try: + self.data += data + except: + self.data += data.decode('utf8') + + def handle_entityref(self, name): + self.data += self.entitydefs.get(name, '') + + def handle_charref(self, name): + try: + n = int(name) + except ValueError: + return + if not 0 <= n <= 255: + return + self.handle_data(self.charrefs.get(n)) + + def handle_starttag(self, tag, attrs): + if tag == 'td': + self.data += ' ' + elif tag == 'br': + self.data += '\n' + + def handle_endtag(self, tag): + if tag == 'p': + self.data += '\n' + + +def html_to_text(value): + """Utility function to extract text content from HTML + + >>> from pyams_utils.html import html_to_text + >>> html = '''

This is a HTML text part.

''' + >>> html_to_text(html) + 'This is a HTML text part.\\n' + + HTML parser should handle entities correctly: + >>> html = '''

Header

This is an < ò > entity.

''' + >>> html_to_text(html) + 'Header\\nThis is an < o > entity.\\n\\n' + + """ + if value is None: + return '' + parser = MyHTMLParser() + parser.feed(value) + parser.close() + return parser.data diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/i18n.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/i18n.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,76 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages + + +def normalize_lang(lang): + lang = lang.strip().lower() + lang = lang.replace('_', '-') + lang = lang.replace(' ', '') + return lang + + +def get_browser_language(request): + """Custom locale negotiator + + Copied from zope.publisher code + """ + accept_langs = request.headers.get('Accept-Language', '').split(',') + + # Normalize lang strings + accept_langs = [normalize_lang(l) for l in accept_langs] + # Then filter out empty ones + accept_langs = [l for l in accept_langs if l] + + accepts = [] + for index, lang in enumerate(accept_langs): + l = lang.split(';', 2) + + # If not supplied, quality defaults to 1... + quality = 1.0 + + if len(l) == 2: + q = l[1] + if q.startswith('q='): + q = q.split('=', 2)[1] + try: + quality = float(q) + except ValueError: + # malformed quality value, skip it. + continue + + if quality == 1.0: + # ... but we use 1.9 - 0.001 * position to + # keep the ordering between all items with + # 1.0 quality, which may include items with no quality + # defined, and items with quality defined as 1. + quality = 1.9 - (0.001 * index) + + accepts.append((quality, l[0])) + + # Filter langs with q=0, which means + # unwanted lang according to the spec + # See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 + accepts = [acc for acc in accepts if acc[0]] + + accepts.sort() + accepts.reverse() + + return [lang for qual, lang in accepts][0] if accepts else None diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,30 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages + + +# ZODB application name settings key +PYAMS_APPLICATION_SETTINGS_KEY = 'pyams.application_name' + +# ZODB default application name +PYAMS_APPLICATION_DEFAULT_NAME = 'application' + +# Settings key to define site root factory +PYAMS_APPLICATION_FACTORY_KEY = 'pyams.application_factory' diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/data.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/data.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,36 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# 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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/site.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/site.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,58 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.component.interfaces import IObjectEvent + +# import packages +from zope.interface import Interface, Attribute + + +class ISiteRoot(IAttributeAnnotatable): + """Marker interface for site root""" + + +class INewLocalSiteCreatedEvent(IObjectEvent): + """Event interface when a new site root has been created""" + + +class ISiteUpgradeEvent(IObjectEvent): + """Event interface when a site upgrade is requested""" + + +SITE_GENERATIONS_KEY = 'pyams.generations' + + +class ISiteGenerations(Interface): + """Site generations interface""" + + generation = Attribute("Current schema generation") + + def evolve(self, site, current=None): + """Evolve from current generation to last one""" + + +class IConfigurationManager(IAttributeAnnotatable): + """Configuration manager marker interface""" + + +class IConfigurationFactory(Interface): + """Configuration factory interface + + This factory may be loaded through an adapter + """ diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/size.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/size.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,28 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# 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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/tales.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/tales.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,31 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# 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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/text.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/text.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,37 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# 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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/interfaces/timezone.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/timezone.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,49 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# 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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/list.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/list.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,41 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages + + +def unique(seq, idfun=None): + """Extract unique values from list, preserving order + + >>> from pyams_utils.list import unique + >>> mylist = [1, 2, 3, 2, 1] + >>> unique(mylist) + [1, 2, 3] + """ + if idfun is None: + def idfun(x): return x + seen = {} + result = [] + for item in seq: + marker = idfun(item) + if marker in seen: + continue + seen[marker] = 1 + result.append(item) + return result diff -r 16d47bd81d84 -r 3f89629b9e54 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 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,521 @@ +# French translations for PACKAGE package +# This file is distributed under the same license as the PACKAGE package. +# Thierry Florac , 2015. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-01-24 16:53+0100\n" +"PO-Revision-Date: 2015-01-18 01:01+0100\n" +"Last-Translator: Thierry Florac \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Generated-By: Lingua 3.7\n" + +#: src/pyams_utils/encoding.py:33 +msgid "English (ASCII)" +msgstr "Anglais (ASCII)" + +#: src/pyams_utils/encoding.py:34 +msgid "Traditional Chinese (big5)" +msgstr "Chinois traditionnel (big5)" + +#: src/pyams_utils/encoding.py:35 +msgid "Traditional Chinese (big5hkscs)" +msgstr "Chinois traditionnel (big5hkscs)" + +#: src/pyams_utils/encoding.py:36 +msgid "English (cp037)" +msgstr "Anglais (cp037)" + +#: src/pyams_utils/encoding.py:37 +msgid "Hebrew (cp424)" +msgstr "Hébreu (cp424)" + +#: src/pyams_utils/encoding.py:38 +msgid "English (cp437)" +msgstr "Anglais (cp437)" + +#: src/pyams_utils/encoding.py:39 +msgid "Western Europe (cp500)" +msgstr "Europe de l'ouest (cp500)" + +#: src/pyams_utils/encoding.py:40 +msgid "Arabic (cp720)" +msgstr "Arabe (cp720)" + +#: src/pyams_utils/encoding.py:41 +msgid "Greek (cp737)" +msgstr "Grec (cp737)" + +#: src/pyams_utils/encoding.py:42 +msgid "Baltic languages (cp775)" +msgstr "Langues baltes (cp775)" + +#: src/pyams_utils/encoding.py:43 +msgid "Western Europe (cp850)" +msgstr "Europe de l'ouest (cp850)" + +#: src/pyams_utils/encoding.py:44 +msgid "Central and Eastern Europe (cp852)" +msgstr "Europe centrale et de l'est (cp852)" + +#: src/pyams_utils/encoding.py:45 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)" +msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (cp855)" + +#: src/pyams_utils/encoding.py:46 +msgid "Hebrew (cp856)" +msgstr "Hébreu (cp856)" + +#: src/pyams_utils/encoding.py:47 +msgid "Turkish (cp857)" +msgstr "Turc (cp857)" + +#: src/pyams_utils/encoding.py:48 +msgid "Western Europe (cp858)" +msgstr "Europe de l'ouest (cp858)" + +#: src/pyams_utils/encoding.py:49 +msgid "Portuguese (cp860)" +msgstr "Portugais (cp860)" + +#: src/pyams_utils/encoding.py:50 +msgid "Icelandic (cp861)" +msgstr "Islandais (cp861)" + +#: src/pyams_utils/encoding.py:51 +msgid "Hebrew (cp862)" +msgstr "Hébreu (cp862)" + +#: src/pyams_utils/encoding.py:52 +msgid "Canadian (cp863)" +msgstr "Canadien (cp863)" + +#: src/pyams_utils/encoding.py:53 +msgid "Arabic (cp864)" +msgstr "Arabe (cp864)" + +#: src/pyams_utils/encoding.py:54 +msgid "Danish, Norwegian (cp865)" +msgstr "Danois, Norvégien (cp865)" + +#: src/pyams_utils/encoding.py:55 +msgid "Russian (cp866)" +msgstr "Russe (cp866)" + +#: src/pyams_utils/encoding.py:56 +msgid "Greek (cp869)" +msgstr "Grec (cp869)" + +#: src/pyams_utils/encoding.py:57 +msgid "Thai (cp874)" +msgstr "Thaï (cp874)" + +#: src/pyams_utils/encoding.py:58 +msgid "Greek (cp875)" +msgstr "Grec (cp875)" + +#: src/pyams_utils/encoding.py:59 +msgid "Japanese (cp932)" +msgstr "Japonais (cp932)" + +#: src/pyams_utils/encoding.py:60 +msgid "Korean (cp949)" +msgstr "Coréen (cp949)" + +#: src/pyams_utils/encoding.py:61 +msgid "Traditional Chinese (cp950)" +msgstr "Chinois traditionnel (cp950)" + +#: src/pyams_utils/encoding.py:62 +msgid "Urdu (cp1006)" +msgstr "Ourdou (cp1006)" + +#: src/pyams_utils/encoding.py:63 +msgid "Turkish (cp1026)" +msgstr "Turc (cp1026)" + +#: src/pyams_utils/encoding.py:64 +msgid "Western Europe (cp1140)" +msgstr "Europe de l'ouest (cp1140)" + +#: src/pyams_utils/encoding.py:65 +msgid "Central and Eastern Europe (cp1250)" +msgstr "Europe centrale et de l'est (cp1250)" + +#: src/pyams_utils/encoding.py:66 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)" +msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (cp1251)" + +#: src/pyams_utils/encoding.py:67 +msgid "Western Europe (cp1252)" +msgstr "Europe de l'ouest (cp1252)" + +#: src/pyams_utils/encoding.py:68 +msgid "Greek (cp1253)" +msgstr "Grec (cp1253)" + +#: src/pyams_utils/encoding.py:69 +msgid "Turkish (cp1254)" +msgstr "Turc (cp1254)" + +#: src/pyams_utils/encoding.py:70 +msgid "Hebrew (cp1255)" +msgstr "Hébreu (cp1255)" + +#: src/pyams_utils/encoding.py:71 +msgid "Arabic (cp1256)" +msgstr "Arabe (cp1256)" + +#: src/pyams_utils/encoding.py:72 +msgid "Baltic languages (cp1257)" +msgstr "Langues baltes (cp1257)" + +#: src/pyams_utils/encoding.py:73 +msgid "Vietnamese (cp1258)" +msgstr "Viernamien (cp1258)" + +#: src/pyams_utils/encoding.py:74 +msgid "Japanese (euc_jp)" +msgstr "Japonais (euc-jp)" + +#: src/pyams_utils/encoding.py:75 +msgid "Japanese (euc_jis_2004)" +msgstr "Japonais (euc-jis-2004)" + +#: src/pyams_utils/encoding.py:76 +msgid "Japanese (euc_jisx0213)" +msgstr "Japonais (euc-jisx0213)" + +#: src/pyams_utils/encoding.py:77 +msgid "Korean (euc_kr)" +msgstr "Coréen (euc-kr)" + +#: src/pyams_utils/encoding.py:78 +msgid "Simplified Chinese (gb2312)" +msgstr "Chinois simplifié (gb2312)" + +#: src/pyams_utils/encoding.py:79 +msgid "Unified Chinese (gbk)" +msgstr "Chinois unifié (gbk)" + +#: src/pyams_utils/encoding.py:80 +msgid "Unified Chinese (gb18030)" +msgstr "Chinois unifié (gb18030)" + +#: src/pyams_utils/encoding.py:81 +msgid "Simplified Chinese (hz)" +msgstr "Chinois simplifié (hz)" + +#: src/pyams_utils/encoding.py:82 +msgid "Japanese (iso2022_jp)" +msgstr "Japonais (iso2022-jp)" + +#: src/pyams_utils/encoding.py:83 +msgid "Japanese (iso2022_jp_1)" +msgstr "Japonais (iso2022-jp-1)" + +#: src/pyams_utils/encoding.py:84 +msgid "" +"Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)" +msgstr "" +"Japonais, Coréen, Chinois simplifié, Europe de l'ouest, Grec (iso2022-jp-2)" + +#: src/pyams_utils/encoding.py:85 +msgid "Japanese (iso2022_jp_2004)" +msgstr "Japonais (iso2022-jp-2004)" + +#: src/pyams_utils/encoding.py:86 +msgid "Japanese (iso2022_jp_3)" +msgstr "Japonais (iso2022-jp-3)" + +#: src/pyams_utils/encoding.py:87 +msgid "Japanese (iso2022_jp_ext)" +msgstr "Japonais (iso2022-jp-ext)" + +#: src/pyams_utils/encoding.py:88 +msgid "Korean (iso2022_kr)" +msgstr "Coréen (iso2022-kr)" + +#: src/pyams_utils/encoding.py:89 +msgid "West Europe (latin_1)" +msgstr "Europe de l'ouest (latin-1)" + +#: src/pyams_utils/encoding.py:90 +msgid "Central and Eastern Europe (iso8859_2)" +msgstr "Europe centrale et de l'est (iso8859-2)" + +#: src/pyams_utils/encoding.py:91 +msgid "Esperanto, Maltese (iso8859_3)" +msgstr "Espéranto, Maltais (iso8859-3)" + +#: src/pyams_utils/encoding.py:92 +msgid "Baltic languages (iso8859_4)" +msgstr "Langues baltes (iso8859-4)" + +#: src/pyams_utils/encoding.py:93 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)" +msgstr "Bulgare, Biélorusse, Macédonien, Russe, Serbe (iso8859-5)" + +#: src/pyams_utils/encoding.py:94 +msgid "Arabic (iso8859_6)" +msgstr "Arabe (iso8859-6)" + +#: src/pyams_utils/encoding.py:95 +msgid "Greek (iso8859_7)" +msgstr "Grec (iso8869-7)" + +#: src/pyams_utils/encoding.py:96 +msgid "Hebrew (iso8859_8)" +msgstr "Hébreu (iso8859-8)" + +#: src/pyams_utils/encoding.py:97 +msgid "Turkish (iso8859_9)" +msgstr "Turc (iso8859-9)" + +#: src/pyams_utils/encoding.py:98 +msgid "Nordic languages (iso8859_10)" +msgstr "Langues nordiques (iso8859-10)" + +#: src/pyams_utils/encoding.py:99 +msgid "Baltic languages (iso8859_13)" +msgstr "Langues baltes (iso8859-13)" + +#: src/pyams_utils/encoding.py:100 +msgid "Celtic languages (iso8859_14)" +msgstr "Langues celtes (iso8859-14)" + +#: src/pyams_utils/encoding.py:101 +msgid "Western Europe (iso8859_15)" +msgstr "Europe de l'ouest (iso8859-15)" + +#: src/pyams_utils/encoding.py:102 +msgid "South-Eastern Europe (iso8859_16)" +msgstr "Europe du sud-est (iso8859-16)" + +#: src/pyams_utils/encoding.py:103 +msgid "Korean (johab)" +msgstr "Coréen (johab)" + +#: src/pyams_utils/encoding.py:104 +msgid "Russian (koi8_r)" +msgstr "Russe (kio8-r)" + +#: src/pyams_utils/encoding.py:105 +msgid "Ukrainian (koi8_u)" +msgstr "Ukrainien (kio8-u)" + +#: src/pyams_utils/encoding.py:106 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)" +msgstr "Bulgare, Biolorusse, Macédonien, Russe, Serve (mac-cyrillic)" + +#: src/pyams_utils/encoding.py:107 +msgid "Greek (mac_greek)" +msgstr "Grec (mac-greek)" + +#: src/pyams_utils/encoding.py:108 +msgid "Icelandic (mac_iceland)" +msgstr "Islandais (mac-iceland)" + +#: src/pyams_utils/encoding.py:109 +msgid "Central and Eastern Europe (mac_latin2)" +msgstr "Europe centrale et de l'ouest (mac-latin2)" + +#: src/pyams_utils/encoding.py:110 +msgid "Western Europe (mac_roman)" +msgstr "Europe de l'Ouest (mac-roman)" + +#: src/pyams_utils/encoding.py:111 +msgid "Turkish (mac_turkish)" +msgstr "Turc (mac-turkish)" + +#: src/pyams_utils/encoding.py:112 +msgid "Kazakh (ptcp154)" +msgstr "Kazak (ptcp154)" + +#: src/pyams_utils/encoding.py:113 +msgid "Japanese (shift_jis)" +msgstr "Japonais (shift_jis)" + +#: src/pyams_utils/encoding.py:114 +msgid "Japanese (shift_jis_2004)" +msgstr "Japonais (shift-jis-2004)" + +#: src/pyams_utils/encoding.py:115 +msgid "Japanese (shift_jisx0213)" +msgstr "Japonais (shift-jisx0213)" + +#: src/pyams_utils/encoding.py:116 +msgid "all languages (utf_32)" +msgstr "toutes les langues (utf-32)" + +#: src/pyams_utils/encoding.py:117 +msgid "all languages (utf_32_be)" +msgstr "toutes les langues (utf-32-be)" + +#: src/pyams_utils/encoding.py:118 +msgid "all languages (utf_32_le)" +msgstr "toutes les langues (utf-32-le)" + +#: src/pyams_utils/encoding.py:119 +msgid "all languages (utf_16)" +msgstr "toutes les langues (utf-16)" + +#: src/pyams_utils/encoding.py:120 +msgid "all languages (BMP only - utf_16_be)" +msgstr "toutes les langues (BMP seulement - utf-16-be" + +#: src/pyams_utils/encoding.py:121 +msgid "all languages (BMP only - utf_16_le)" +msgstr "toutes les langues (BMP seulement - utf-16-le)" + +#: src/pyams_utils/encoding.py:122 +msgid "all languages (utf_7)" +msgstr "toutes les langues (utf-7)" + +#: src/pyams_utils/encoding.py:123 +msgid "all languages (utf_8)" +msgstr "toutes les langues (utf-8)" + +#: src/pyams_utils/encoding.py:124 +msgid "all languages (utf_8_sig)" +msgstr "toutes les langues (utf-8-sig)" + +#: src/pyams_utils/date.py:75 +msgid "%d/%m/%Y" +msgstr "%d/%m/%Y" + +#: src/pyams_utils/date.py:76 +msgid "%d/%m/%Y - %H:%M" +msgstr "%d/%m/%Y - %H:%M" + +#: src/pyams_utils/date.py:78 +msgid "on %d/%m/%Y" +msgstr "le %d/%m/%Y" + +#: src/pyams_utils/date.py:79 +msgid "on %d/%m/%Y at %H:%M" +msgstr "le %d/%m/%Y à %H:%M" + +#: src/pyams_utils/date.py:106 +#, c-format +msgid "%d months ago" +msgstr "Il y a %d mois" + +#: src/pyams_utils/date.py:135 +#, c-format +msgid "%d months" +msgstr "%d mois" + +#: src/pyams_utils/date.py:108 +#, c-format +msgid "%d weeks ago" +msgstr "Il y a %d semaines" + +#: src/pyams_utils/date.py:137 +#, c-format +msgid "%d weeks" +msgstr "%d semaines" + +#: src/pyams_utils/date.py:110 +#, c-format +msgid "%d days ago" +msgstr "Il y a %d jours" + +#: src/pyams_utils/date.py:112 +msgid "the day before yesterday" +msgstr "avant-hier" + +#: src/pyams_utils/date.py:139 +#, c-format +msgid "%d days" +msgstr "%d jours" + +#: src/pyams_utils/date.py:114 +msgid "yesterday" +msgstr "hier" + +#: src/pyams_utils/date.py:143 +#, c-format +msgid "%d day and %d hours" +msgstr "%d jours et %d heures" + +#: src/pyams_utils/date.py:146 +#, c-format +msgid "%d hours" +msgstr "%d heures" + +#: src/pyams_utils/date.py:118 +#, c-format +msgid "%d hours ago" +msgstr "Il y a %d heures" + +#: src/pyams_utils/date.py:122 +msgid "less than 5 minutes ago" +msgstr "Il y a moins de 5 minutes" + +#: src/pyams_utils/date.py:150 +#, c-format +msgid "%d minutes" +msgstr "%d minutes" + +#: src/pyams_utils/date.py:152 +#, c-format +msgid "%d seconds" +msgstr "%d secondes" + +#: src/pyams_utils/date.py:120 +#, c-format +msgid "%d minutes ago" +msgstr "Il y a %d minutes" + +#: src/pyams_utils/size.py:61 +msgid "0.0## Gb" +msgstr "0.0## Go" + +#: src/pyams_utils/size.py:53 +msgid "0 bytes" +msgstr "0 octets" + +#: src/pyams_utils/size.py:56 +msgid "0.# Kb" +msgstr "0.# Ko" + +#: src/pyams_utils/size.py:59 +msgid "0.0# Mb" +msgstr "0.0# Mo" + +#: src/pyams_utils/schema.py:48 +msgid "Color length must be 3 or 6 characters" +msgstr "La longueur d'une couleur doit être de 3 ou 6 caractères" + +#: src/pyams_utils/schema.py:51 +msgid "" +"Color value must contain only valid hexadecimal color codes (numbers or " +"letters between 'A' end 'F')" +msgstr "" +"Une couleur ne doit contenir que des valeurs hexadécimales correctes (nombres " +"ou lettres de 'A' à 'F')" + +#: src/pyams_utils/timezone/interfaces.py:32 +msgid "Server timezone" +msgstr "Fuseau horaire du serveur" + +#: src/pyams_utils/timezone/interfaces.py:33 +msgid "Default server timezone" +msgstr "Fuseau horaire par défaut" + +#: src/pyams_utils/interfaces/text.py:36 +msgid "Renderer name" +msgstr "Nom de l'outil de rendu" + +#: src/pyams_utils/browser/decimal.py:35 +msgid "The entered value is not a valid decimal literal." +msgstr "La valeur saisie n'est pas une valeur décimale correcte." + +#~ msgid "Test" +#~ msgstr "test" diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,516 @@ +# +# SOME DESCRIPTIVE TITLE +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2015. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2015-01-24 16:53+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Lingua 3.8\n" + +#: ./src/pyams_utils/encoding.py:33 +msgid "English (ASCII)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:34 +msgid "Traditional Chinese (big5)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:35 +msgid "Traditional Chinese (big5hkscs)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:36 +msgid "English (cp037)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:37 +msgid "Hebrew (cp424)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:38 +msgid "English (cp437)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:39 +msgid "Western Europe (cp500)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:40 +msgid "Arabic (cp720)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:41 +msgid "Greek (cp737)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:42 +msgid "Baltic languages (cp775)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:43 +msgid "Western Europe (cp850)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:44 +msgid "Central and Eastern Europe (cp852)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:45 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp855)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:46 +msgid "Hebrew (cp856)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:47 +msgid "Turkish (cp857)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:48 +msgid "Western Europe (cp858)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:49 +msgid "Portuguese (cp860)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:50 +msgid "Icelandic (cp861)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:51 +msgid "Hebrew (cp862)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:52 +msgid "Canadian (cp863)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:53 +msgid "Arabic (cp864)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:54 +msgid "Danish, Norwegian (cp865)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:55 +msgid "Russian (cp866)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:56 +msgid "Greek (cp869)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:57 +msgid "Thai (cp874)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:58 +msgid "Greek (cp875)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:59 +msgid "Japanese (cp932)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:60 +msgid "Korean (cp949)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:61 +msgid "Traditional Chinese (cp950)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:62 +msgid "Urdu (cp1006)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:63 +msgid "Turkish (cp1026)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:64 +msgid "Western Europe (cp1140)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:65 +msgid "Central and Eastern Europe (cp1250)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:66 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (cp1251)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:67 +msgid "Western Europe (cp1252)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:68 +msgid "Greek (cp1253)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:69 +msgid "Turkish (cp1254)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:70 +msgid "Hebrew (cp1255)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:71 +msgid "Arabic (cp1256)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:72 +msgid "Baltic languages (cp1257)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:73 +msgid "Vietnamese (cp1258)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:74 +msgid "Japanese (euc_jp)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:75 +msgid "Japanese (euc_jis_2004)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:76 +msgid "Japanese (euc_jisx0213)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:77 +msgid "Korean (euc_kr)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:78 +msgid "Simplified Chinese (gb2312)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:79 +msgid "Unified Chinese (gbk)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:80 +msgid "Unified Chinese (gb18030)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:81 +msgid "Simplified Chinese (hz)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:82 +msgid "Japanese (iso2022_jp)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:83 +msgid "Japanese (iso2022_jp_1)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:84 +msgid "" +"Japanese, Korean, Simplified Chinese, Western Europe, Greek (iso2022_jp_2)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:85 +msgid "Japanese (iso2022_jp_2004)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:86 +msgid "Japanese (iso2022_jp_3)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:87 +msgid "Japanese (iso2022_jp_ext)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:88 +msgid "Korean (iso2022_kr)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:89 +msgid "West Europe (latin_1)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:90 +msgid "Central and Eastern Europe (iso8859_2)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:91 +msgid "Esperanto, Maltese (iso8859_3)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:92 +msgid "Baltic languages (iso8859_4)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:93 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (iso8859_5)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:94 +msgid "Arabic (iso8859_6)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:95 +msgid "Greek (iso8859_7)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:96 +msgid "Hebrew (iso8859_8)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:97 +msgid "Turkish (iso8859_9)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:98 +msgid "Nordic languages (iso8859_10)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:99 +msgid "Baltic languages (iso8859_13)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:100 +msgid "Celtic languages (iso8859_14)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:101 +msgid "Western Europe (iso8859_15)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:102 +msgid "South-Eastern Europe (iso8859_16)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:103 +msgid "Korean (johab)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:104 +msgid "Russian (koi8_r)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:105 +msgid "Ukrainian (koi8_u)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:106 +msgid "Bulgarian, Byelorussian, Macedonian, Russian, Serbian (mac_cyrillic)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:107 +msgid "Greek (mac_greek)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:108 +msgid "Icelandic (mac_iceland)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:109 +msgid "Central and Eastern Europe (mac_latin2)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:110 +msgid "Western Europe (mac_roman)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:111 +msgid "Turkish (mac_turkish)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:112 +msgid "Kazakh (ptcp154)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:113 +msgid "Japanese (shift_jis)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:114 +msgid "Japanese (shift_jis_2004)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:115 +msgid "Japanese (shift_jisx0213)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:116 +msgid "all languages (utf_32)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:117 +msgid "all languages (utf_32_be)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:118 +msgid "all languages (utf_32_le)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:119 +msgid "all languages (utf_16)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:120 +msgid "all languages (BMP only - utf_16_be)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:121 +msgid "all languages (BMP only - utf_16_le)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:122 +msgid "all languages (utf_7)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:123 +msgid "all languages (utf_8)" +msgstr "" + +#: ./src/pyams_utils/encoding.py:124 +msgid "all languages (utf_8_sig)" +msgstr "" + +#: ./src/pyams_utils/date.py:75 +msgid "%d/%m/%Y" +msgstr "" + +#: ./src/pyams_utils/date.py:76 +msgid "%d/%m/%Y - %H:%M" +msgstr "" + +#: ./src/pyams_utils/date.py:78 +msgid "on %d/%m/%Y" +msgstr "" + +#: ./src/pyams_utils/date.py:79 +msgid "on %d/%m/%Y at %H:%M" +msgstr "" + +#: ./src/pyams_utils/date.py:106 +#, c-format +msgid "%d months ago" +msgstr "" + +#: ./src/pyams_utils/date.py:135 +#, c-format +msgid "%d months" +msgstr "" + +#: ./src/pyams_utils/date.py:108 +#, c-format +msgid "%d weeks ago" +msgstr "" + +#: ./src/pyams_utils/date.py:137 +#, c-format +msgid "%d weeks" +msgstr "" + +#: ./src/pyams_utils/date.py:110 +#, c-format +msgid "%d days ago" +msgstr "" + +#: ./src/pyams_utils/date.py:112 +msgid "the day before yesterday" +msgstr "" + +#: ./src/pyams_utils/date.py:139 +#, c-format +msgid "%d days" +msgstr "" + +#: ./src/pyams_utils/date.py:114 +msgid "yesterday" +msgstr "" + +#: ./src/pyams_utils/date.py:143 +#, c-format +msgid "%d day and %d hours" +msgstr "" + +#: ./src/pyams_utils/date.py:146 +#, c-format +msgid "%d hours" +msgstr "" + +#: ./src/pyams_utils/date.py:118 +#, c-format +msgid "%d hours ago" +msgstr "" + +#: ./src/pyams_utils/date.py:122 +msgid "less than 5 minutes ago" +msgstr "" + +#: ./src/pyams_utils/date.py:150 +#, c-format +msgid "%d minutes" +msgstr "" + +#: ./src/pyams_utils/date.py:152 +#, c-format +msgid "%d seconds" +msgstr "" + +#: ./src/pyams_utils/date.py:120 +#, c-format +msgid "%d minutes ago" +msgstr "" + +#: ./src/pyams_utils/size.py:61 +msgid "0.0## Gb" +msgstr "" + +#: ./src/pyams_utils/size.py:53 +msgid "0 bytes" +msgstr "" + +#: ./src/pyams_utils/size.py:56 +msgid "0.# Kb" +msgstr "" + +#: ./src/pyams_utils/size.py:59 +msgid "0.0# Mb" +msgstr "" + +#: ./src/pyams_utils/schema.py:48 +msgid "Color length must be 3 or 6 characters" +msgstr "" + +#: ./src/pyams_utils/schema.py:51 +msgid "" +"Color value must contain only valid hexadecimal color codes (numbers or " +"letters between 'A' end 'F')" +msgstr "" + +#: ./src/pyams_utils/timezone/interfaces.py:32 +msgid "Server timezone" +msgstr "" + +#: ./src/pyams_utils/timezone/interfaces.py:33 +msgid "Default server timezone" +msgstr "" + +#: ./src/pyams_utils/interfaces/text.py:36 +msgid "Renderer name" +msgstr "" + +#: ./src/pyams_utils/browser/decimal.py:35 +msgid "The entered value is not a valid decimal literal." +msgstr "" diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/property.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/property.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,105 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages +from pyams_utils.request import check_request, get_request_data, set_request_data +from pyams_utils.session import get_session_data, set_session_data + + +class cached(object): + """Custom property decorator to define a property or function + which is calculated only once + + When applied on a function, caching is based on input arguments + """ + + def __init__(self, function): + self._function = function + self._cache = {} + + def __call__(self, *args): + try: + return self._cache[args] + except KeyError: + self._cache[args] = self._function(*args) + return self._cache[args] + + def expire(self, *args): + del self._cache[args] + + +class cached_property(object): + """A read-only @property decorator that is only evaluated once. The value is cached + on the object itself rather than the function or class; this should prevent + memory leakage. + """ + def __init__(self, fget, doc=None): + self.fget = fget + self.__doc__ = doc or fget.__doc__ + self.__name__ = fget.__name__ + self.__module__ = fget.__module__ + + def __get__(self, obj, cls): + if obj is None: + return self + obj.__dict__[self.__name__] = result = self.fget(obj) + return result + + +_marker = object() + +class request_property(object): + """Define a property stored in request annotations""" + + def __init__(self, fget, key=None): + self.fget = fget + if key is None: + key = "%s.%s" % (fget.__module__, fget.__name__) + self.key = key + + def __get__(self, obj, cls): + if obj is None: + return self + request = check_request() + data = get_request_data(request, self.key, _marker) + if data is _marker: + data = self.fget(obj) + set_request_data(request, self.key, data) + return data + + +class session_property(object): + """Define a property stored into session""" + + def __init__(self, fget, app, key=None): + self.fget = fget + self.app = app + if key is None: + key = "%s.%s" % (fget.__module__, fget.__name__) + self.key = key + + def __get__(self, obj, cls): + if obj is None: + return self + request = check_request() + data = get_session_data(request, self.app, self.key, _marker) + if data is _marker: + data = self.fget(obj) + set_session_data(request, self.app, self.key, data) + return data diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/protocol/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/protocol/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ +# diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/protocol/http.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/protocol/http.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,79 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import httplib2 +import urllib.parse + +# import interfaces + +# import packages + + +class HTTPClient(object): + """HTTP client""" + + def __init__(self, method, protocol, servername, url, params={}, credentials=(), + proxy=(), rdns=True, proxy_auth=(), timeout=None, headers={}): + """Intialize HTTP connection""" + self.connection = None + self.method = method + self.protocol = protocol + self.servername = servername + self.url = url + self.params = params + self.location = None + self.credentials = credentials + self.proxy = proxy + self.rdns = rdns + self.proxy_auth = proxy_auth + self.timeout = timeout + self.headers = headers + if 'User-Agent' not in headers: + self.headers['User-Agent'] = 'PyAMS HTTP Client/1.0' + + def get_response(self): + """Common HTTP request""" + if self.proxy: + proxy_info = httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP, + proxy_host=self.proxy[0], + proxy_port=self.proxy[1], + proxy_rdns=self.rdns, + proxy_user=self.proxy_auth and self.proxy_auth[0] or None, + proxy_pass=self.proxy_auth and self.proxy_auth[1] or None) + else: + proxy_info = None + http = httplib2.Http(timeout=self.timeout, proxy_info=proxy_info) + if self.credentials: + http.add_credentials(self.credentials[0], self.credentials[1]) + uri = '%s://%s%s' % (self.protocol, self.servername, self.url) + if self.params: + uri += '?' + urllib.parse.urlencode(self.params) + response, content = http.request(uri, self.method, headers=self.headers) + return response, content + + +def get_client(method, protocol, servername, url, params={}, credentials=(), proxy=(), + rdns=True, proxy_auth=(), timeout=None, headers={}): + """HTTP client factory""" + return HTTPClient(method, protocol, servername, url, params, credentials, proxy, + rdns, proxy_auth, timeout, headers) + + +def get_client_from_url(url, credentials=(), proxy=(), rdns=True, proxy_auth=(), timeout=None, headers={}): + """HTTP client factory from URL""" + elements = urllib.parse.urlparse(url) + return HTTPClient('GET', elements.scheme, elements.netloc, elements.path, elements.params, + credentials, proxy, rdns, proxy_auth, timeout, headers) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/protocol/xmlrpc.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/protocol/xmlrpc.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,144 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import base64 +import http.client +import http.cookiejar +import socket +import urllib.request +import xmlrpc.client + +# import interfaces + +# import packages + + +class XMLRPCCookieAuthTransport(xmlrpc.client.Transport): + """An XML-RPC transport handling authentication via cookies""" + + _http_connection = http.client.HTTPConnection + + def __init__(self, user_agent, credentials=(), cookies=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, headers=None): + xmlrpc.client.Transport.__init__(self) + self.user_agent = user_agent + self.credentials = credentials + self.cookies = cookies + self.timeout = timeout + self.headers = headers + + def make_connection(self, host): + # This is the make_connection that runs under Python 2.7 and newer. + # The code is pulled straight from 2.7 xmlrpclib, except replacing + # HTTPConnection with self._http_connection + if self._connection and host == self._connection[0]: + return self._connection[1] + chost, self._extra_headers, _x509 = self.get_host_info(host) + self._connection = host, self._http_connection(chost, timeout=self.timeout) + return self._connection[1] + + # override the send_host hook to also send authentication info + def send_host(self, connection, host): + connection.putheader('Host', host) + if (self.cookies is not None) and (len(self.cookies) > 0): + for cookie in self.cookies: + connection.putheader('Cookie', '%s=%s' % (cookie.name, cookie.value)) + elif self.credentials: + auth = 'Basic %s' % base64.encodebytes("%s:%s" % self.credentials).strip() + connection.putheader('Authorization', auth) + + # send user agent + def send_user_agent(self, connection): + connection.putheader('User-Agent', self.user_agent) + + # send custom headers + def send_headers(self, connection, headers): + xmlrpc.client.Transport.send_headers(self, connection, headers) + for k, v in (self.headers or {}).iteritems(): + connection.putheader(k, v) + + # dummy request class for extracting cookies + class CookieRequest(urllib.request.Request): + pass + + # dummy response info headers helper + class CookieResponseHelper: + def __init__(self, response): + self.response = response + def getheaders(self, header): + return self.response.msg.getallmatchingheaders(header) + + # dummy response class for extracting cookies + class CookieResponse: + def __init__(self, response): + self.response = response + def info(self): + return XMLRPCCookieAuthTransport.CookieResponseHelper(self.response) + + def request(self, host, handler, request_body, verbose=False): + # issue XML-RPC request + connection = self.make_connection(host) + self.verbose = verbose + if verbose: + connection.set_debuglevel(1) + self.send_request(connection, handler, request_body) + self.send_host(connection, host) + self.send_user_agent(connection) + self.send_headers(connection) + self.send_content(connection, request_body) + # get response + return self.get_response(connection, host, handler) + + def get_response(self, connection, host, handler): + response = connection.getresponse() + # extract cookies from response headers + if self.cookies is not None: + crequest = XMLRPCCookieAuthTransport.CookieRequest('http://%s/' % host) + cresponse = XMLRPCCookieAuthTransport.CookieResponse(response) + self.cookies.extract_cookies(cresponse, crequest) + if response.status != 200: + raise xmlrpc.client.ProtocolError(host + handler, response.status, response.reason, response.getheaders()) + return self.parse_response(response) + + +class SecureXMLRPCCookieAuthTransport(XMLRPCCookieAuthTransport): + """Secure XML-RPC transport""" + + _http_connection = http.client.HTTPSConnection + + +def get_client(uri, credentials=(), verbose=False, allow_none=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, headers=None): + """Get an XML-RPC client which supports basic authentication""" + if uri.startswith('https:'): + transport = SecureXMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS secure transport)', credentials, + timeout=timeout, headers=headers) + else: + transport = XMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS basic transport)', credentials, + timeout=timeout, headers=headers) + return xmlrpc.client.Server(uri, transport=transport, verbose=verbose, allow_none=allow_none) + + +def get_client_with_cookies(uri, credentials=(), verbose=False, allow_none=0, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + headers=None, cookies=None): + """Get an XML-RPC client which supports authentication through cookies""" + if cookies is None: + cookies = http.cookiejar.CookieJar() + if uri.startswith('https:'): + transport = SecureXMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS secure cookie transport)', + credentials, cookies, timeout, headers) + else: + transport = XMLRPCCookieAuthTransport('Python XML-RPC Client/0.1 (PyAMS basic cookie transport)', + credentials, cookies, timeout, headers) + return xmlrpc.client.Server(uri, transport=transport, verbose=verbose, allow_none=allow_none) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/registry.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/registry.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,123 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import venusian + +# import interfaces +from zope.component.interfaces import ComponentLookupError + +# import packages +from pyramid.threadlocal import get_current_registry, get_current_request +from zope.interface import implementedBy, providedBy + + +def _get_registries(): + """Get list of component registries""" + registry = get_current_registry() + yield registry + request = get_current_request() + if (request is not None) and (request.registry != registry): + yield request.registry + + +def registered_utilities(): + """Get utilities registrations as generator""" + for registry in _get_registries(): + for utility in registry.registeredUtilities(): + yield utility + + +def query_utility(provided, name='', default=None): + """Query utility registered with given interface""" + for registry in _get_registries(): + utility = registry.queryUtility(provided, name, default) + if utility is not None: + return utility + return default + + +def get_utility(provided, name=''): + """Get utility registered with given interface""" + for registry in _get_registries(): + utility = registry.queryUtility(provided, name) + if utility is not None: + return utility + raise ComponentLookupError(provided, name) + + +def get_utilities_for(interface): + """Get utilities registered with given interface as (name, util)""" + for registry in _get_registries(): + for utility in registry.getUtilitiesFor(interface): + yield utility + + +def get_all_utilities_registered_for(interface): + """Get list of registered utilities for given interface""" + result = [] + for registry in _get_registries(): + for utilities in registry.getAllUtilitiesRegisteredFor(interface): + result.extend(utilities) + return result + + +class utility_config(object): + """Function or class decorator to declare a utility""" + + venusian = venusian + + def __init__(self, **settings): + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + depth = settings.pop('_depth', 0) + + def callback(context, name, ob): + if type(ob) is type: + factory = ob + component = None + else: + factory = None + component = ob + + provides = settings.get('provides') + if provides is None: + if factory: + provides = list(implementedBy(factory)) + else: + provides = list(providedBy(component)) + if len(provides) == 1: + provides = provides[0] + else: + raise TypeError("Missing 'provides' argument") + + config = context.config.with_package(info.module) + config.registry.registerUtility(component=component, factory=factory, + provided=provides, name=settings.get('name', '')) + + info = self.venusian.attach(wrapped, callback, category='pyams_utility', + depth=depth + 1) + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/request.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/request.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,80 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations +from zope.security.interfaces import NoInteraction + +# import packages +from pyramid.request import Request +from pyramid.threadlocal import get_current_request +from zope.interface import alsoProvides + + +def get_request(raise_exception=True): + """Get current request + + Raises a NoInteraction exception if there is no active request""" + request = get_current_request() + if (request is None) and raise_exception: + raise NoInteraction("No request") + return request + + +def query_request(): + """Query current request + + Returns None if there is no active request""" + try: + return get_request() + except NoInteraction: + return None + + +def check_request(path='/', environ=None, base_url=None, headers=None, POST=None, **kw): + """Get current request, or create a new blank one if missing""" + try: + return get_request() + except NoInteraction: + return Request.blank(path, environ, base_url, headers, POST, **kw) + + +def get_annotations(request): + """Define 'annotations' request property""" + alsoProvides(request, IAttributeAnnotatable) + return IAnnotations(request) + + +def get_debug(request): + """Define 'debug' request property""" + class Debug(): + def __init__(self): + self.showTAL = False + self.sourceAnnotations = False + return Debug() + + +def get_request_data(request, key, default=None): + """Get data associated with request""" + annotations = request.annotations + return annotations.get(key, default) + + +def set_request_data(request, key, value): + """Associate data with request""" + annotations = request.annotations + annotations[key] = value diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/schema.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/schema.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,120 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import string + +# import interfaces +from zope.schema.interfaces import ITextLine, IDecimal, IList, ITuple, IPassword + +# import Zope3 packages +from zope.interface import implementer +from zope.schema import TextLine, Decimal, List, Tuple, Password, ValidationError + +# import local packages + +from pyams_utils import _ + + +# +# Encoded password field +# + +class IEncodedPassword(IPassword): + """Encoded password field interface""" + + +@implementer(IEncodedPassword) +class EncodedPassword(Password): + """Encoded password field""" + + _type = None + + def fromUnicode(self, str): + return str + + def constraint(self, value): + return True + + +# +# Color field +# + +class IColorField(ITextLine): + """Marker interface for color fields""" + + +@implementer(IColorField) +class ColorField(TextLine): + """Color field""" + + def __init__(self, *args, **kw): + super(ColorField, self).__init__(max_length=6, *args, **kw) + + def _validate(self, value): + if len(value) not in (3, 6): + raise ValidationError(_("Color length must be 3 or 6 characters")) + for v in value: + if v not in string.hexdigits: + raise ValidationError(_("Color value must contain only valid hexadecimal color codes (numbers or " + "letters between 'A' end 'F')")) + super(ColorField, self)._validate(value) + + +# +# Pointed decimal field +# + +class IDottedDecimalField(IDecimal): + """Marker interface for dotted decimal fields""" + + +@implementer(IDottedDecimalField) +class DottedDecimalField(Decimal): + """Dotted decimal field""" + + +# +# Dates range field +# + +class IDatesRangeField(ITuple): + """Marker interface for dates range fields""" + + +@implementer(IDatesRangeField) +class DatesRangeField(Tuple): + """Dates range field""" + + def __init__(self, value_type=None, unique=False, **kw): + super(DatesRangeField, self).__init__(value_type=None, unique=False, + min_length=2, max_length=2, **kw) + + +# +# TextLine list field +# + +class ITextLineListField(IList): + """Marker interface for textline list field""" + + +@implementer(ITextLineListField) +class TextLineListField(List): + """TextLine list field""" + + def __init__(self, value_type=None, unique=False, **kw): + super(TextLineListField, self).__init__(value_type=TextLine(), unique=True, **kw) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/scripts/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/scripts/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ +# diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/scripts/zodb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/scripts/zodb.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,46 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import optparse +import sys +import textwrap + +# import interfaces + +# import packages +from pyams_utils.site import site_upgrade +from pyramid.paster import bootstrap + + +def upgrade_site(): + """Check for site upgrade""" + usage = "usage: %prog config_uri" + description = """Check for database upgrade. + Usage: pyams_upgrade production.ini + """ + parser = optparse.OptionParser(usage=usage, + description=textwrap.dedent(description)) + options, args = parser.parse_args(sys.argv[1:]) + if not len(args) >= 1: + print("You must provide at least one configuration file") + return 2 + config_uri = args[0] + env = bootstrap(config_uri) + settings, closer = env['registry'].settings, env['closer'] + try: + site_upgrade(env['request']) + finally: + closer() diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/session.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/session.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,32 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages + + +def get_session_data(request, app, key, default=None): + """Get data associated with a given session""" + session = request.session + return session.get('{0}::{1}'.format(app, key), default) + + +def set_session_data(request, app, key, value): + """Set data associated to a given session""" + session = request.session + session['{0}::{1}'.format(app, key)] = value diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/site.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/site.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,167 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_utils.interfaces import PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME, \ + PYAMS_APPLICATION_FACTORY_KEY +from pyams_utils.interfaces.site import ISiteRoot, INewLocalSiteCreatedEvent, ISiteUpgradeEvent, ISiteGenerations, \ + SITE_GENERATIONS_KEY, IConfigurationManager +from zope.annotation.interfaces import IAnnotations +from zope.component.interfaces import IPossibleSite, ISite, ObjectEvent +from zope.traversing.interfaces import IBeforeTraverseEvent, ITraversable + +# import packages +from persistent.dict import PersistentDict +from pyams_utils.adapter import adapter_config +from pyams_utils.registry import get_utilities_for, query_utility +from pyramid.events import subscriber +from pyramid.exceptions import NotFound +from pyramid.path import DottedNameResolver +from pyramid.security import Allow, ALL_PERMISSIONS +from pyramid.threadlocal import manager, get_current_registry +from pyramid_zodbconn import get_connection +from zope.container.folder import Folder +from zope.interface import implementer +from zope.lifecycleevent import ObjectCreatedEvent +from zope.site import hooks +from zope.site.site import LocalSiteManager, SiteManagerContainer + + +@implementer(ISiteRoot, IConfigurationManager) +class BaseSiteRoot(Folder, SiteManagerContainer): + """Default site root""" + + __acl__ = [(Allow, 'system:admin', ALL_PERMISSIONS)] + + config_klass = None + + +@adapter_config(name='etc', context=ISiteRoot, provides=ITraversable) +class SiteRootEtcTraverser(object): + """Site root ++etc++ namespace traverser""" + + def __init__(self, context): + self.context = context + + def traverse(self, name, furtherpath=None): + if name == 'site': + return self.context.getSiteManager() + raise NotFound + + +@implementer(INewLocalSiteCreatedEvent) +class NewLocalSiteCreatedEvent(ObjectEvent): + """New local site created event""" + + +def site_factory(request): + """Build a new site including registered utilities""" + conn = get_connection(request) + root = conn.root() + application_key = request.registry.settings.get(PYAMS_APPLICATION_SETTINGS_KEY, + PYAMS_APPLICATION_DEFAULT_NAME) + application = root.get(application_key) + if application is None: + factory = request.registry.settings.get(PYAMS_APPLICATION_FACTORY_KEY) + if factory: + resolver = DottedNameResolver() + factory = resolver.maybe_resolve(factory) + else: + factory = BaseSiteRoot + application = root[application_key] = factory() + if IPossibleSite.providedBy(application): + sm = LocalSiteManager(application, default_folder=False) + application.setSiteManager(sm) + try: + # if some components require a valid and complete registry + # with all registered utilities, they can subscribe to + # INewLocalSiteCreatedEvent event interface + hooks.setSite(application) + get_current_registry().notify(NewLocalSiteCreatedEvent(application)) + finally: + hooks.setSite(None) + import transaction + transaction.commit() + return application + + +@implementer(ISiteUpgradeEvent) +class SiteUpgradeEvent(ObjectEvent): + """Site upgrade request event""" + + +def site_upgrade(request): + """Upgrade site when needed + + This function is executed by pyams_upgrade console script. + Site generations are registered as named utilities providing + ISiteGenerations interface. + Current site generations are stored into annotations. + """ + application = site_factory(request) + if application is not None: + try: + hooks.setSite(application) + annotations = IAnnotations(application) + generations = annotations.get(SITE_GENERATIONS_KEY) + if generations is None: + generations = annotations[SITE_GENERATIONS_KEY] = PersistentDict() + for name, utility in get_utilities_for(ISiteGenerations): + if not name: + name = '.'.join((utility.__module__, utility.__class__.__name__)) + current = generations.get(name) + if (not current) or (current < utility.generation): + print("Upgrading {0} from generation {1} to {2}...".format(name, current, utility.generation)) + utility.evolve(application, current) + generations[name] = utility.generation + finally: + hooks.setSite(None) + import transaction + transaction.commit() + return application + + +@subscriber(IBeforeTraverseEvent, context_selector=ISite) +def handle_site_before_traverse(event): + """Push registry and request to threadlocal manager when an + object implementing ISite is traversed + """ + manager.push({'registry': event.object.getSiteManager(), + 'request': event.request}) + hooks.setSite(event.object) + + +def check_required_utilities(site, utilities): + """Utility function to check for required utilities + + utilities argument is a tuple made of: + - the utility interface + - the utility name + - the utility factory + - the default name when creating the utility + """ + registry = get_current_registry() + for interface, name, factory, default_id in utilities: + utility = query_utility(interface, name=name) + if utility is None: + sm = site.getSiteManager() + if default_id in sm: + continue + utility = factory() + registry.notify(ObjectCreatedEvent(utility)) + sm[default_id] = utility + sm.registerUtility(utility, interface, name=name) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/size.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/size.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,59 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces + +# import packages +from babel.core import Locale +from babel.numbers import format_decimal +from pyams_utils.request import check_request + +from pyams_utils import _ + + +def get_human_size(value, request=None): + """Convert given bytes value in human readable format + + >>> from pyramid.testing import DummyRequest + >>> request = DummyRequest(params={'_LOCALE_': 'fr'}) + >>> request.locale_name + 'fr' + + >>> from pyams_utils.size import get_human_size + >>> get_human_size(256, request) + '256 bytes' + >>> get_human_size(3678, request) + '3,6 Kb' + >>> get_human_size(6785342, request) + '6,47 Mb' + >>> get_human_size(3674815342, request) + '3,422 Gb' + """ + if request is None: + request = check_request() + translate = request.localizer.translate + locale = Locale(request.locale_name) + if value < 1024: + return format_decimal(value, translate(_('0 bytes')), locale) + value /= 1024 + if value < 1024: + return format_decimal(value, translate(_('0.# Kb')), locale) + value /= 1024 + if value < 1024: + return format_decimal(value, translate(_('0.0# Mb')), locale) + value /= 1024 + return format_decimal(value, translate(_('0.0## Gb')), locale) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/tales.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/tales.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,80 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import re + +# import interfaces +from pyams_utils.interfaces.tales import ITALESExtension + +# import packages +from chameleon.astutil import Symbol +from chameleon.codegen import template +from chameleon.tales import StringExpr +from zope.contentprovider.tales import addTALNamespaceData + + +class ContextExprMixin(object): + """Mixin-class for expression compilers.""" + + transform = None + + def __call__(self, target, engine): + # Make call to superclass to assign value to target + assignment = super(ContextExprMixin, self).__call__(target, engine) + transform = template("target = transform(econtext, target)", + target=target, + transform=self.transform) + return assignment + transform + + +FUNCTION_EXPRESSION = re.compile('(.+)\((.+)\)') + + +def render_extension(econtext, name): + name = name.strip() + + context = econtext.get('context') + request = econtext.get('request') + view = econtext.get('view') + + func_match = FUNCTION_EXPRESSION.match(name) + if func_match: + name, argument = func_match.groups() + arg_value = econtext.get(argument, argument) + else: + arg_value = None + + registry = request.registry + extension = registry.queryMultiAdapter((context, request, view), ITALESExtension, name=name) + if extension is None: + extension = registry.queryMultiAdapter((context, request), ITALESExtension, name=name) + if extension is None: + extension = registry.queryAdapter(context, ITALESExtension, name=name) + + # provide a useful error message, if the extension was not found. + if extension is None: + return None + + # Insert the data gotten from the context + addTALNamespaceData(extension, econtext) + + return extension.render(arg_value) + + +class ExtensionExpr(ContextExprMixin, StringExpr): + """extension: TALES expression""" + + transform = Symbol(render_extension) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/tests/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ + diff -r 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +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 16d47bd81d84 -r 3f89629b9e54 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 Thu Feb 19 00:46:48 2015 +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 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/text.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/text.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,140 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import html +import docutils.core + +# import interfaces +from pyams_utils.interfaces.tales import ITALESExtension +from pyams_utils.interfaces.text import IHTMLRenderer +from pyramid.interfaces import IRequest +from zope.schema.interfaces import IVocabularyFactory + +# import packages +from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config +from pyams_utils.request import check_request +from zope.component import adapter +from zope.interface import implementer, provider, Interface +from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry + + +def get_text_start(text, length, max=0): + """Get first words of given text with maximum given length + + If @max is specified, text is shortened only if remaining text is longer than @max + + @param text: initial text + @param length: maximum length of resulting text + @param max: if > 0, @text is shortened only if remaining text is longer than max + + >>> from pyams_utils.text import get_text_start + >>> get_text_start('This is a long string', 10) + 'This is a…' + >>> 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='text', context=(str, IRequest), provides=IHTMLRenderer) +class BaseHTMLRenderer(object): + """Raw text renderer utility class""" + + def __init__(self, context, request): + self.context = context + self.request = request + + def render(self, **kwargs): + return self.context + + +@adapter_config(name='text', context=(str, IRequest), provides=IHTMLRenderer) +class TextRenderer(BaseHTMLRenderer): + """Render raw text to HTML""" + + def render(self, **kwargs): + return html.escape(self.context).replace('\n', '
\n') + + +@adapter_config(name='rest', context=(str, IRequest), provides=IHTMLRenderer) +class ReStructuredTextRenderer(BaseHTMLRenderer): + """Render reStructuredText to HTML""" + + def render(self, **kwargs): + """Render reStructuredText to HTML""" + overrides = { + 'halt_level': 6, + 'input_encoding': 'unicode', + 'output_encoding': 'unicode', + 'initial_header_level': 3, + } + if 'settings' in kwargs: + overrides.update(kwargs['settings']) + parts = docutils.core.publish_parts(self.context, + writer_name='html', + settings_overrides=overrides) + return ''.join((parts['body_pre_docinfo'], parts['docinfo'], parts['body'])) + + +def text_to_html(text, renderer='text'): + """Convert text to HTML using the given renderer""" + request = check_request() + registry = request.registry + renderer = registry.queryMultiAdapter((text, request), IHTMLRenderer, name=renderer) + if renderer is not None: + return renderer.render() + + +@adapter_config(name='html', context=(Interface, Interface, Interface), provides=ITALESExtension) +class HTMLTalesExtension(ContextRequestViewAdapter): + """extension:html TALES expression""" + + def render(self, context): + if context is None: + context = self.context + renderer = self.request.registry.queryMultiAdapter((context, self.request, self.view), IHTMLRenderer) + if renderer is not None: + return renderer.render() + elif isinstance(context, str): + return text_to_html(context, 'text') + else: + return str(context) + + +@provider(IVocabularyFactory) +class RenderersVocabulary(SimpleVocabulary): + """Text renderers vocabulary""" + + def __init__(self): + request = check_request() + registry = request.registry + translate = registry.localizer.translate + terms = [SimpleTerm(name, name, translate(adapt.title).label) + for name, adapt in registry.getAdapters(('', request), IHTMLRenderer)] + super(RenderersVocabulary, self).__init__(terms) + +getVocabularyRegistry().register('PyAMS HTML renderers', RenderersVocabulary) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/timezone/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/timezone/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,68 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +from datetime import datetime + +import pytz + + +# import interfaces +from pyams_utils.interfaces.timezone import IServerTimezone +from pyramid.interfaces import IRequest +from zope.interface.common.idatetime import ITZInfo + +# import packages +from pyams_utils.adapter import adapter_config +from pyams_utils.registry import query_utility + + +GMT = pytz.timezone('GMT') +_tz = pytz.timezone('Europe/Paris') +tz = _tz + + +@adapter_config(context=IRequest, provides=ITZInfo) +def tzinfo(request=None): + """request to timezone adapter + + There is no easy way to get timezone from a request. + This adapter assumes that the timezone is given by + a registered utility... + """ + util = query_utility(IServerTimezone) + if util is not None: + return pytz.timezone(util.timezone) + return GMT + + +def tztime(value): + if not value: + return None + if not isinstance(value, datetime): + return value + if not value.tzinfo: + value = GMT.localize(value) + return value.astimezone(tzinfo()) + + +def gmtime(value): + if not value: + return None + if not isinstance(value, datetime): + return value + if not value.tzinfo: + value = GMT.localize(value) + return value.astimezone(GMT) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/timezone/utility.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/timezone/utility.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,57 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_utils.interfaces.site import ISiteGenerations +from pyams_utils.interfaces.timezone import IServerTimezone +from zope.site.interfaces import INewLocalSite + +# import packages +from persistent import Persistent +from pyams_utils.registry import utility_config +from pyams_utils.site import check_required_utilities +from pyramid.events import subscriber +from zope.container.contained import Contained +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty + + +@implementer(IServerTimezone) +class ServerTimezoneUtility(Persistent, Contained): + + timezone = FieldProperty(IServerTimezone['timezone']) + + +REQUIRED_UTILITIES = ((IServerTimezone, '', ServerTimezoneUtility, 'Server timezone'),) + + +@subscriber(INewLocalSite) +def handle_new_local_site(event): + """Create a new ServerTimezoneUtility when a site is created""" + site = event.manager.__parent__ + check_required_utilities(site, REQUIRED_UTILITIES) + + +@utility_config(name='PyAMS timezone checker', provides=ISiteGenerations) +class TimezoneGenerationsChecker(object): + """Timezone generations checker""" + + generation = 1 + + def evolve(self, site, current=None): + """Check for required utilities""" + check_required_utilities(site, REQUIRED_UTILITIES) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/timezone/vocabulary.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/timezone/vocabulary.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,35 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import pytz + +# import interfaces +from zope.schema.interfaces import IVocabularyFactory + +# import packages +from zope.interface import provider +from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary, getVocabularyRegistry + + +@provider(IVocabularyFactory) +class TimezonesVocabulary(SimpleVocabulary): + """Timezones vocabulary""" + + def __init__(self, *args, **kw): + terms = [SimpleTerm(t, t, t) for t in pytz.all_timezones] + super(TimezonesVocabulary, self).__init__(terms) + +getVocabularyRegistry().register('PyAMS timezones', TimezonesVocabulary) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/traversing.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/traversing.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,174 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyramid.interfaces import VH_ROOT_KEY +from zope.traversing.interfaces import ITraversable, BeforeTraverseEvent + +# import packages +from pyramid.compat import decode_path_info, is_nonstr_iter +from pyramid.exceptions import URLDecodeError, NotFound +from pyramid.threadlocal import get_current_registry +from pyramid.traversal import ResourceTreeTraverser, slash, split_path_info, empty +from zope.interface import Interface + + +class NamespaceTraverser(ResourceTreeTraverser): + """Custom traverser handling views and namespaces + + This is an upgraded version of native Pyramid traverser. + It adds: + - a new BeforeTraverseEvent before traversing each object in the path + - support for namespaces with "++" notation + """ + + NAMESPACE_SELECTOR = '++' + + def __call__(self, request): + + environ = request.environ + matchdict = request.matchdict + + if matchdict is not None: + path = matchdict.get('traverse', slash) or slash + if is_nonstr_iter(path): + # this is a *traverse stararg (not a {traverse}) + # routing has already decoded these elements, so we just + # need to join them + path = '/' + slash.join(path) or slash + + subpath = matchdict.get('subpath', ()) + if not is_nonstr_iter(subpath): + # this is not a *subpath stararg (just a {subpath}) + # routing has already decoded this string, so we just need + # to split it + subpath = split_path_info(subpath) + + else: + subpath = () + try: + # empty if mounted under a path in mod_wsgi, for example + path = request.path_info or slash + except KeyError: + # if environ['PATH_INFO'] is just not there + path = slash + except UnicodeDecodeError as e: + raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) + + if VH_ROOT_KEY in environ: + # HTTP_X_VHM_ROOT + vroot_path = decode_path_info(environ[VH_ROOT_KEY]) + vroot_tuple = split_path_info(vroot_path) + vpath = vroot_path + path + vroot_idx = len(vroot_tuple) - 1 + else: + vroot_tuple = () + vpath = path + vroot_idx = -1 + + root = self.root + ob = vroot = root + + request.registry.notify(BeforeTraverseEvent(root, request)) + + if vpath == slash: + # invariant: vpath must not be empty + # prevent a call to traversal_path if we know it's going + # to return the empty tuple + vpath_tuple = () + + else: + # we do dead reckoning here via tuple slicing instead of + # pushing and popping temporary lists for speed purposes + # and this hurts readability; apologies + i = 0 + view_selector = self.VIEW_SELECTOR + ns_selector = self.NAMESPACE_SELECTOR + vpath_tuple = split_path_info(vpath) + + for segment in vpath_tuple: + request.registry.notify(BeforeTraverseEvent(ob, request)) + + if segment[:2] == view_selector: + return {'context': ob, + 'view_name': segment[2:], + 'subpath': vpath_tuple[i + 1:], + 'traversed': vpath_tuple[:vroot_idx + i + 1], + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + + if segment[:2] == ns_selector: + ns, name = segment[2:].split(ns_selector, 1) + registry = get_current_registry() + traverser = registry.queryMultiAdapter((ob, request), ITraversable, ns) + if traverser is None: + traverser = registry.queryAdapter(ob, ITraversable, ns) + if traverser is None: + traverser = registry.queryAdapter(request, ITraversable, ns) + if traverser is None: + raise NotFound() + ob = traverser.traverse(name, vpath_tuple[vroot_idx + i + 1:]) + i += 1 + continue + + try: + getitem = ob.__getitem__ + except AttributeError: + return {'context': ob, + 'view_name': segment, + 'subpath': vpath_tuple[i + 1:], + 'traversed': vpath_tuple[:vroot_idx + i + 1], + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + + try: + next = getitem(segment) + except KeyError: + return {'context': ob, + 'view_name': segment, + 'subpath': vpath_tuple[i + 1:], + 'traversed': vpath_tuple[:vroot_idx + i + 1], + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + if i == vroot_idx: + vroot = next + ob = next + i += 1 + + return {'context': ob, + 'view_name': empty, + 'subpath': subpath, + 'traversed': vpath_tuple, + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + + +def get_parent(context, interface=Interface, allow_context=True): + """Get first parent of the context that implements given interface""" + if allow_context: + parent = context + else: + parent = getattr(context, '__parent__', None) + while parent is not None: + if interface.providedBy(parent): + return interface(parent) + parent = getattr(parent, '__parent__', None) + return None diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/unicode.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/unicode.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,173 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import codecs +import string + +# import interfaces + +# import packages + + +_unicodeTransTable = {} +def _fillUnicodeTransTable(): + _corresp = [ + ("A", [0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x0100, 0x0102, 0x0104]), + ("AE", [0x00C6]), + ("a", [0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x0101, 0x0103, 0x0105]), + ("ae", [0x00E6]), + ("C", [0x00C7, 0x0106, 0x0108, 0x010A, 0x010C]), + ("c", [0x00E7, 0x0107, 0x0109, 0x010B, 0x010D]), + ("D", [0x00D0, 0x010E, 0x0110]), + ("d", [0x00F0, 0x010F, 0x0111]), + ("E", [0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x0112, 0x0114, 0x0116, 0x0118, 0x011A]), + ("e", [0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x0113, 0x0115, 0x0117, 0x0119, 0x011B]), + ("G", [0x011C, 0x011E, 0x0120, 0x0122]), + ("g", [0x011D, 0x011F, 0x0121, 0x0123]), + ("H", [0x0124, 0x0126]), + ("h", [0x0125, 0x0127]), + ("I", [0x00CC, 0x00CD, 0x00CE, 0x00CF, 0x0128, 0x012A, 0x012C, 0x012E, 0x0130]), + ("i", [0x00EC, 0x00ED, 0x00EE, 0x00EF, 0x0129, 0x012B, 0x012D, 0x012F, 0x0131]), + ("IJ", [0x0132]), + ("ij", [0x0133]), + ("J", [0x0134]), + ("j", [0x0135]), + ("K", [0x0136]), + ("k", [0x0137, 0x0138]), + ("L", [0x0139, 0x013B, 0x013D, 0x013F, 0x0141]), + ("l", [0x013A, 0x013C, 0x013E, 0x0140, 0x0142]), + ("N", [0x00D1, 0x0143, 0x0145, 0x0147, 0x014A]), + ("n", [0x00F1, 0x0144, 0x0146, 0x0148, 0x0149, 0x014B]), + ("O", [0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D8, 0x014C, 0x014E, 0x0150]), + ("o", [0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F8, 0x014D, 0x014F, 0x0151]), + ("OE", [0x0152]), + ("oe", [0x0153]), + ("R", [0x0154, 0x0156, 0x0158]), + ("r", [0x0155, 0x0157, 0x0159]), + ("S", [0x015A, 0x015C, 0x015E, 0x0160]), + ("s", [0x015B, 0x015D, 0x015F, 0x01610, 0x017F]), + ("T", [0x0162, 0x0164, 0x0166]), + ("t", [0x0163, 0x0165, 0x0167]), + ("U", [0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0168, 0x016A, 0x016C, 0x016E, 0x0170, 0x172]), + ("u", [0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x016D, 0x016F, 0x0171]), + ("W", [0x0174]), + ("w", [0x0175]), + ("Y", [0x00DD, 0x0176, 0x0178]), + ("y", [0x00FD, 0x00FF, 0x0177]), + ("Z", [0x0179, 0x017B, 0x017D]), + ("z", [0x017A, 0x017C, 0x017E]) + ] + for char, codes in _corresp: + for code in codes: + _unicodeTransTable[code] = char + +_fillUnicodeTransTable() + + +def translate_string(s, escape_slashes=False, force_lower=True, spaces=' ', keep_chars='_-.'): + """Remove extended characters from string and replace them with 'basic' ones + + @param s: text to be cleaned. + @type s: str or unicode + @param escape_slashes: if True, slashes are also converted + @type escape_slashes: boolean + @param force_lower: if True, result is automatically converted to lower case + @type force_lower: boolean + @return: text without diacritics + @rtype: unicode + """ + if escape_slashes: + s = s.replace("\\", "/").split("/")[-1] + s = s.strip() + if isinstance(s, bytes): + s = s.decode("utf-8", "replace") + s = s.translate(_unicodeTransTable) + s = ''.join([a for a in s.translate(_unicodeTransTable) + if a.replace(' ', '-') in (string.ascii_letters + string.digits + (keep_chars or ''))]) + if force_lower: + s = s.lower() + if spaces != ' ': + s = s.replace(' ', spaces) + return s + + +def nvl(value, default=''): + """Get specified value, or an empty string if value is empty + + @param value: text to be checked + @param default: default value + @return: value, or default if value is empty + """ + return value or default + + +def uninvl(value, default='', encoding='utf-8'): + """Get specified value converted to unicode, or an empty unicode string if value is empty + + @param value: text to be checked + @type value: str or unicode + @param default: default value + @return: value, or default if value is empty + @rtype: unicode + """ + if isinstance(value, str): + return value + try: + return codecs.decode(value or default, encoding) + except: + return codecs.decode(value or default, 'latin1') + + +def unidict(value, encoding='utf-8'): + """Get specified dict with values converted to unicode + + @param value: input dict of strings which may be converted to unicode + @type value: dict + @return: input dict converted to unicode + @rtype: dict + """ + result = {} + for key in value: + result[key] = uninvl(value[key], encoding) + return result + + +def unilist(value, encoding='utf-8'): + """Get specified list with values converted to unicode + + @param value: input list of strings which may be converted to unicode + @type value: list + @return: input list converted to unicode + @rtype: list + """ + if not isinstance(value, (list, tuple)): + return uninvl(value, encoding) + return [uninvl(v, encoding) for v in value] + + +def encode(value, encoding='utf-8'): + """Encode given Unicode value to bytes with given encoding""" + return value.encode(encoding) if isinstance(value, str) else value + + +def utf8(value): + """Encode given unicode value to UTF-8 encoded bytes""" + return encode(value, 'utf-8') + + +def decode(value, encoding='utf-8'): + """Decode given bytes value to unicode with given encoding""" + return value.decode(encoding) if isinstance(value, bytes) else value diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/url.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/url.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,48 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from persistent.interfaces import IPersistent +from pyams_utils.interfaces.tales import ITALESExtension + +# import packages +from pyams_utils.adapter import ContextRequestViewAdapter, adapter_config +from pyramid.url import resource_url +from zope.interface import Interface + + +def absolute_url(context, request, view_name=None): + """Get resource absolute_url""" + result = resource_url(context, request) + if result.endswith('/'): + result = result[:-1] + if view_name: + if view_name.startswith('#'): + result += view_name + else: + result += '/' + view_name + return result + + +@adapter_config(name='absolute_url', context=(IPersistent, Interface, Interface), provides=ITALESExtension) +class AbsoluteUrlTalesExtension(ContextRequestViewAdapter): + """extension:absolute_url(context) TALES extension""" + + def render(self, context): + if context is None: + context = self.context + return absolute_url(context, self.request) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/views/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/views/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,1 @@ +# diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/views/decimal.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/views/decimal.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,52 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import decimal + +# import interfaces +from z3c.form.interfaces import IWidget, IDataConverter + +# import packages +from pyams_utils.adapter import adapter_config +from pyams_utils.schema import IDottedDecimalField +from z3c.form.converter import BaseDataConverter, FormatterValidationError + +from pyams_utils import _ + + +@adapter_config(context=(IDottedDecimalField, IWidget), provides=IDataConverter) +class DottedDecimalDataConverter(BaseDataConverter): + """Dotted decimal field data converter""" + + errorMessage = _('The entered value is not a valid decimal literal.') + + def __init__(self, field, widget): + super(DottedDecimalDataConverter, self).__init__(field, widget) + + def toWidgetValue(self, value): + if not value: + return self.field.missing_value + return value + + def toFieldValue(self, value): + if value is self.field.missing_value: + return '' + if not value: + return None + try: + return decimal.Decimal(value) + except decimal.InvalidOperation: + raise FormatterValidationError(self.errorMessage, value) diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/wsgi.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/wsgi.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,42 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + +# import standard library + +# import interfaces + +# import packages + + +def wsgi_environ_cache(*names): + """Wrap a function/method to cache its result for call into request.environ + + :param list[string] names: keys to cache into environ, the len(names) must + be equal to the result's length or scalar + :return: + """ + def decorator(fn): + def function_wrapper(self, request): + scalar = len(names) == 1 + try: + rs = [request.environ[cached_key] for cached_key in names] + except KeyError: + rs = fn(self, request) + if scalar: + rs = [rs, ] + request.environ.update(zip(names, rs)) + return rs[0] if scalar else rs + return function_wrapper + + return decorator diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/zmi/__init__.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,20 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# 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 diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/zmi/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/zmi/configure.zcml Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,6 @@ + + + + + diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/zmi/timezone.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/zmi/timezone.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,53 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_skin.layer import IPyAMSLayer +from pyams_utils.interfaces.timezone import IServerTimezone + +# import packages +from pyams_form.form import AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_zmi.form import AdminDialogEditForm +from pyramid.view import view_config +from z3c.form import field + +from pyams_utils import _ + + +@pagelet_config(name='properties.html', context=IServerTimezone, layer=IPyAMSLayer, + permission='system.view') +class ServerTimezonePropertiesEditForm(AdminDialogEditForm): + """Server timezone properties edit form""" + + legend = _("Update server timezone properties") + fields = field.Fields(IServerTimezone) + ajax_handler = 'properties.json' + + @property + def title(self): + return self.context.__name__ + + def updateWidgets(self, prefix=None): + super(ServerTimezonePropertiesEditForm, self).updateWidgets() + self.widgets['timezone'].addClass('select2') + + +@view_config(name='properties.json', context=IServerTimezone, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class ServerTimezonePropertiesAJAXEditForm(AJAXEditForm, ServerTimezonePropertiesEditForm): + """Server timezone properties edit form, AJAX renderer""" diff -r 16d47bd81d84 -r 3f89629b9e54 src/pyams_utils/zodb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/zodb.py Thu Feb 19 00:46:48 2015 +0100 @@ -0,0 +1,55 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from persistent.interfaces import IPersistent +from transaction.interfaces import ITransactionManager +from ZODB.interfaces import IConnection + +# import packages +from pyams_utils.adapter import adapter_config + + +@adapter_config(context=IPersistent, provides=IConnection) +def get_connection(obj): + """An adapter which gets a ZODB connection of a persistent object. + + We are assuming the object has a parent if it has been created in + this transaction. + + Raises ValueError if it is impossible to get a connection. + """ + cur = obj + while not getattr(cur, '_p_jar', None): + cur = getattr(cur, '__parent__', None) + if cur is None: + return None + return cur._p_jar + + +# IPersistent adapters copied from zc.twist package +# also register this for adapting from IConnection +@adapter_config(context=IPersistent, provides=ITransactionManager) +def get_transaction_manager(obj): + conn = IConnection(obj) # typically this will be + # zope.app.keyreference.persistent.connectionOfPersistent + try: + return conn.transaction_manager + except AttributeError: + return conn._txn_mgr + # or else we give up; who knows. transaction_manager is the more + # recent spelling.