- Feature: Implemented GenericSetup import/export handlers and registered

import/export steps.


git-svn-id: http://svn.dataflake.org/svn/Products.LDAPMultiPlugins/trunk@1675 835909ba-7c00-0410-bfa4-884f43845301
This commit is contained in:
jens 2008-12-19 07:15:06 +00:00
parent e369705dd6
commit 6cba8ae8fb
12 changed files with 529 additions and 45 deletions

View File

@ -1,12 +1,19 @@
##############################################################################
#
# ActiveDirectoryMultiPlugin Shim to use the LDAPUserFolder with the
# PluggableAuthenticationService w/ AD
# Copyright (c) Jens Vagelpohl 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.
#
##############################################################################
""" ActiveDirectoryUserFolder shim module
__doc__ = """ ActiveDirectoryUserFolder shim module """
__version__ = '$Revision$'[11:-2]
$Id$
"""
# General Python imports
import logging

View File

@ -6,6 +6,9 @@ To see earlier changes please see HISTORY.txt.
1.8 (unreleased)
----------------
- Feature: Implemented GenericSetup import/export handlers and registered
import/export steps.
- Bug: Fixed the Zope dependency, which was listed as 2.8+. It's 2.9+.

View File

@ -1,15 +1,19 @@
##############################################################################
#
# LDAPMultiPlugin Shim to use the LDAPUserFolder with the
# PluggableAuthenticationService
# Copyright (c) Jens Vagelpohl and Contributors. All Rights Reserved.
#
# This software is governed by a license. See
# LICENSE.txt for the terms of this license.
# 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.
#
##############################################################################
""" LDAPMultiPlugin, a LDAP-enabled PluggableAuthSErvice plugin
__doc__ = """ LDAPUserFolder shim module """
__version__ = '$Revision$'[11:-2]
$Id$
"""
# General Python imports
import logging

View File

@ -1,30 +1,42 @@
##############################################################################
#
# LDAPPluginBase Base class for LDAP-based PAS-Plugins
# Copyright (c) Jens Vagelpohl 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.
#
##############################################################################
""" Base class for LDAPMultiPlugins-based PAS plugins
__doc__ = """ LDAPPluginBase module """
__version__ = '$Revision$'[11:-2]
$Id$
"""
# General Python imports
import copy
import logging
# Zope imports
from Acquisition import aq_base
from OFS.Folder import Folder
from OFS.Cache import Cacheable
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.SecurityManagement import getSecurityManager
from Acquisition import aq_base
from Globals import InitializeClass
from OFS.Cache import Cacheable
from OFS.Folder import Folder
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PluggableAuthService.interfaces.plugins import \
IAuthenticationPlugin, IRolesPlugin, ICredentialsResetPlugin, \
IPropertiesPlugin
IAuthenticationPlugin
from Products.PluggableAuthService.interfaces.plugins import \
ICredentialsResetPlugin
from Products.PluggableAuthService.interfaces.plugins import IPropertiesPlugin
from Products.PluggableAuthService.interfaces.plugins import IRolesPlugin
from Products.PluggableAuthService.utils import classImplements
from Products.LDAPMultiPlugins.interfaces import ILDAPMultiPlugin
logger = logging.getLogger('event.LDAPMultiPlugin')
@ -34,7 +46,8 @@ class LDAPPluginBase(Folder, BasePlugin, Cacheable):
security = ClassSecurityInfo()
manage_options = ( BasePlugin.manage_options[:1]
+ Folder.manage_options
+ Folder.manage_options [:1]
+ Folder.manage_options[2:]
+ Cacheable.manage_options
)
@ -184,6 +197,7 @@ classImplements( LDAPPluginBase
, ICredentialsResetPlugin
, IPropertiesPlugin
, IRolesPlugin
, ILDAPMultiPlugin
)
InitializeClass(LDAPPluginBase)

View File

