# HG changeset patch # User Thierry Florac # Date 1425570256 -3600 # Node ID e87103c49c8a70fb18c877b825a9ca0d7c89db9e # Parent 6a55c8cbced084c94c2dee8a79478d742790a683 Added ZEO connection utility diff -r 6a55c8cbced0 -r e87103c49c8a src/pyams_utils/interfaces/zeo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/interfaces/zeo.py Thu Mar 05 16:44:16 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 + +# import packages +from zope.interface import Interface, Attribute +from zope.schema import TextLine, Int, Password, Bool + +from pyams_utils import _ + + +class IZEOConnection(Interface): + """ZEO connection settings interface""" + + name = TextLine(title=_("Connection name"), + description=_("Registration name of ZEO connection"), + required=True) + + server_name = TextLine(title=_("ZEO server name"), + description=_("Hostname of ZEO server"), + required=True, + default='localhost') + + server_port = Int(title=_("ZEO server port"), + description=_("Port number of ZEO server"), + required=True, + default=8100) + + storage = TextLine(title=_("ZEO server storage"), + description=_("Storage name on ZEO server"), + required=True, + default='1') + + username = TextLine(title=_("ZEO user name"), + description=_("User name on ZEO server"), + required=False) + + password = Password(title=_("ZEO password"), + description=_("User password on ZEO server"), + required=False) + + server_realm = TextLine(title=_("ZEO server realm"), + description=_("Realm name on ZEO server"), + required=False) + + blob_dir = TextLine(title=_("BLOBs directory"), + description=_("Directory path for blob data"), + required=False) + + shared_blob_dir = Bool(title=_("Shared BLOBs directory ?"), + description=_("""Flag whether the blob_dir is a server-shared filesystem """ + """that should be used instead of transferring blob data over zrpc."""), + required=True, + default=False) + + connection = Attribute(_("Opened ZEO connection")) + + def get_settings(self): + """Get ZEO connection setting as a JSON dict""" + + def update(self, settings): + """Update internal fields with given settings dict""" + + def get_connection(self, wait=False, get_storage=False): + """Open ZEO connection with given settings""" diff -r 6a55c8cbced0 -r e87103c49c8a src/pyams_utils/zmi/zeo.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_utils/zmi/zeo.py Thu Mar 05 16:44:16 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 interfaces +from pyams_skin.interfaces.viewlet import IToolbarAddingMenu +from pyams_skin.layer import IPyAMSLayer +from pyams_utils.interfaces.zeo import IZEOConnection +from pyams_zmi.layer import IAdminLayer +from z3c.form.interfaces import IDataExtractedEvent, DISPLAY_MODE +from zope.component.interfaces import ISite + +# import packages +from pyams_form.form import AJAXAddForm, AJAXEditForm +from pyams_pagelet.pagelet import pagelet_config +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_utils.registry import query_utility +from pyams_utils.url import absolute_url +from pyams_utils.zodb import ZEOConnectionUtility +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.control_panel import UtilitiesTable +from pyams_zmi.form import AdminDialogAddForm, AdminDialogEditForm +from pyramid.events import subscriber +from pyramid.view import view_config +from z3c.form import field +from zope.interface import Invalid + +from pyams_utils import _ + + +@viewlet_config(name='add-zeo-connection.menu', context=ISite, layer=IAdminLayer, + view=UtilitiesTable, manager=IToolbarAddingMenu, permission='system.manage') +class ZEOConnectionAddMenu(ToolbarMenuItem): + """ZEO connection add menu""" + + label = _("Add ZEO connection...") + label_css_class = 'fa fa-fw fa-database' + url = 'add-zeo-connection.html' + modal_target = True + + +@pagelet_config(name='add-zeo-connection.html', context=ISite, layer=IPyAMSLayer, permission='system.manage') +class ZEOConnectionAddForm(AdminDialogAddForm): + """ZEO connection add form""" + + title = _("Utilities") + legend = _("Add ZEO connection") + icon_css_class = 'fa fa-fw fa-database' + + fields = field.Fields(IZEOConnection) + ajax_handler = 'add-zeo-connection.json' + edit_permission = None + + def create(self, data): + return ZEOConnectionUtility() + + def add(self, object): + manager = self.context.getSiteManager() + manager['zeo::{0}'.format(object.name.lower())] = object + + def nextURL(self): + return absolute_url(self.context, self.request, 'utilities.html') + + +@subscriber(IDataExtractedEvent, form_selector=ZEOConnectionAddForm) +def handle_new_connection_data_extraction(event): + """Handle new connection data extraction""" + manager = event.form.context.getSiteManager() + name = event.data['name'] + if 'zeo::{0}'.format(name.lower()) in manager: + event.form.widgets.errors += (Invalid(_("Specified connection name is already used!")), ) + connection = query_utility(IZEOConnection, name=name) + if connection is not None: + event.form.widgets.errors += (Invalid(_("A ZEO connection is already registered with this name!")), ) + + +@view_config(name='add-zeo-connection.json', context=ISite, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class ZEOConnectionAJAXAddForm(AJAXAddForm, ZEOConnectionAddForm): + """ZEO connection add form, AJAX view""" + + +@pagelet_config(name='properties.html', context=IZEOConnection, layer=IPyAMSLayer, permission='system.view') +class ZEOConnectionPropertiesEditForm(AdminDialogEditForm): + """ZEO connection properties edit form""" + + @property + def title(self): + translate = self.request.localizer.translate + return translate(_("ZEO connection: {0}")).format(self.context.name) + + legend = _("Update SQLAlchemy engine properties") + icon_css_class = 'fa fa-fw fa-database' + + fields = field.Fields(IZEOConnection) + ajax_handler = 'properties.json' + edit_permission = 'system.manage' + + def updateWidgets(self, prefix=None): + super(ZEOConnectionPropertiesEditForm, self).updateWidgets(prefix) + self.widgets['name'].mode = DISPLAY_MODE + + +@view_config(name='properties.json', context=IZEOConnection, request_type=IPyAMSLayer, + permission='system.manage', renderer='json', xhr=True) +class ZEOConnectionPropertiesAJAXEditForm(AJAXEditForm, ZEOConnectionPropertiesEditForm): + """ZEO connection properties edit form, AJAX view""" diff -r 6a55c8cbced0 -r e87103c49c8a src/pyams_utils/zodb.py --- a/src/pyams_utils/zodb.py Thu Mar 05 16:44:00 2015 +0100 +++ b/src/pyams_utils/zodb.py Thu Mar 05 16:44:16 2015 +0100 @@ -17,11 +17,26 @@ # import interfaces from persistent.interfaces import IPersistent +from pyams_utils.interfaces.site import IOptionalUtility +from pyams_utils.interfaces.zeo import IZEOConnection from transaction.interfaces import ITransactionManager from ZODB.interfaces import IConnection +from zope.annotation.interfaces import IAttributeAnnotatable +from zope.lifecycleevent.interfaces import IObjectAddedEvent, IObjectRemovedEvent +from zope.schema.interfaces import IVocabularyFactory # import packages +from persistent import Persistent from pyams_utils.adapter import adapter_config +from pyramid.events import subscriber +from ZEO import ClientStorage +from ZODB import DB +from zope.componentvocabulary.vocabulary import UtilityVocabulary +from zope.container.contained import Contained +from zope.interface import implementer, provider +from zope.schema import getFieldNames +from zope.schema.fieldproperty import FieldProperty +from zope.schema.vocabulary import getVocabularyRegistry @adapter_config(context=IPersistent, provides=IConnection) @@ -46,10 +61,98 @@ @adapter_config(context=IPersistent, provides=ITransactionManager) def get_transaction_manager(obj): conn = IConnection(obj) # typically this will be - # zope.app.keyreference.persistent.connectionOfPersistent + # zope.keyreference.persistent.connectionOfPersistent try: return conn.transaction_manager except AttributeError: return conn._txn_mgr # or else we give up; who knows. transaction_manager is the more # recent spelling. + + +@implementer(IZEOConnection) +class ZEOConnection(object): + """ZEO connection object""" + + _storage = None + _db = None + _connection = None + + name = FieldProperty(IZEOConnection['name']) + server_name = FieldProperty(IZEOConnection['server_name']) + server_port = FieldProperty(IZEOConnection['server_port']) + storage = FieldProperty(IZEOConnection['storage']) + username = FieldProperty(IZEOConnection['username']) + password = FieldProperty(IZEOConnection['password']) + server_realm = FieldProperty(IZEOConnection['server_realm']) + blob_dir = FieldProperty(IZEOConnection['blob_dir']) + shared_blob_dir = FieldProperty(IZEOConnection['shared_blob_dir']) + + def get_settings(self): + result = {} + for name in getFieldNames(IZEOConnection): + result[name] = getattr(self, name) + return result + + def update(self, settings): + names = getFieldNames(IZEOConnection) + for key, value in settings.items(): + if key in names: + setattr(self, key, value) + + def get_connection(self, wait=False, get_storage=False): + storage = ClientStorage.ClientStorage((self.server_name, self.server_port), + storage=self.storage, + username=self.username or '', + password=self.password or '', + realm=self.server_realm, + blob_dir=self.blob_dir, + shared_blob_dir=self.shared_blob_dir, + wait=wait) + db = DB(storage) + return (storage, db) if get_storage else db + + @property + def connection(self): + return self._connection + + # Context manager methods + def __enter__(self): + self._storage, self._db = self.get_connection(get_storage=True) + self._connection = self._db.open() + return self._connection.root() + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._connection is not None: + self._connection.close() + if self._storage is not None: + self._storage.close() + + +@implementer(IOptionalUtility, IAttributeAnnotatable) +class ZEOConnectionUtility(ZEOConnection, Persistent, Contained): + """Persistent ZEO connection utility""" + + +@subscriber(IObjectAddedEvent, context_selector=IZEOConnection) +def handle_added_connection(event): + """Register new ZEO connection when added""" + manager = event.newParent + manager.registerUtility(event.object, IZEOConnection, name=event.object.name) + + +@subscriber(IObjectRemovedEvent, context_selector=IZEOConnection) +def handle_removed_connection(event): + """Un-register ZEO connection when deleted""" + manager = event.oldParent + manager.unregisterUtility(event.object, IZEOConnection, name=event.object.name) + + +@provider(IVocabularyFactory) +class ZEOConnectionVocabulary(UtilityVocabulary): + """ZEO connections vocabulary""" + + interface = IZEOConnection + nameOnly = True + +getVocabularyRegistry().register('PyAMS ZEO connections', ZEOConnectionVocabulary)