Added ZEO connection utility
authorThierry Florac <thierry.florac@onf.fr>
Thu, 05 Mar 2015 16:44:16 +0100
changeset 10 e87103c49c8a
parent 9 6a55c8cbced0
child 11 7693c14c352e
Added ZEO connection utility
src/pyams_utils/interfaces/zeo.py
src/pyams_utils/zmi/zeo.py
src/pyams_utils/zodb.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 <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface, Attribute
+from zope.schema import TextLine, Int, Password, Bool
+
+from pyams_utils import _
+
+
+class IZEOConnection(Interface):
+    """ZEO connection settings interface"""
+
+    name = TextLine(title=_("Connection name"),
+                    description=_("Registration name of ZEO connection"),
+                    required=True)
+
+    server_name = TextLine(title=_("ZEO server name"),
+                           description=_("Hostname of ZEO server"),
+                           required=True,
+                           default='localhost')
+
+    server_port = Int(title=_("ZEO server port"),
+                      description=_("Port number of ZEO server"),
+                      required=True,
+                      default=8100)
+
+    storage = TextLine(title=_("ZEO server storage"),
+                       description=_("Storage name on ZEO server"),
+                       required=True,
+                       default='1')
+
+    username = TextLine(title=_("ZEO user name"),
+                        description=_("User name on ZEO server"),
+                        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"""
--- /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 <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_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"""
--- 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)