@ -1,14 +1,19 @@
##############################################################################
#
# __init__.py Initialization code for the LDAP Multi Plugins
# Copyright (c) Jens Vagelpohl and Contributors. All Rights Reserved.
#
# This software is governed by a license. See
# LICENSE.txt for the terms of this license.
# 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.
#
##############################################################################
""" LDAPMultiPlugin product initialization
__doc__ = """ LDAPUserFolder shims initialization module """
__version__ = '$Revision$'[11:-2]
$Id$
"""
from AccessControl.Permissions import add_user_folders
from Products.PluggableAuthService.PluggableAuthService import \

View File

@ -0,0 +1,21 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
i18n_domain="ldapmultiplugins"
>
<adapter factory=".exportimport.LDAPMultiPluginXMLAdapter"/>
<genericsetup:importStep
name="ldapmultiplugins"
title="LDAPMultiPlugins"
description="Import LDAPMultiPlugins settings"
handler="Products.LDAPMultiPlugins.exportimport.importLDAPMultiPlugins"/>
<genericsetup:exportStep
name="ldapmultiplugins"
title="LDAPMultiPlugins"
description="Export LDAPMultiPlugins settings"
handler="Products.LDAPMultiPlugins.exportimport.exportLDAPMultiPlugins"/>
</configure>

View File

@ -0,0 +1,105 @@
##############################################################################
#
# Copyright (c) 2000-2008 Jens Vagelpohl 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.
#
##############################################################################
""" LDAPMultiPlugins GenericSetup support
$Id$
"""
from zope.component import adapts
from Products.GenericSetup.interfaces import ISetupEnviron
from Products.GenericSetup.utils import exportObjects
from Products.GenericSetup.utils import importObjects
from Products.GenericSetup.utils import ObjectManagerHelpers
from Products.GenericSetup.utils import PropertyManagerHelpers
from Products.GenericSetup.utils import XMLAdapterBase
from Products.LDAPMultiPlugins.interfaces import ILDAPMultiPlugin
class LDAPMultiPluginXMLAdapter( XMLAdapterBase
, ObjectManagerHelpers
, PropertyManagerHelpers
):
""" Export/import LDAPMultiPlugins plugins
"""
adapts(ILDAPMultiPlugin, ISetupEnviron)
_LOGGER_ID = 'ldapmultiplugins'
def _exportNode(self):
""" Export the object as a DOM node.
"""
node = self._getObjectNode('object')
node.appendChild(self._extractProperties())
node.appendChild(self._extractObjects())
self._logger.info('LDAPMultiPlugin exported.')
return node
def _importNode(self, node):
""" Import the object from the DOM node.
"""
if self.environ.shouldPurge():
self._purgeProperties()
self._purgeObjects()
self._initProperties(node)
self._initObjects(node)
self._logger.info('LDAPMultiPlugin imported.')
def _exportBody(self):
""" Export the object as a file body.
"""
if not ILDAPMultiPlugin.providedBy(self.context):
return None
return XMLAdapterBase._exportBody(self)
body = property(_exportBody, XMLAdapterBase._importBody)
def importLDAPMultiPlugins(context):
""" Import LDAPMultiPlugin settings from an XML file
When using this step directly, the setup tool is expected to be
inside the PluggableAuthService object
"""
pas = context.getSite()
ldapmultiplugins = [ x for x in context.getSite().objectValues()
if ILDAPMultiPlugin.providedBy(x) ]
if not ldapmultiplugins:
context.getLogger('ldapmultiplugins').debug('Nothing to export.')
for plugin in ldapmultiplugins:
importObjects(plugin, '', context)
def exportLDAPMultiPlugins(context):
""" Export LDAPMultiPlugin settings to an XML file
When using this step directly, the setup tool is expected to be
inside the PluggableAuthService object
"""
pas = context.getSite()
ldapmultiplugins = [ x for x in context.getSite().objectValues()
if ILDAPMultiPlugin.providedBy(x) ]
if not ldapmultiplugins:
context.getLogger('ldapmultiplugins').debug('Nothing to export.')
for plugin in ldapmultiplugins:
exportObjects(plugin, '', context)

View File

