# HG changeset patch # User Thierry Florac # Date 1544011799 -3600 # Node ID 40f12a3d67db0dfd7c610140c70908c9949ed9f6 Rebuild repository after error diff -r 000000000000 -r 40f12a3d67db .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,19 @@ + +syntax: regexp +^develop-eggs$ +syntax: regexp +^parts$ +syntax: regexp +^bin$ +syntax: regexp +^\.installed\.cfg$ +syntax: regexp +^\.settings$ +syntax: regexp +^build$ +syntax: regexp +^dist$ +syntax: regexp +^\.idea$ +syntax: regexp +.*\.pyc$ diff -r 000000000000 -r 40f12a3d67db .hgtags --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,10 @@ +00db38a07bb345bfccae2210123d3676195ca054 0.1.0 +8ee5e8dfd4da44fcf4881e96d26a1bcee0112dd1 0.1.1 +7ca0455fcded3d176d1d2ad37c58def90a82532c 0.1.2 +3695b338fe3d9cfcc20d918f4e40f6da9932a2b9 0.1.3 +960cfacb72497a8a63c4ce8eb197abddcd3a2141 0.1.4 +3093606fa69511c9483ba0681d48657d78ee8498 0.1.5 +b06631af55a5a8f2178502ea5e2c5335c4fd2b8c 0.1.6 +29149bd0b72a80d89d021481b2c1770758d45c46 0.1.7 +2d2d5040fee051f2896948a146bbff6e7a906533 0.1.8 +a3b92ff96755ee8bc471e3e87775c94bbfc21ca6 0.1.9 diff -r 000000000000 -r 40f12a3d67db LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,42 @@ +Zope Public License (ZPL) Version 2.1 +===================================== + +A copyright notice accompanies this license document that identifies +the copyright holders. + +This license has been certified as open source. It has also been designated +as GPL compatible by the Free Software Foundation (FSF). + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions in source code must retain the accompanying copyright + notice, this list of conditions, and the following disclaimer. + 2. Redistributions in binary form must reproduce the accompanying copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Names of the copyright holders must not be used to endorse or promote + products derived from this software without prior written permission + from the copyright holders. + 4. The right to distribute this software or to use it for any purpose does + not give you the right to use Servicemarks (sm) or Trademarks (tm) of the + copyright holders. Use of them is covered by separate agreement with the + copyright holders. + 5. If any files are modified, you must cause the modified files to carry + prominent notices stating that you changed the files and the date of any + change. + + +Disclaimer +========== + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff -r 000000000000 -r 40f12a3d67db MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,5 @@ +include *.txt +recursive-include docs * +recursive-include src * +global-exclude *.pyc +global-exclude *.*~ diff -r 000000000000 -r 40f12a3d67db bootstrap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bootstrap.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,178 @@ +############################################################################## +# +# Copyright (c) 2006 Zope Foundation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +"""Bootstrap a buildout-based project + +Simply run this script in a directory containing a buildout.cfg. +The script accepts buildout command-line options, so you can +use the -c option to specify an alternate configuration file. +""" + +import os +import shutil +import sys +import tempfile + +from optparse import OptionParser + +tmpeggs = tempfile.mkdtemp() + +usage = '''\ +[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] + +Bootstraps a buildout-based project. + +Simply run this script in a directory containing a buildout.cfg, using the +Python that you want bin/buildout to use. + +Note that by using --find-links to point to local resources, you can keep +this script from going over the network. +''' + +parser = OptionParser(usage=usage) +parser.add_option("-v", "--version", help="use a specific zc.buildout version") + +parser.add_option("-t", "--accept-buildout-test-releases", + dest='accept_buildout_test_releases', + action="store_true", default=False, + help=("Normally, if you do not specify a --version, the " + "bootstrap script and buildout gets the newest " + "*final* versions of zc.buildout and its recipes and " + "extensions for you. If you use this flag, " + "bootstrap and buildout will get the newest releases " + "even if they are alphas or betas.")) +parser.add_option("-c", "--config-file", + help=("Specify the path to the buildout configuration " + "file to be used.")) +parser.add_option("-f", "--find-links", + help=("Specify a URL to search for buildout releases")) +parser.add_option("--allow-site-packages", + action="store_true", default=False, + help=("Let bootstrap.py use existing site packages")) + + +options, args = parser.parse_args() + +###################################################################### +# load/install setuptools + +try: + if options.allow_site_packages: + import setuptools + import pkg_resources + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +ez = {} +exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez) + +if not options.allow_site_packages: + # ez_setup imports site, which adds site packages + # this will remove them from the path to ensure that incompatible versions + # of setuptools are not in the path + import site + # inside a virtualenv, there is no 'getsitepackages'. + # We can't remove these reliably + if hasattr(site, 'getsitepackages'): + for sitepackage_path in site.getsitepackages(): + sys.path[:] = [x for x in sys.path if sitepackage_path not in x] + +setup_args = dict(to_dir=tmpeggs, download_delay=0) +ez['use_setuptools'](**setup_args) +import setuptools +import pkg_resources + +# This does not (always?) update the default working set. We will +# do it. +for path in sys.path: + if path not in pkg_resources.working_set.entries: + pkg_resources.working_set.add_entry(path) + +###################################################################### +# Install buildout + +ws = pkg_resources.working_set + +cmd = [sys.executable, '-c', + 'from setuptools.command.easy_install import main; main()', + '-mZqNxd', tmpeggs] + +find_links = os.environ.get( + 'bootstrap-testing-find-links', + options.find_links or + ('http://downloads.buildout.org/' + if options.accept_buildout_test_releases else None) + ) +if find_links: + cmd.extend(['-f', find_links]) + +setuptools_path = ws.find( + pkg_resources.Requirement.parse('setuptools')).location + +requirement = 'zc.buildout' +version = options.version +if version is None and not options.accept_buildout_test_releases: + # Figure out the most recent final version of zc.buildout. + import setuptools.package_index + _final_parts = '*final-', '*final' + + def _final_version(parsed_version): + for part in parsed_version: + if (part[:1] == '*') and (part not in _final_parts): + return False + return True + index = setuptools.package_index.PackageIndex( + search_path=[setuptools_path]) + if find_links: + index.add_find_links((find_links,)) + req = pkg_resources.Requirement.parse(requirement) + if index.obtain(req) is not None: + best = [] + bestv = None + for dist in index[req.project_name]: + distv = dist.parsed_version + if _final_version(distv): + if bestv is None or distv > bestv: + best = [dist] + bestv = distv + elif distv == bestv: + best.append(dist) + if best: + best.sort() + version = best[-1].version +if version: + requirement = '=='.join((requirement, version)) +cmd.append(requirement) + +import subprocess +if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: + raise Exception( + "Failed to execute command:\n%s" % repr(cmd)[1:-1]) + +###################################################################### +# Import and run buildout + +ws.add_entry(tmpeggs) +ws.require(requirement) +import zc.buildout.buildout + +if not [a for a in args if '=' not in a]: + args.append('bootstrap') + +# if -c was provided, we push it back into args for buildout' main function +if options.config_file is not None: + args[0:0] = ['-c', options.config_file] + +zc.buildout.buildout.main(args) +shutil.rmtree(tmpeggs) diff -r 000000000000 -r 40f12a3d67db buildout.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/buildout.cfg Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,74 @@ +[buildout] +extends = http://download.ztfy.org/pyams/pyams-dev.cfg +eggs-directory = /var/local/env/pyams/eggs +find-links = http://download.ztfy.org/eggs + +socket-timeout = 3 +show-picked-versions = true +newest = false + +allow-hosts = + bitbucket.org + *.python.org + *.sourceforge.net + github.com + +versions = versions +newest = false +#allow-picked-versions = false + +src = src +develop = + . + ../ext/lingua + ../pyams_catalog + ../pyams_file + ../pyams_form + ../pyams_i18n + ../pyams_pagelet + ../pyams_skin + ../pyams_template + ../pyams_utils + ../pyams_viewlet + ../pyams_zmi + +parts = + package + i18n + pyflakes + test + +[package] +recipe = zc.recipe.egg +eggs = + pyams_alchemy + pyramid + sqlalchemy + zope.component + zope.interface + zope.sqlalchemy + +[i18n] +recipe = zc.recipe.egg +eggs = + babel + lingua + +[pyflakes] +recipe = zc.recipe.egg +eggs = pyflakes +scripts = pyflakes +entry-points = pyflakes=pyflakes.scripts.pyflakes:main +initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"] + +[pyflakesrun] +recipe = collective.recipe.cmd +on_install = true +cmds = ${buildout:develop}/bin/${pyflakes:scripts} + +[test] +recipe = zc.recipe.testrunner +eggs = pyams_alchemy [test] + +[versions] +pyams_alchemy = 0.1.9 diff -r 000000000000 -r 40f12a3d67db docs/HISTORY.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/HISTORY.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,57 @@ +History +======= + +0.1.9 +----- + - updated text areas CSS class + +0.1.8 +----- + - use "ajax_config" decorator to declare AJAX views + +0.1.7 +----- + - added prefix to engine edit form + +0.1.6 +----- + - added check of registry settings in dynamic schema mixin class + +0.1.5 +----- + - upgrade zope.sqlalchemy package to release 1.0.0 + - updated global registry access + - refactored for last ZMI package + - added logging + +0.1.4 +----- + - use DocFieldProperty to generate Sphinx documentation + +0.1.3 +----- + - added "meta.zcml" file to configuration to allow static declaration of SQLAlchemy engines + from ZCML files through PyAMS "engine" directive + - added 'use_pool' as attribute to SQLALchemy engine utility interface (including via ZCML) and as argument + to 'get_user_engine' function to be able to disable pooling for a given user engine or session. + This argument can be usefull when session is used from PyAMS scheduler jobs where connections + shouldn't be stored and reused... + - added 'echo_pool' as attribute to SQLAlchemy engine utility interface (including via ZCML) to be able to + log pool checkouts and checkins + - added 'clear_engine' method with subscriber on modification event to force removal of volatile attributes + - changed vocabulary declaration + - changed widget CSS class attribute + +0.1.2 +----- + - use named permissions + +0.1.1 +----- + - corrected closing of database connections on pool checkin/checkout + - added DataLoader class to help data migration between databases + - added DynamicSchemaMixin class to allow definition of schema name from configuration file + +0.1.0 +----- + - First release diff -r 000000000000 -r 40f12a3d67db docs/README.txt diff -r 000000000000 -r 40f12a3d67db setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,73 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +""" +This module contains pyams_alchemy package +""" +import os +from setuptools import setup, find_packages + +DOCS = os.path.join(os.path.dirname(__file__), + 'docs') + +README = os.path.join(DOCS, 'README.txt') +HISTORY = os.path.join(DOCS, 'HISTORY.txt') + +version = '0.1.9' +long_description = open(README).read() + '\n\n' + open(HISTORY).read() + +tests_require = [] + +setup(name='pyams_alchemy', + version=version, + description="PyAMS interfaces and classes for SQLAlchemy integration", + long_description=long_description, + classifiers=[ + "License :: OSI Approved :: Zope Public License", + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords='Pyramid PyAMS SQLALchemy', + author='Thierry Florac', + author_email='tflorac@ulthar.net', + url='http://hg.ztfy.org/pyams/pyams_alchemy', + license='ZPL', + packages=find_packages('src'), + package_dir={'': 'src'}, + namespace_packages=[], + include_package_data=True, + package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']}, + zip_safe=False, + # uncomment this to be able to run tests with setup.py + test_suite="pyams_alchemy.tests.test_utilsdocs.test_suite", + tests_require=tests_require, + extras_require=dict(test=tests_require), + install_requires=[ + 'setuptools', + # -*- Extra requirements: -*- + 'pyams_utils', + 'pyramid', + 'sqlalchemy', + 'zope.component', + 'zope.componentvocabulary', + 'zope.container', + 'zope.interface', + 'zope.lifecycleevent', + 'zope.schema', + 'zope.sqlalchemy >= 1.0.0', + ], + entry_points=""" + # -*- Entry points: -*- + """, + ) diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/PKG-INFO --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/PKG-INFO Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,76 @@ +Metadata-Version: 2.1 +Name: pyams-alchemy +Version: 0.1.9 +Summary: PyAMS interfaces and classes for SQLAlchemy integration +Home-page: http://hg.ztfy.org/pyams/pyams_alchemy +Author: Thierry Florac +Author-email: tflorac@ulthar.net +License: ZPL +Description: + + History + ======= + + 0.1.9 + ----- + - updated text areas CSS class + + 0.1.8 + ----- + - use "ajax_config" decorator to declare AJAX views + + 0.1.7 + ----- + - added prefix to engine edit form + + 0.1.6 + ----- + - added check of registry settings in dynamic schema mixin class + + 0.1.5 + ----- + - upgrade zope.sqlalchemy package to release 1.0.0 + - updated global registry access + - refactored for last ZMI package + - added logging + + 0.1.4 + ----- + - use DocFieldProperty to generate Sphinx documentation + + 0.1.3 + ----- + - added "meta.zcml" file to configuration to allow static declaration of SQLAlchemy engines + from ZCML files through PyAMS "engine" directive + - added 'use_pool' as attribute to SQLALchemy engine utility interface (including via ZCML) and as argument + to 'get_user_engine' function to be able to disable pooling for a given user engine or session. + This argument can be usefull when session is used from PyAMS scheduler jobs where connections + shouldn't be stored and reused... + - added 'echo_pool' as attribute to SQLAlchemy engine utility interface (including via ZCML) to be able to + log pool checkouts and checkins + - added 'clear_engine' method with subscriber on modification event to force removal of volatile attributes + - changed vocabulary declaration + - changed widget CSS class attribute + + 0.1.2 + ----- + - use named permissions + + 0.1.1 + ----- + - corrected closing of database connections on pool checkin/checkout + - added DataLoader class to help data migration between databases + - added DynamicSchemaMixin class to allow definition of schema name from configuration file + + 0.1.0 + ----- + - First release + +Keywords: Pyramid PyAMS SQLALchemy +Platform: UNKNOWN +Classifier: License :: OSI Approved :: Zope Public License +Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python +Classifier: Framework :: Pyramid +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Provides-Extra: test diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/SOURCES.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/SOURCES.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,32 @@ +MANIFEST.in +setup.py +docs/HISTORY.txt +docs/README.txt +src/pyams_alchemy/__init__.py +src/pyams_alchemy/configure.zcml +src/pyams_alchemy/engine.py +src/pyams_alchemy/loader.py +src/pyams_alchemy/meta.zcml +src/pyams_alchemy/metaconfigure.py +src/pyams_alchemy/metadirectives.py +src/pyams_alchemy/mixin.py +src/pyams_alchemy.egg-info/PKG-INFO +src/pyams_alchemy.egg-info/SOURCES.txt +src/pyams_alchemy.egg-info/dependency_links.txt +src/pyams_alchemy.egg-info/entry_points.txt +src/pyams_alchemy.egg-info/namespace_packages.txt +src/pyams_alchemy.egg-info/not-zip-safe +src/pyams_alchemy.egg-info/requires.txt +src/pyams_alchemy.egg-info/top_level.txt +src/pyams_alchemy/doctests/README.txt +src/pyams_alchemy/interfaces/__init__.py +src/pyams_alchemy/locales/pyams_alchemy.mo +src/pyams_alchemy/locales/pyams_alchemy.pot +src/pyams_alchemy/locales/fr/LC_MESSAGES/pyams_alchemy.mo +src/pyams_alchemy/locales/fr/LC_MESSAGES/pyams_alchemy.po +src/pyams_alchemy/tests/__init__.py +src/pyams_alchemy/tests/test_utilsdocs.py +src/pyams_alchemy/tests/test_utilsdocstrings.py +src/pyams_alchemy/zmi/__init__.py +src/pyams_alchemy/zmi/engine.py +src/pyams_alchemy/zmi/templates/engine-test.pt \ No newline at end of file diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/dependency_links.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/dependency_links.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/entry_points.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/entry_points.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,3 @@ + + # -*- Entry points: -*- + \ No newline at end of file diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/namespace_packages.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/namespace_packages.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/not-zip-safe --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/not-zip-safe Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/requires.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/requires.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,13 @@ +setuptools +pyams_utils +pyramid +sqlalchemy +zope.component +zope.componentvocabulary +zope.container +zope.interface +zope.lifecycleevent +zope.schema +zope.sqlalchemy>=1.0.0 + +[test] diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy.egg-info/top_level.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy.egg-info/top_level.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,1 @@ +pyams_alchemy diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/__init__.py Wed Dec 05 13:09:59 2018 +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. +# + +""" +PyAMS SQLAlchemy integration +============================ + +*pyams_alchemy* package is a small package which can be used to make SQLAlchemy integration more simple. + +The main goal of *pyams_alchemy* package is to define the :class:`pyams_alchemy.engine.AlchemyEngineUtility` class: +this class can be stored persistently into PyAMS local site manager (see :ref:`site`) to store settings of an +SQLAlchemy engine; on user request, the :func:`pyams_alchemy.engine.get_user_session` function can be used to get +access to a new SQLAlchemy engine session matching these settings which will to be bound to current Pyramid's +transaction. + + +Dynamic schema names +++++++++++++++++++++ + +Some times you may have to be able to setup, for a given table, a schema name which is not static but can be dynamic +through a configuration option. + +This can be done easily with the help of the :class:`pyams_alchemy.mixin.DynamicSchemaMixin` which you can inherit from +in any SQLAlchemy table subclass. + +When this is done, the schema name can be defined into Pyramid's configuration file into a setting which is called +*pyams_alchemy:{module_name}.{class_name}.schema*; for example like in +*pyams_alchemy:pyams_content.package.TableName.schema*. If not specified, the table's schema name can be defined in a +classic *__schema__* table's attribute. + +""" + +__docformat__ = 'restructuredtext' + + +from pyramid.i18n import TranslationStringFactory +_ = TranslationStringFactory('pyams_alchemy') + + +from sqlalchemy.ext.declarative import declarative_base +Base = declarative_base() + + +def includeme(config): + """Pyramid include""" + + # add translations + config.add_translation_dirs('pyams_alchemy:locales') + + # load registry components + try: + import pyams_zmi + except ImportError: + config.scan(ignore='pyams_alchemy.zmi') + else: + config.scan() + + if hasattr(config, 'load_zcml'): + config.load_zcml('configure.zcml') diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/configure.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/configure.zcml Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,7 @@ + + + + + + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/doctests/README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/doctests/README.txt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,3 @@ +===================== +pyams_alchemy package +===================== diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/engine.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/engine.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,262 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library +import logging +logger = logging.getLogger('PyAMS (SQLAlchemy)') + +import sqlalchemy +import time +from datetime import datetime +from sqlalchemy.event import listens_for +from sqlalchemy.orm.scoping import scoped_session +from sqlalchemy.orm.session import sessionmaker +from sqlalchemy.pool import Pool, NullPool +from threading import Thread, Lock + +# import interfaces +from pyams_alchemy.interfaces import REQUEST_SESSION_KEY, IAlchemyEngineUtility +from pyams_utils.interfaces.site import IOptionalUtility +from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectModifiedEvent, IObjectRemovedEvent + +# import packages +from persistent import Persistent +from persistent.dict import PersistentDict +from pyams_utils.registry import query_utility +from pyams_utils.request import check_request, get_request_data, set_request_data +from pyams_utils.vocabulary import vocabulary_config +from pyramid.events import subscriber +from zope.componentvocabulary.vocabulary import UtilityVocabulary +from zope.container.contained import Contained +from zope.interface import implementer +from zope.schema.fieldproperty import FieldProperty +from zope.sqlalchemy.datamanager import _SESSION_STATE, STATUS_READONLY, STATUS_ACTIVE, \ + ZopeTransactionExtension, join_transaction + + +CONNECTIONS_TIMESTAMP = {} +CONNECTIONS_LOCK = Lock() + + +@listens_for(Pool, 'checkout') +def handle_pool_checkout(connection, record, proxy): + """Pool connection checkout + + Called when a connection is retrieved from the pool. + If the connection record is already marked, we remove it from the mapping. + """ + with CONNECTIONS_LOCK: + if record in CONNECTIONS_TIMESTAMP: + logger.debug("Removing timestamp for checked-out connection {0!r} ({1!r})".format(connection, record)) + del CONNECTIONS_TIMESTAMP[record] + + +@listens_for(Pool, 'checkin') +def handle_pool_checkin(connection, record): + """Pool connection checkin + + Called when a connection returns to the pool. + We apply a timestamp on the connection record to be able to close it automatically + after 5 minutes without being used. + """ + with CONNECTIONS_LOCK: + logger.debug("Setting inactivity timestamp for checked-in connection {0!r} ({1!r})".format(connection, record)) + CONNECTIONS_TIMESTAMP[record] = datetime.utcnow() + + +class ConnectionCleanerThread(Thread): + """Background thread used to clean unused database connections + + Each connection is referenced in CONNECTION_TIMESTAMPS mapping on checkin and is invalidated + if not being used after 5 minutes + """ + timeout = 300 + + def run(self): + while True: + now = datetime.utcnow() + for connection, value in list(CONNECTIONS_TIMESTAMP.items()): + delta = now - value + if delta.total_seconds() > self.timeout: + logger.debug("Invalidating unused connection {0!r} from pool".format(connection)) + with CONNECTIONS_LOCK: + connection.invalidate() + del CONNECTIONS_TIMESTAMP[connection] + time.sleep(60) + +logger.info("Starting SQLAlchemy connections management thread") +cleaner_thread = ConnectionCleanerThread() +cleaner_thread.daemon = True +cleaner_thread.start() + + +@implementer(IAlchemyEngineUtility, IOptionalUtility) +class AlchemyEngineUtility(object): + """SQLAlchemy engine utility""" + + name = FieldProperty(IAlchemyEngineUtility['name']) + dsn = FieldProperty(IAlchemyEngineUtility['dsn']) + echo = FieldProperty(IAlchemyEngineUtility['echo']) + use_pool = FieldProperty(IAlchemyEngineUtility['use_pool']) + pool_size = FieldProperty(IAlchemyEngineUtility['pool_size']) + pool_recycle = FieldProperty(IAlchemyEngineUtility['pool_recycle']) + echo_pool = FieldProperty(IAlchemyEngineUtility['echo_pool']) + encoding = FieldProperty(IAlchemyEngineUtility['encoding']) + convert_unicode = FieldProperty(IAlchemyEngineUtility['convert_unicode']) + + def __init__(self, name='', dsn='', echo=False, use_pool=True, pool_size=25, pool_recycle=-1, echo_pool=False, + encoding='utf-8', convert_unicode=False, **kwargs): + self.name = name + self.dsn = dsn + self.echo = echo + self.use_pool = use_pool + self.pool_size = pool_size + self.pool_recycle = pool_recycle + self.echo_pool = echo_pool + self.encoding = encoding + self.convert_unicode = convert_unicode + self.kw = PersistentDict() + self.kw.update(kwargs) + + def __setattr__(self, key, value): + super(AlchemyEngineUtility, self).__setattr__(key, value) + if (key != '_v_engine') and hasattr(self, '_v_engine'): + delattr(self, '_v_engine') + + def get_engine(self, use_pool=True): + kw = {} + kw.update(self.kw) + if not (use_pool and self.use_pool): + # Always create a new engine when pooling is disabled to help engine disposal + return sqlalchemy.create_engine(self.dsn, + echo=self.echo, + poolclass=NullPool, + encoding=self.encoding, + convert_unicode=self.convert_unicode, + strategy='threadlocal', + **kw) + else: + # Store engine into volatile attributes when pooling is enabled + engine = getattr(self, '_v_engine', None) + if engine is None: + engine = self._v_engine = sqlalchemy.create_engine(self.dsn, + echo=self.echo, + pool_size=self.pool_size, + pool_recycle=self.pool_recycle, + echo_pool=self.echo_pool, + encoding=self.encoding, + convert_unicode=self.convert_unicode, + strategy='threadlocal', + **kw) + return engine + + def clear_engine(self): + if hasattr(self, '_v_engine'): + delattr(self, '_v_engine') + + +class PersistentAlchemyEngineUtility(Persistent, AlchemyEngineUtility, Contained): + """Persistent implementation of SQLAlchemy engine utility""" + + +@subscriber(IObjectAddedEvent, context_selector=IAlchemyEngineUtility) +def handle_added_engine(event): + """Register new SQLAlchemy engine when added""" + manager = event.newParent + manager.registerUtility(event.object, IAlchemyEngineUtility, name=event.object.name or '') + + +@subscriber(IObjectModifiedEvent, context_selector=IAlchemyEngineUtility) +def handle_modified_engine(event): + """Clear SQLAlchemy engine volatile attributes when modified""" + IAlchemyEngineUtility(event.object).clear_engine() + + +@subscriber(IObjectRemovedEvent, context_selector=IAlchemyEngineUtility) +def handle_removed_engine(event): + """Un-register an SQLAlchemy engine when deleted""" + manager = event.oldParent + manager.unregisterUtility(event.object, IAlchemyEngineUtility, name=event.object.name or '') + + +def get_engine(engine, use_pool=True): + """Get engine matching given utility name""" + if isinstance(engine, str): + engine = query_utility(IAlchemyEngineUtility, name=engine) + if engine is not None: + return engine.get_engine(use_pool) + + +def get_session(engine, join=True, status=STATUS_ACTIVE, request=None, alias=None, + twophase=True, use_zope_extension=True, use_pool=True): + """Get a new SQLALchemy session + + Session is stored in request and in session storage. + See :func:`get_user_session` function to get arguments documentation. + """ + if request is None: + request = check_request() + logger.debug("Checked request {0!r}".format(request)) + if not alias: + alias = engine + session_data = get_request_data(request, REQUEST_SESSION_KEY, {}) + session = session_data.get(alias) + if session is None: + _engine = get_engine(engine, use_pool) + if use_zope_extension: + factory = scoped_session(sessionmaker(bind=_engine, + twophase=twophase, + extension=ZopeTransactionExtension())) + else: + factory = sessionmaker(bind=_engine, twophase=twophase) + session = factory() + if join: + join_transaction(session, initial_state=status) + if status != STATUS_READONLY: + _SESSION_STATE[session] = session + if session is not None: + session_data[alias] = session + set_request_data(request, REQUEST_SESSION_KEY, session_data) + logger.debug("Using SQLAlchemy session {0!r}".format(session)) + return session + + +def get_user_session(engine, join=True, status=STATUS_ACTIVE, request=None, alias=None, + twophase=True, use_zope_extension=True, use_pool=True): + """Get a new SQLAlchemy session + + :param str engine: name of an SQLAlchemy engine session utility; if *engine* is not given as a string, it is + returned as-is. + :param bool join: if *True*, session is joined to the current Pyramid transaction + :param str status: status of the new session; can be STATUS_ACTIVE or STATUS_READONLY + :param request: currently running request + :param str alias: alias to use in connections mapping for this session + :param bool twophase: if *False*, session will be isolated and not included into two-phase transactions mechanism + :param bool use_zope_extension: if *True*, use ZopeTransactionExtension scoped session + :param bool use_pool: if *True*, this session will use a pool + """ + if isinstance(engine, str): + session = get_session(engine, join, status, request, alias, twophase, use_zope_extension, use_pool) + else: + session = engine + return session + + +@vocabulary_config(name='PyAMS SQLAlchemy engines') +class EnginesVocabulary(UtilityVocabulary): + """SQLAlchemy engines vocabulary""" + + interface = IAlchemyEngineUtility + nameOnly = True diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/interfaces/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/interfaces/__init__.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,81 @@ +# +# 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.interface import Interface + +# import packages +from zope.schema import TextLine, Bool, Int, Choice + +from pyams_alchemy import _ + + +REQUEST_SESSION_KEY = 'pyams_alchemy.session' + + +class IAlchemyEngineUtility(Interface): + """SQLALchemy engine definition interface""" + + name = TextLine(title=_("Engine name"), + description=_("Keep empty if this engine is the default engine..."), + required=False, + default='') + + dsn = TextLine(title=_('DSN'), + description=_('RFC-1738 compliant URL for the database connection'), + required=True, + default=u'sqlite://') + + echo = Bool(title=_('Echo SQL?'), + description=_("Log all SQL statements to system logger"), + required=True, + default=False) + + use_pool = Bool(title=_("Use connections pool?"), + description=_("If 'no', collections pooling will be disabled"), + required=True, + default=True) + + pool_size = Int(title=_("Pool size"), + description=_("SQLAlchemy connections pool size"), + required=False, + default=25) + + pool_recycle = Int(title=_("Pool recycle time"), + description=_("SQLAlchemy connection recycle time (-1 for none)"), + required=False, + default=-1) + + echo_pool = Bool(title=_("Echo pool?"), + description=_("Log all pool checkouts/checkins to system logger?"), + required=True, + default=False) + + encoding = Choice(title=_('Encoding'), + required=True, + vocabulary='PyAMS encodings', + default='utf-8') + + convert_unicode = Bool(title=_('Convert Unicode'), + required=True, + default=False) + + def get_engine(self, use_pool=True): + """Get SQLAlchemy engine""" + + def clear_engine(self): + """Remove inner volatile attributes when utility properties are modified""" diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/loader.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/loader.py Wed Dec 05 13:09:59 2018 +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 logging +logger = logging.getLogger('PyAMS (alchemy)') + +# import interfaces + +# import packages +from pyams_alchemy.engine import get_user_session +from sqlalchemy.orm.session import make_transient + + +class DataLoader(object): + """SQLAlchemy data loader + + This utility class is used to migrate entities from a given connection + to another one. + WARNING: actually, given entities must share the same schema name!!! + """ + + def __init__(self, source, target, entities): + """ + Initialize data loader + + :param source: name of registered source engine + :param target: name of registered target engine + :param entities: list of migrated entities + """ + self.source_session = get_user_session(source) + self.target_session = get_user_session(target) + self.entities = entities + + def run(self): + source = self.source_session + target = self.target_session + for entity in self.entities: + logger.info('Loading entity {0!r}'.format(entity)) + for record in source.query(entity): + source.expunge(record) + make_transient(record) + target.add(record) diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/locales/fr/LC_MESSAGES/pyams_alchemy.mo Binary file src/pyams_alchemy/locales/fr/LC_MESSAGES/pyams_alchemy.mo has changed diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/locales/fr/LC_MESSAGES/pyams_alchemy.po --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/locales/fr/LC_MESSAGES/pyams_alchemy.po Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,160 @@ +# +# 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: 2016-10-23 16:31+0200\n" +"PO-Revision-Date: 2015-03-03 16:56+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" +"Generated-By: Lingua 3.8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: src/pyams_alchemy/metadirectives.py:30 +#: src/pyams_alchemy/interfaces/__init__.py:33 +msgid "Engine name" +msgstr "Nom de la connexion" + +#: src/pyams_alchemy/metadirectives.py:31 +msgid "Empty if this engine is the default engine." +msgstr "Laisser ce nom vide s'il s'agit de la connexion par défaut" + +#: src/pyams_alchemy/metadirectives.py:35 +msgid "Database URL" +msgstr "URI" + +#: src/pyams_alchemy/metadirectives.py:36 +#: src/pyams_alchemy/interfaces/__init__.py:39 +msgid "RFC-1738 compliant URL for the database connection" +msgstr "URI au format RFC-1738 pour la connexion à la base de données" + +#: src/pyams_alchemy/metadirectives.py:39 +msgid "Echo SQL statements" +msgstr "Tracer les instructions SQL" + +#: src/pyams_alchemy/metadirectives.py:43 +#: src/pyams_alchemy/interfaces/__init__.py:48 +msgid "Use connections pool?" +msgstr "Utiliser un pool ?" + +#: src/pyams_alchemy/metadirectives.py:44 +#: src/pyams_alchemy/interfaces/__init__.py:49 +msgid "If 'no', collections pooling will be disabled" +msgstr "Si 'non', l'utilisation d'un pool de connexions sera désactivé" + +#: src/pyams_alchemy/metadirectives.py:48 +#: src/pyams_alchemy/interfaces/__init__.py:53 +msgid "Pool size" +msgstr "Taille du pool" + +#: src/pyams_alchemy/metadirectives.py:49 +#: src/pyams_alchemy/interfaces/__init__.py:54 +msgid "SQLAlchemy connections pool size" +msgstr "Taille du pool de connexions SQLALchemy" + +#: src/pyams_alchemy/metadirectives.py:53 +#: src/pyams_alchemy/interfaces/__init__.py:58 +msgid "Pool recycle time" +msgstr "Durée de recyclage" + +#: src/pyams_alchemy/metadirectives.py:54 +#: src/pyams_alchemy/interfaces/__init__.py:59 +msgid "SQLAlchemy connection recycle time (-1 for none)" +msgstr "" +"Durée de vie (en secondes) des connexions avant leur recyclage ; indiquer -1 pour " +"conserver les connexions" + +#: src/pyams_alchemy/metadirectives.py:58 +#: src/pyams_alchemy/interfaces/__init__.py:63 +msgid "Echo pool?" +msgstr "Traces du pool ?" + +#: src/pyams_alchemy/metadirectives.py:59 +#: src/pyams_alchemy/interfaces/__init__.py:64 +msgid "Log all pool checkouts/checkins to system logger?" +msgstr "Tracer toutes les entrées/sorties du pool de connexions dans le système de logs de l'application" + +#: src/pyams_alchemy/metadirectives.py:63 +#: src/pyams_alchemy/interfaces/__init__.py:68 +msgid "Encoding" +msgstr "Encodage" + +#: src/pyams_alchemy/metadirectives.py:68 +#: src/pyams_alchemy/interfaces/__init__.py:73 +msgid "Convert Unicode" +msgstr "Conversion Unicode" + +#: src/pyams_alchemy/zmi/engine.py:56 +msgid "Add SQLAlchemy engine..." +msgstr "Ajouter un moteur SQLAlchemy..." + +#: src/pyams_alchemy/zmi/engine.py:66 +msgid "Utilities" +msgstr "Utilitaires" + +#: src/pyams_alchemy/zmi/engine.py:67 +msgid "Add SQLAlchemy engine" +msgstr "Ajout d'un moteur SQLAlchemy" + +#: src/pyams_alchemy/zmi/engine.py:114 +msgid "Update SQLAlchemy engine properties" +msgstr "Modification des propriétés d'un moteur SQLAlchemy" + +#: src/pyams_alchemy/zmi/engine.py:138 +msgid "Test connection..." +msgstr "Tester la connexion..." + +#: src/pyams_alchemy/zmi/engine.py:169 +msgid "Test SQLAlchemy engine" +msgstr "Test d'un moteur SQLAlchemy" + +#: src/pyams_alchemy/zmi/engine.py:205 +msgid "Query results" +msgstr "Résultats de la requête" + +#: src/pyams_alchemy/zmi/engine.py:148 +msgid "SQL query" +msgstr "Requête SQL" + +#: src/pyams_alchemy/zmi/engine.py:155 +msgid "Close" +msgstr "Fermer" + +#: src/pyams_alchemy/zmi/engine.py:156 +msgid "Execute SQL" +msgstr "Exécuter" + +#: src/pyams_alchemy/zmi/engine.py:92 +msgid "Specified engine name is already used!" +msgstr "Le nom indiqué pour ce moteur est déjà utilisé !" + +#: src/pyams_alchemy/zmi/engine.py:95 +msgid "An SQLAlchemy engine is already registered with this name!" +msgstr "Un moteur SQLAlchemy est déjà enregistré avec ce nom !" + +#: src/pyams_alchemy/zmi/engine.py:112 src/pyams_alchemy/zmi/engine.py:167 +#, python-format +msgid "SQLAlchemy engine: {0}" +msgstr "Moteur SQLAlchemy : {0}" + +#: src/pyams_alchemy/interfaces/__init__.py:34 +msgid "Keep empty if this engine is the default engine..." +msgstr "Laisser ce nom vide s'il s'agit du moteur par défaut..." + +#: src/pyams_alchemy/interfaces/__init__.py:38 +msgid "DSN" +msgstr "DSN" + +#: src/pyams_alchemy/interfaces/__init__.py:43 +msgid "Echo SQL?" +msgstr "Traces SQL ?" + +#: src/pyams_alchemy/interfaces/__init__.py:44 +msgid "Log all SQL statements to system logger" +msgstr "Tracer toutes les instructions SQL dans le système de logs de l'application" diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/locales/pyams_alchemy.mo Binary file src/pyams_alchemy/locales/pyams_alchemy.mo has changed diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/locales/pyams_alchemy.pot --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/locales/pyams_alchemy.pot Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,158 @@ +# +# SOME DESCRIPTIVE TITLE +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , 2016. +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE 1.0\n" +"POT-Creation-Date: 2016-10-23 16:31+0200\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_alchemy/metadirectives.py:30 +#: ./src/pyams_alchemy/interfaces/__init__.py:33 +msgid "Engine name" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:31 +msgid "Empty if this engine is the default engine." +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:35 +msgid "Database URL" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:36 +#: ./src/pyams_alchemy/interfaces/__init__.py:39 +msgid "RFC-1738 compliant URL for the database connection" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:39 +msgid "Echo SQL statements" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:43 +#: ./src/pyams_alchemy/interfaces/__init__.py:48 +msgid "Use connections pool?" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:44 +#: ./src/pyams_alchemy/interfaces/__init__.py:49 +msgid "If 'no', collections pooling will be disabled" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:48 +#: ./src/pyams_alchemy/interfaces/__init__.py:53 +msgid "Pool size" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:49 +#: ./src/pyams_alchemy/interfaces/__init__.py:54 +msgid "SQLAlchemy connections pool size" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:53 +#: ./src/pyams_alchemy/interfaces/__init__.py:58 +msgid "Pool recycle time" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:54 +#: ./src/pyams_alchemy/interfaces/__init__.py:59 +msgid "SQLAlchemy connection recycle time (-1 for none)" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:58 +#: ./src/pyams_alchemy/interfaces/__init__.py:63 +msgid "Echo pool?" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:59 +#: ./src/pyams_alchemy/interfaces/__init__.py:64 +msgid "Log all pool checkouts/checkins to system logger?" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:63 +#: ./src/pyams_alchemy/interfaces/__init__.py:68 +msgid "Encoding" +msgstr "" + +#: ./src/pyams_alchemy/metadirectives.py:68 +#: ./src/pyams_alchemy/interfaces/__init__.py:73 +msgid "Convert Unicode" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:56 +msgid "Add SQLAlchemy engine..." +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:66 +msgid "Utilities" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:67 +msgid "Add SQLAlchemy engine" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:114 +msgid "Update SQLAlchemy engine properties" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:138 +msgid "Test connection..." +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:169 +msgid "Test SQLAlchemy engine" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:205 +msgid "Query results" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:148 +msgid "SQL query" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:155 +msgid "Close" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:156 +msgid "Execute SQL" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:92 +msgid "Specified engine name is already used!" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:95 +msgid "An SQLAlchemy engine is already registered with this name!" +msgstr "" + +#: ./src/pyams_alchemy/zmi/engine.py:112 ./src/pyams_alchemy/zmi/engine.py:167 +#, python-format +msgid "SQLAlchemy engine: {0}" +msgstr "" + +#: ./src/pyams_alchemy/interfaces/__init__.py:34 +msgid "Keep empty if this engine is the default engine..." +msgstr "" + +#: ./src/pyams_alchemy/interfaces/__init__.py:38 +msgid "DSN" +msgstr "" + +#: ./src/pyams_alchemy/interfaces/__init__.py:43 +msgid "Echo SQL?" +msgstr "" + +#: ./src/pyams_alchemy/interfaces/__init__.py:44 +msgid "Log all SQL statements to system logger" +msgstr "" diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/meta.zcml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/meta.zcml Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,14 @@ + + + + + + + + + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/metaconfigure.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/metaconfigure.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,30 @@ +# +# Copyright (c) 2008-2015 Thierry Florac +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# + +__docformat__ = 'restructuredtext' + + +# import standard library + +# import interfaces +from pyams_alchemy.interfaces import IAlchemyEngineUtility + +# import packages +from pyams_alchemy.engine import AlchemyEngineUtility +from zope.component import zcml + + +def engine_directive(context, name='', dsn='', echo=False, use_pool=True, pool_size=25, pool_recycle=-2, + echo_pool=False, encoding='utf-8', convert_unicode=False, **kwargs): + engine = AlchemyEngineUtility(name, dsn, echo, use_pool, pool_size, pool_recycle, echo_pool, encoding, + convert_unicode, **kwargs) + zcml.utility(context, IAlchemyEngineUtility, engine, name=name) diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/metadirectives.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/metadirectives.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,70 @@ +# +# 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 TextLine, Bool, Int, Choice + +from pyams_alchemy import _ + + +class IEngineDirective(Interface): + """Define a new SQLAlchemy engine""" + + name = TextLine(title=_('Engine name'), + description=_('Empty if this engine is the default engine.'), + required=False, + default='') + + dsn = TextLine(title=_('Database URL'), + description=_('RFC-1738 compliant URL for the database connection'), + required=True) + + echo = Bool(title=_('Echo SQL statements'), + required=False, + default=False) + + use_pool = Bool(title=_("Use connections pool?"), + description=_("If 'no', collections pooling will be disabled"), + required=True, + default=True) + + pool_size = Int(title=_("Pool size"), + description=_("SQLAlchemy connections pool size"), + required=False, + default=25) + + pool_recycle = Int(title=_("Pool recycle time"), + description=_("SQLAlchemy connection recycle time (-1 for none)"), + required=False, + default=-1) + + echo_pool = Bool(title=_("Echo pool?"), + description=_("Log all pool checkouts/checkins to system logger?"), + required=True, + default=False) + + encoding = Choice(title=_('Encoding'), + required=True, + vocabulary='PyAMS encodings', + default='utf-8') + + convert_unicode = Bool(title=_('Convert Unicode'), + required=True, + default=False) diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/mixin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/mixin.py Wed Dec 05 13:09:59 2018 +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 + +# import packages +from pyams_utils.registry import get_global_registry +from sqlalchemy.ext.declarative import declared_attr + + +class DynamicSchemaMixin(object): + """Dynamic schema mixin class + + This class is used to set an entity schema name in Pyramid settings + """ + + __schema__ = None + + @classmethod + def get_schema_settings_name(cls): + return 'pyams_alchemy:{0}.{1}.schema'.format(cls.__module__, cls.__name__) + + @classmethod + def get_schema(cls): + settings_name = cls.get_schema_settings_name() + if settings_name: + registry = get_global_registry() + if hasattr(registry, 'settings'): + return {'schema': registry.settings.get(settings_name, cls.__schema__)} + return {'schema': cls.__schema__} + + @declared_attr + def __table_args__(cls): + return cls.get_schema() diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/tests/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/tests/__init__.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,1 @@ + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/tests/test_utilsdocs.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/tests/test_utilsdocs.py Wed Dec 05 13:09:59 2018 +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. +# + +""" +Generic Test case for pyams_alchemy doctest +""" +__docformat__ = 'restructuredtext' + +import unittest +import doctest +import sys +import os + + +current_dir = os.path.dirname(__file__) + +def doc_suite(test_dir, setUp=None, tearDown=None, globs=None): + """Returns a test suite, based on doctests found in /doctest.""" + suite = [] + if globs is None: + globs = globals() + + flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | + doctest.REPORT_ONLY_FIRST_FAILURE) + + package_dir = os.path.split(test_dir)[0] + if package_dir not in sys.path: + sys.path.append(package_dir) + + doctest_dir = os.path.join(package_dir, 'doctests') + + # filtering files on extension + docs = [os.path.join(doctest_dir, doc) for doc in + os.listdir(doctest_dir) if doc.endswith('.txt')] + + for test in docs: + suite.append(doctest.DocFileSuite(test, optionflags=flags, + globs=globs, setUp=setUp, + tearDown=tearDown, + module_relative=False)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') + diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/tests/test_utilsdocstrings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/tests/test_utilsdocstrings.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,62 @@ +# +# 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. +# + +""" +Generic Test case for pyams_alchemy 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_alchemy.%s' % test + suite.append(doctest.DocTestSuite(location, optionflags=flags, + globs=globs)) + + return unittest.TestSuite(suite) + +def test_suite(): + """returns the test suite""" + return doc_suite(current_dir) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite') diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/zmi/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/zmi/__init__.py Wed Dec 05 13:09:59 2018 +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 000000000000 -r 40f12a3d67db src/pyams_alchemy/zmi/engine.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/zmi/engine.py Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,233 @@ +# +# 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_alchemy.interfaces import IAlchemyEngineUtility +from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu, ITableItemColumnActionsMenu +from pyams_skin.layer import IPyAMSLayer +from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION, MANAGE_SYSTEM_PERMISSION +from pyams_zmi.layer import IAdminLayer +from z3c.form.interfaces import IDataExtractedEvent, DISPLAY_MODE +from zope.component.interfaces import ISite + +# import packages +from pyams_alchemy.engine import PersistentAlchemyEngineUtility, get_user_session +from pyams_form.form import AJAXAddForm, ajax_config +from pyams_form.schema import CloseButton +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.table import BaseTable +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_template.template import template_config +from pyams_utils.registry import query_utility +from pyams_utils.url import absolute_url +from pyams_viewlet.viewlet import viewlet_config, Viewlet +from pyams_zmi.form import AdminDialogEditForm, AdminDialogAddForm +from pyams_zmi.zmi.control_panel import UtilitiesTable +from pyramid.events import subscriber +from z3c.form import field, button +from z3c.table.column import GetAttrColumn +from zope.interface import Interface, Invalid +from zope.schema import Text + +from pyams_alchemy import _ + + +@viewlet_config(name='add-sqlalchemy-engine.menu', context=ISite, layer=IAdminLayer, + view=UtilitiesTable, manager=IToolbarAddingMenu, permission=MANAGE_SYSTEM_PERMISSION) +class AlchemyEngineAddMenu(ToolbarMenuItem): + """SQLAlchemy engine add menu""" + + label = _("Add SQLAlchemy engine...") + label_css_class = 'fa fa-fw fa-database' + url = 'add-sqlalchemy-engine.html' + modal_target = True + + +@pagelet_config(name='add-sqlalchemy-engine.html', context=ISite, layer=IPyAMSLayer, permission=MANAGE_SYSTEM_PERMISSION) +@ajax_config(name='add-sqlalchemy-engine.json', context=ISite, layer=IPyAMSLayer, permission=MANAGE_SYSTEM_PERMISSION, + base=AJAXAddForm) +class AlchemyEngineAddForm(AdminDialogAddForm): + """SQLAlchemy engine add form""" + + title = _("Utilities") + legend = _("Add SQLAlchemy engine") + icon_css_class = 'fa fa-fw fa-database' + + fields = field.Fields(IAlchemyEngineUtility) + edit_permission = None + + def create(self, data): + return PersistentAlchemyEngineUtility() + + def add(self, object): + manager = self.context.getSiteManager() + name = (object.name or '').lower() or '__DEFAULT__' + manager['sql::{0}'.format(name)] = object + + def nextURL(self): + return absolute_url(self.context, self.request, 'utilities.html') + + +@subscriber(IDataExtractedEvent, form_selector=AlchemyEngineAddForm) +def handle_new_engine_data_extraction(event): + """Handle new engine data extraction""" + manager = event.form.context.getSiteManager() + name = event.data['name'] or '' + if 'sql::{0}'.format(name.lower() or '__DEFAULT__') in manager: + event.form.widgets.errors += (Invalid(_("Specified engine name is already used!")), ) + engine = query_utility(IAlchemyEngineUtility, name=name) + if engine is not None: + event.form.widgets.errors += (Invalid(_("An SQLAlchemy engine is already registered with this name!")), ) + + +@pagelet_config(name='properties.html', context=IAlchemyEngineUtility, layer=IPyAMSLayer, + permission=VIEW_SYSTEM_PERMISSION) +@ajax_config(name='properties.json', context=IAlchemyEngineUtility, layer=IPyAMSLayer) +class AlchemyEnginePropertiesEditForm(AdminDialogEditForm): + """SQLAlchemy engine properties edit form""" + + prefix = 'engine_properties.' + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("SQLAlchemy engine: {0}")).format(self.context.name) + + legend = _("Update SQLAlchemy engine properties") + icon_css_class = 'fa fa-fw fa-database' + + fields = field.Fields(IAlchemyEngineUtility) + edit_permission = MANAGE_SYSTEM_PERMISSION + + def updateWidgets(self, prefix=None): + super(AlchemyEnginePropertiesEditForm, self).updateWidgets(prefix) + self.widgets['name'].mode = DISPLAY_MODE + self.widgets['encoding'].addClass('select2') + + +@viewlet_config(name='test-engine.menu', context=IAlchemyEngineUtility, layer=IAdminLayer, + view=UtilitiesTable, manager=ITableItemColumnActionsMenu, permission=MANAGE_SYSTEM_PERMISSION) +class AlchemyEngineTestMenu(ToolbarMenuItem): + """SQLAlchemy engine test menu""" + + label = _("Test connection...") + label_css_class = 'fa fa-fw fa-play' + url = 'test-sqlalchemy-engine.html' + modal_target = True + stop_propagation = True + + +class IAlchemyEngineTestFields(Interface): + """SQLAlchemy engine test fields""" + + query = Text(title=_("SQL query"), + required=True) + + +class IAlchemyEngineTestButtons(Interface): + """SQLAlchemy engine test form buttons""" + + close = CloseButton(name='close', title=_("Close")) + execute = button.Button(name='execute', title=_("Execute SQL")) + + +@pagelet_config(name='test-sqlalchemy-engine.html', context=IAlchemyEngineUtility, layer=IPyAMSLayer, + permission=MANAGE_SYSTEM_PERMISSION) +@ajax_config(name='test-sqlalchemy-engine.json', context=IAlchemyEngineUtility, layer=IPyAMSLayer, base=AJAXAddForm) +class AlchemyEngineTestForm(AdminDialogAddForm): + """SQLAlchemy engine test form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("SQLAlchemy engine: {0}")).format(self.context.name) + + legend = _("Test SQLAlchemy engine") + icon_css_class = 'fa fa-fw fa-database' + + fields = field.Fields(IAlchemyEngineTestFields) + buttons = button.Buttons(IAlchemyEngineTestButtons) + edit_permission = MANAGE_SYSTEM_PERMISSION + + @property + def form_target(self): + return '#{0}_test_result'.format(self.id) + + def updateActions(self): + super(AlchemyEngineTestForm, self).updateActions() + if 'execute' in self.actions: + self.actions['execute'].addClass('btn-primary') + + def createAndAdd(self, data): + data = data.get(self, data) + session = get_user_session(self.context.name) + return session.execute(data.get('query')) + + def get_ajax_output(self, changes): + result = AlchemyEngineTestResults(self.context, self.request, changes) + result.update() + return { + 'status': 'success', + 'content': { + 'html': result.render() + }, + 'close_form': False + } + + +@viewlet_config(name='engine-test-suffix', layer=IAdminLayer, manager=IWidgetsSuffixViewletsManager, + view=AlchemyEngineTestForm, weight=50) +@template_config(template='templates/engine-test.pt') +class AlchemyEngineTestSuffix(Viewlet): + """SQLAlchemy engine test suffix""" + + +class AlchemyEngineTestResults(BaseTable): + """Alchemy engine test results table""" + + title = _("Query results") + + sortOn = None + values = None + + def __init__(self, context, request, values): + super(AlchemyEngineTestResults, self).__init__(context, request) + self.values = list(values) + + @property + def data_attributes(self): + attrs = super(AlchemyEngineTestResults, self).data_attributes + attrs['table'] = { + 'data-ams-datatable-global-filter': 'false', + 'data-ams-datatable-pagination-size': 'false' + } + return attrs + + def initColumns(self): + if not self.values: + self.columns = None + else: + columns = [] + row = self.values[0] + for col in row.keys(): + column = GetAttrColumn(self.context, self.request, self) + column.__name__ = col + column.header = col + column.attrName = col + columns.append(column) + self.columns = columns diff -r 000000000000 -r 40f12a3d67db src/pyams_alchemy/zmi/templates/engine-test.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_alchemy/zmi/templates/engine-test.pt Wed Dec 05 13:09:59 2018 +0100 @@ -0,0 +1,1 @@ +