@ -0,0 +1,23 @@
##############################################################################
#
# Copyright (c) Jens Vagelpohl 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.
#
##############################################################################
""" Interfaces for LDAPMultiPlugins package classes
$Id$
"""
from zope.interface import Interface
class ILDAPMultiPlugin(Interface):
""" Marker interface for the LDAPMultiPlugins plugin classes
"""

View File

@ -1 +1 @@
""" This space intentionally left blank """
# package me

View File

@ -0,0 +1,63 @@
##############################################################################
#
# Copyright (c) Jens Vagelpohl 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.
#
##############################################################################
""" Test helper classes
$Id$
"""
from OFS.Folder import Folder
from Products.Five import zcml
from Products.GenericSetup.testing import BodyAdapterTestCase
from Products.GenericSetup.testing import ExportImportZCMLLayer
from Products.GenericSetup.tests.common import BaseRegistryTests
class LMPXMLAdapterTestsBase(BodyAdapterTestCase):
layer = ExportImportZCMLLayer
def _getTargetClass(self):
from Products.LDAPMultiPlugins.exportimport \
import LDAPMultiPluginXMLAdapter
return LDAPMultiPluginXMLAdapter
def setUp(self):
import Products.LDAPMultiPlugins
import Products.LDAPUserFolder
BodyAdapterTestCase.setUp(self)
try:
import Products.CMFCore
zcml.load_config('meta.zcml', Products.CMFCore)
except ImportError:
pass
zcml.load_config('configure.zcml', Products.LDAPUserFolder)
zcml.load_config('configure.zcml', Products.LDAPMultiPlugins)
class _LDAPMultiPluginsSetup(BaseRegistryTests):
layer = ExportImportZCMLLayer
def _initSite(self, use_changed=False):
self.root.site = Folder(id='site')
site = self.root.site
site._setObject('tested',self._getTargetClass()('tested'))
if use_changed:
self._edit()
return site

View File

@ -1,36 +1,45 @@
#####################################################################
##############################################################################
#
# test_LDAPMultiPlugins.py
# Copyright (c) Jens Vagelpohl and Contributors. All Rights Reserved.
#
# This software is governed by a license. See
# LICENSE.txt for the terms of this license.
# 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.
#
#####################################################################
""" Unit tests for LDAPMultiPlugin and ActiveDirectoryMultiPlugin
##############################################################################
""" LDAPMultiPlugin and ActiveDirectoryMultiPlugin unit tests
$Id$
"""
from unittest import main
from unittest import makeSuite
from unittest import TestSuite
from unittest import TestCase
import unittest
from Products.PluggableAuthService.interfaces.plugins import \
IUserEnumerationPlugin, IGroupsPlugin, IGroupEnumerationPlugin, \
IRoleEnumerationPlugin
IUserEnumerationPlugin
from Products.PluggableAuthService.interfaces.plugins import \
IGroupsPlugin
from Products.PluggableAuthService.interfaces.plugins import \
IGroupEnumerationPlugin
from Products.PluggableAuthService.interfaces.plugins import \
IRoleEnumerationPlugin
from Products.LDAPMultiPlugins.interfaces import ILDAPMultiPlugin
class LMPBaseTests(TestCase):
class LMPBaseTests(unittest.TestCase):
def _getTargetClass(self):
from Products.LDAPMultiPlugins.LDAPPluginBase import LDAPPluginBase
return LDAPPluginBase
def test_interfaces(self):
from zope.interface.verify import verifyClass
verifyClass(ILDAPMultiPlugin, self._getTargetClass())
verifyClass(IUserEnumerationPlugin, self._getTargetClass())
verifyClass(IGroupsPlugin, self._getTargetClass())
verifyClass(IGroupEnumerationPlugin, self._getTargetClass())
@ -52,12 +61,12 @@ class LMPTests(LMPBaseTests):
return LDAPMultiPlugin
def test_suite():
return TestSuite((
makeSuite( ADMPTests ),
makeSuite( LMPTests ),
return unittest.TestSuite((
unittest.makeSuite(ADMPTests),
unittest.makeSuite(LMPTests),
))
if __name__ == '__main__':
main(defaultTest='test_suite')
unittest.main(defaultTest='test_suite')

View File

@ -0,0 +1,230 @@
##############################################################################
#
# Copyright (c) Jens Vagelpohl 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.
#
##############################################################################
""" Export/import tests
$Id$
"""
import unittest
from Products.GenericSetup.tests.common import DummyExportContext
from Products.GenericSetup.tests.common import DummyImportContext
from Products.LDAPMultiPlugins.tests.base import _LDAPMultiPluginsSetup
from Products.LDAPMultiPlugins.tests.base import LMPXMLAdapterTestsBase
class LDAPMultiPluginXMLAdapterTests(LMPXMLAdapterTestsBase):
def setUp(self):
from Products.LDAPMultiPlugins.LDAPMultiPlugin import LDAPMultiPlugin
LMPXMLAdapterTestsBase.setUp(self)
self._obj = LDAPMultiPlugin('tested')
self._BODY = _LDAPMULTIPLUGIN_BODY
class ActiveDirectoryMultiPluginXMLAdapterTests(LMPXMLAdapterTestsBase):
def setUp(self):
from Products.LDAPMultiPlugins.ActiveDirectoryMultiPlugin import \
ActiveDirectoryMultiPlugin
LMPXMLAdapterTestsBase.setUp(self)
self._obj = ActiveDirectoryMultiPlugin('tested')
self._BODY = _ACTIVEDIRECTORYMULTIPLUGIN_BODY
class LDAPMultiPluginExportTests(_LDAPMultiPluginsSetup):
def _getTargetClass(self):
from Products.LDAPMultiPlugins.LDAPMultiPlugin import LDAPMultiPlugin
return LDAPMultiPlugin
def _edit(self):
plugin = self.root.site.tested
plugin.title = 'Plugin Title'
plugin.prefix = 'plugin_prefix'
def test_unchanged(self):
from Products.LDAPMultiPlugins.exportimport import \
exportLDAPMultiPlugins
site = self._initSite(use_changed=False)
context = DummyExportContext(site)
exportLDAPMultiPlugins(context)
self.assertEqual(len(context._wrote), 1)
filename, text, content_type = context._wrote[0]
self.assertEqual(filename, 'tested.xml')
self._compareDOM(text, _LDAPMULTIPLUGIN_BODY)
self.assertEqual(content_type, 'text/xml')
def test_changed(self):
from Products.LDAPMultiPlugins.exportimport import \
exportLDAPMultiPlugins
site = self._initSite(use_changed=True)
context = DummyExportContext(site)
exportLDAPMultiPlugins(context)
self.assertEqual(len(context._wrote), 1)
filename, text, content_type = context._wrote[0]
self.assertEqual(filename, 'tested.xml')
self._compareDOM(text, _CHANGED_LMP_EXPORT)
self.assertEqual(content_type, 'text/xml')
class ADMultiPluginExportTests(_LDAPMultiPluginsSetup):
def _getTargetClass(self):
from Products.LDAPMultiPlugins.ActiveDirectoryMultiPlugin import \
ActiveDirectoryMultiPlugin
return ActiveDirectoryMultiPlugin
def _edit(self):
plugin = self.root.site.tested
plugin.title = 'Plugin Title'
plugin.prefix = 'plugin_prefix'
plugin.groupid_attr = 'cn'
plugin.grouptitle_attr = 'sn'
plugin.group_class = 'groupOfNames'
plugin.group_recurse = 0
plugin.group_recurse_depth = 0
def test_unchanged(self):
from Products.LDAPMultiPlugins.exportimport import \
exportLDAPMultiPlugins
site = self._initSite(use_changed=False)
context = DummyExportContext(site)
exportLDAPMultiPlugins(context)
self.assertEqual(len(context._wrote), 1)
filename, text, content_type = context._wrote[0]
self.assertEqual(filename, 'tested.xml')
self._compareDOM(text, _ACTIVEDIRECTORYMULTIPLUGIN_BODY)
self.assertEqual(content_type, 'text/xml')
def test_changed(self):
from Products.LDAPMultiPlugins.exportimport import \
exportLDAPMultiPlugins
site = self._initSite(use_changed=True)
context = DummyExportContext(site)
exportLDAPMultiPlugins(context)
self.assertEqual(len(context._wrote), 1)
filename, text, content_type = context._wrote[0]
self.assertEqual(filename, 'tested.xml')
self._compareDOM(text, _CHANGED_AD_EXPORT)
self.assertEqual(content_type, 'text/xml')
class LDAPMultiPluginImportTests(_LDAPMultiPluginsSetup):
def _getTargetClass(self):
from Products.LDAPMultiPlugins.LDAPMultiPlugin import LDAPMultiPlugin
return LDAPMultiPlugin
def test_normal(self):
from Products.LDAPMultiPlugins.exportimport import \
importLDAPMultiPlugins
site = self._initSite()
plugin = site.tested
context = DummyImportContext(site)
context._files['tested.xml'] = _CHANGED_LMP_EXPORT
importLDAPMultiPlugins(context)
self.assertEquals(plugin.title, 'Plugin Title')
self.assertEquals(plugin.prefix, 'plugin_prefix')
class ADMultiPluginImportTests(_LDAPMultiPluginsSetup):
def _getTargetClass(self):
from Products.LDAPMultiPlugins.ActiveDirectoryMultiPlugin import \
ActiveDirectoryMultiPlugin
return ActiveDirectoryMultiPlugin
def test_normal(self):
from Products.LDAPMultiPlugins.exportimport import \
importLDAPMultiPlugins
site = self._initSite()
plugin = site.tested
context = DummyImportContext(site)
context._files['tested.xml'] = _CHANGED_AD_EXPORT
importLDAPMultiPlugins(context)
self.assertEquals(plugin.title, 'Plugin Title')
self.assertEquals(plugin.prefix, 'plugin_prefix')
self.assertEquals(plugin.groupid_attr, 'cn')
self.assertEquals(plugin.grouptitle_attr, 'sn')
self.assertEquals(plugin.group_class, 'groupOfNames')
self.assertEquals(plugin.group_recurse, 0)
self.assertEquals(plugin.group_recurse_depth, 0)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(LDAPMultiPluginXMLAdapterTests),
unittest.makeSuite(ActiveDirectoryMultiPluginXMLAdapterTests),
unittest.makeSuite(LDAPMultiPluginExportTests),
unittest.makeSuite(ADMultiPluginExportTests),
unittest.makeSuite(LDAPMultiPluginImportTests),
unittest.makeSuite(ADMultiPluginImportTests),
))
_LDAPMULTIPLUGIN_BODY = """\
<?xml version="1.0"?>
<object name="tested" meta_type="LDAP Multi Plugin">
<property name="prefix"></property>
<property name="title"></property>
</object>
"""
_ACTIVEDIRECTORYMULTIPLUGIN_BODY = """\
<?xml version="1.0"?>
<object name="tested" meta_type="ActiveDirectory Multi Plugin">
<property name="prefix"></property>
<property name="title"></property>
<property name="groupid_attr">objectGUID</property>
<property name="grouptitle_attr">cn</property>
<property name="group_class">group</property>
<property name="group_recurse">1</property>
<property name="group_recurse_depth">1</property>
</object>
"""
_CHANGED_LMP_EXPORT = """\
<?xml version="1.0"?>
<object name="tested" meta_type="LDAP Multi Plugin">
<property name="prefix">plugin_prefix</property>
<property name="title">Plugin Title</property>
</object>
"""
_CHANGED_AD_EXPORT = """\
<?xml version="1.0"?>
<object name="tested" meta_type="ActiveDirectory Multi Plugin">
<property name="prefix">plugin_prefix</property>
<property name="title">Plugin Title</property>
<property name="groupid_attr">cn</property>
<property name="grouptitle_attr">sn</property>
<property name="group_class">groupOfNames</property>
<property name="group_recurse">0</property>
<property name="group_recurse_depth">0</property>
</object>
"""