From 01487db6888eedbeaf9b249cd6d8e3cebce2bdf0 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Tue, 1 Dec 2009 20:36:59 +0100 Subject: [PATCH] Added a SAP connector and integrated the test system with coverage.py --- bin/asksap.py | 87 +++++++++++++ bin/generate.py | 116 +++++++++++++++++ gen/generator.py | 110 ----------------- gen/plone25/mixins/TestMixin.py | 72 ++++++++++- gen/plone25/skin/macros.pt | 2 +- gen/plone25/templates/Styles.css.dtml | 2 +- gen/plone25/wrappers/ToolWrapper.py | 5 +- shared/sap.py | 171 ++++++++++++++++++++++++++ 8 files changed, 450 insertions(+), 115 deletions(-) create mode 100644 bin/asksap.py create mode 100644 bin/generate.py create mode 100644 shared/sap.py diff --git a/bin/asksap.py b/bin/asksap.py new file mode 100644 index 0000000..1fc4ffa --- /dev/null +++ b/bin/asksap.py @@ -0,0 +1,87 @@ +'''This script allows to get information about a given SAP RFC function + module.''' + +# ------------------------------------------------------------------------------ +import sys, getpass +from optparse import OptionParser +from appy.shared.sap import Sap, SapError + +# ------------------------------------------------------------------------------ +WRONG_NG_OF_ARGS = 'Wrong number of arguments.' +ERROR_CODE = 1 +P_OPTION = 'The password related to SAP user.' +G_OPTION = 'The name of a SAP group of functions' + +# ------------------------------------------------------------------------------ +class AskSap: + '''This script allows to get information about a given RCF function module + exposed by a distant SAP system. + + usage: %prog [options] host sysnr client user functionName + + "host" is the server name or IP address where SAP runs. + "sysnr" is the SAP system/gateway number (example: 0) + "client" is the SAP client number (example: 040) + "user" is a valid SAP login + "sapElement" is the name of a SAP function (the default) or a given + group of functions (if option -g is given). If -g is + specified, sapElement can be "_all_" and all functions of + all groups are shown. + Examples + -------- + 1) Retrieve info about the function named "ZFX": + python asksap.py 127.0.0.1 0 040 USERGDY ZFX -p passwd + + 2) Retrieve info about group of functions "Z_API": + python asksap.py 127.0.0.1 0 040 USERGDY Z_API -p passwd -g + + 3) Retrieve info about all functions in all groups: + python asksap.py 127.0.0.1 0 040 USERGDY _all_ -p passwd -g + ''' + def manageArgs(self, parser, options, args): + # Check number of args + if len(args) != 5: + print WRONG_NG_OF_ARGS + parser.print_help() + sys.exit(ERROR_CODE) + + def run(self): + optParser = OptionParser(usage=AskSap.__doc__) + optParser.add_option("-p", "--password", action='store', type='string', + dest='password', default='', help=P_OPTION) + optParser.add_option("-g", "--group", action='store_true', + dest='isGroup', default='', help=G_OPTION) + (options, args) = optParser.parse_args() + try: + self.manageArgs(optParser, options, args) + # Ask the password, if it was not given as an option. + password = options.password + if not password: + password = getpass.getpass('Password for the SAP user: ') + connectionParams = args[:4] + [password] + print 'Connecting to SAP...' + sap = Sap(*connectionParams) + sap.connect() + print 'Connected.' + sapElement = args[4] + if options.isGroup: + # Returns info about the functions available in this group of + # functions. + info = sap.getGroupInfo(sapElement) + prefix = 'Group' + else: + # Return info about a given function. + info = sap.getFunctionInfo(sapElement) + prefix = 'Function' + print '%s: %s' % (prefix, sapElement) + print info + sap.disconnect() + except SapError, se: + sys.stderr.write(str(se)) + sys.stderr.write('\n') + sys.exit(ERROR_CODE) + +# ------------------------------------------------------------------------------ +if __name__ == '__main__': + AskSap().run() +# ------------------------------------------------------------------------------ diff --git a/bin/generate.py b/bin/generate.py new file mode 100644 index 0000000..b46d813 --- /dev/null +++ b/bin/generate.py @@ -0,0 +1,116 @@ +'''This script allows to generate a product from a Appy application.''' + +# ------------------------------------------------------------------------------ +import sys, os.path +from optparse import OptionParser +from appy.gen.generator import GeneratorError + +# ------------------------------------------------------------------------------ +ERROR_CODE = 1 +VALID_PRODUCT_TYPES = ('plone25', 'odt') +APP_NOT_FOUND = 'Application not found at %s.' +WRONG_NG_OF_ARGS = 'Wrong number of arguments.' +WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.' +PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \ + 'following: %s' % str(VALID_PRODUCT_TYPES) +C_OPTION = 'Removes from i18n files all labels that are not automatically ' \ + 'generated from your gen-application. It can be useful during ' \ + 'development, when you do lots of name changes (classes, ' \ + 'attributes, states, transitions, etc): in this case, the Appy ' \ + 'i18n label generation machinery produces lots of labels that ' \ + 'then become obsolete.' +S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \ + 'generated i18n files, you will find first all labels ' \ + 'that are automatically generated by appy.gen, in some logical ' \ + 'order (ie: field-related labels appear together, in the order ' \ + 'they are declared in the gen-class). Then, if you have added ' \ + 'labels manually, they will appear afterwards. Sorting labels ' \ + 'may not be desired under development. Indeed, when no sorting ' \ + 'occurs, every time you add or modify a field, class, state, etc, ' \ + 'newly generated labels will all appear together at the end of ' \ + 'the file; so it will be easy to translate them all. When sorting ' \ + 'occurs, those elements may be spread at different places in the ' \ + 'i18n file. When the development is finished, it may be a good ' \ + 'idea to sort the labels to get a clean and logically ordered ' \ + 'set of translation files.' + +class GeneratorScript: + '''usage: %prog [options] app productType outputFolder + + "app" is the path to your Appy application, which may be a + Python module (= a file than ends with .py) or a Python + package (= a folder containing a file named __init__.py). + Your app may reside anywhere (but it needs to be + accessible by the underlying application server, ie Zope), + excepted within the generated product. Typically, if you + generate a Plone product, it may reside within + /lib/python, but not within the + generated product (typically stored in + /Products). + "productType" is the kind of product you want to generate + (currently, only "plone25" and 'odt' are supported; + in the near future, the "plone25" target will also produce + Plone 3-compliant code that will still work with + Plone 2.5). + "outputFolder" is the folder where the product will be generated. + For example, if you specify /my/output/folder for your + application /home/gde/MyApp.py, this script will create + a folder /my/output/folder/MyApp and put in it the + generated product. + + Example: generating a Plone product + ----------------------------------- + In your Zope instance named myZopeInstance, create a folder + "myZopeInstance/lib/python/MyApp". Create into it your Appy application + (we suppose here that it is a Python package, containing a __init__.py + file and other files). Then, chdir into this folder and type + "python /gen/generator.py . plone25 ../../../Products" and the + product will be generated in myZopeInstance/Products/MyApp. + "python" must refer to a Python interpreter that knows package appy.''' + + def generateProduct(self, options, application, productType, outputFolder): + exec 'from appy.gen.%s.generator import Generator' % productType + Generator(application, outputFolder, options).run() + + def manageArgs(self, parser, options, args): + # Check number of args + if len(args) != 3: + print WRONG_NG_OF_ARGS + parser.print_help() + sys.exit(ERROR_CODE) + # Check productType + if args[1] not in VALID_PRODUCT_TYPES: + print PRODUCT_TYPE_ERROR + sys.exit(ERROR_CODE) + # Check existence of application + if not os.path.exists(args[0]): + print APP_NOT_FOUND % args[0] + sys.exit(ERROR_CODE) + # Check existence of outputFolder basic type + if not os.path.exists(args[2]): + print WRONG_OUTPUT_FOLDER + sys.exit(ERROR_CODE) + # Convert all paths in absolute paths + for i in (0,2): + args[i] = os.path.abspath(args[i]) + def run(self): + optParser = OptionParser(usage=GeneratorScript.__doc__) + optParser.add_option("-c", "--i18n-clean", action='store_true', + dest='i18nClean', default=False, help=C_OPTION) + optParser.add_option("-s", "--i18n-sort", action='store_true', + dest='i18nSort', default=False, help=S_OPTION) + (options, args) = optParser.parse_args() + try: + self.manageArgs(optParser, options, args) + print 'Generating %s product in %s...' % (args[1], args[2]) + self.generateProduct(options, *args) + except GeneratorError, ge: + sys.stderr.write(str(ge)) + sys.stderr.write('\n') + optParser.print_help() + sys.exit(ERROR_CODE) + +# ------------------------------------------------------------------------------ +if __name__ == '__main__': + GeneratorScript().run() +# ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py index ec0530f..0179f57 100755 --- a/gen/generator.py +++ b/gen/generator.py @@ -1,6 +1,5 @@ # ------------------------------------------------------------------------------ import os, os.path, sys, parser, symbol, token -from optparse import OptionParser from appy.gen import Type, State, Config, Tool, Flavour from appy.gen.descriptors import * from appy.gen.utils import produceNiceMessage @@ -322,113 +321,4 @@ class Generator: for wfDescr in self.workflows: self.generateWorkflow(wfDescr) self.finalize() print 'Done.' - -# ------------------------------------------------------------------------------ -ERROR_CODE = 1 -VALID_PRODUCT_TYPES = ('plone25', 'odt') -APP_NOT_FOUND = 'Application not found at %s.' -WRONG_NG_OF_ARGS = 'Wrong number of arguments.' -WRONG_OUTPUT_FOLDER = 'Output folder not found. Please create it first.' -PRODUCT_TYPE_ERROR = 'Wrong product type. Product type may be one of the ' \ - 'following: %s' % str(VALID_PRODUCT_TYPES) -C_OPTION = 'Removes from i18n files all labels that are not automatically ' \ - 'generated from your gen-application. It can be useful during ' \ - 'development, when you do lots of name changes (classes, ' \ - 'attributes, states, transitions, etc): in this case, the Appy ' \ - 'i18n label generation machinery produces lots of labels that ' \ - 'then become obsolete.' -S_OPTION = 'Sorts all i18n labels. If you use this option, among the ' \ - 'generated i18n files, you will find first all labels ' \ - 'that are automatically generated by appy.gen, in some logical ' \ - 'order (ie: field-related labels appear together, in the order ' \ - 'they are declared in the gen-class). Then, if you have added ' \ - 'labels manually, they will appear afterwards. Sorting labels ' \ - 'may not be desired under development. Indeed, when no sorting ' \ - 'occurs, every time you add or modify a field, class, state, etc, ' \ - 'newly generated labels will all appear together at the end of ' \ - 'the file; so it will be easy to translate them all. When sorting ' \ - 'occurs, those elements may be spread at different places in the ' \ - 'i18n file. When the development is finished, it may be a good ' \ - 'idea to sort the labels to get a clean and logically ordered ' \ - 'set of translation files.' - -class GeneratorScript: - '''usage: %prog [options] app productType outputFolder - - "app" is the path to your Appy application, which may be a - Python module (= a file than ends with .py) or a Python - package (= a folder containing a file named __init__.py). - Your app may reside anywhere (but it needs to be - accessible by the underlying application server, ie Zope), - excepted within the generated product. Typically, if you - generate a Plone product, it may reside within - /lib/python, but not within the - generated product (typically stored in - /Products). - "productType" is the kind of product you want to generate - (currently, only "plone25" and 'odt' are supported; - in the near future, the "plone25" target will also produce - Plone 3-compliant code that will still work with - Plone 2.5). - "outputFolder" is the folder where the product will be generated. - For example, if you specify /my/output/folder for your - application /home/gde/MyApp.py, this script will create - a folder /my/output/folder/MyApp and put in it the - generated product. - - Example: generating a Plone product - ----------------------------------- - In your Zope instance named myZopeInstance, create a folder - "myZopeInstance/lib/python/MyApp". Create into it your Appy application - (we suppose here that it is a Python package, containing a __init__.py - file and other files). Then, chdir into this folder and type - "python /gen/generator.py . plone25 ../../../Products" and the - product will be generated in myZopeInstance/Products/MyApp. - "python" must refer to a Python interpreter that knows package appy.''' - - def generateProduct(self, options, application, productType, outputFolder): - exec 'from appy.gen.%s.generator import Generator' % productType - Generator(application, outputFolder, options).run() - - def manageArgs(self, parser, options, args): - # Check number of args - if len(args) != 3: - print WRONG_NG_OF_ARGS - parser.print_help() - sys.exit(ERROR_CODE) - # Check productType - if args[1] not in VALID_PRODUCT_TYPES: - print PRODUCT_TYPE_ERROR - sys.exit(ERROR_CODE) - # Check existence of application - if not os.path.exists(args[0]): - print APP_NOT_FOUND % args[0] - sys.exit(ERROR_CODE) - # Check existence of outputFolder basic type - if not os.path.exists(args[2]): - print WRONG_OUTPUT_FOLDER - sys.exit(ERROR_CODE) - # Convert all paths in absolute paths - for i in (0,2): - args[i] = os.path.abspath(args[i]) - def run(self): - optParser = OptionParser(usage=GeneratorScript.__doc__) - optParser.add_option("-c", "--i18n-clean", action='store_true', - dest='i18nClean', default=False, help=C_OPTION) - optParser.add_option("-s", "--i18n-sort", action='store_true', - dest='i18nSort', default=False, help=S_OPTION) - (options, args) = optParser.parse_args() - try: - self.manageArgs(optParser, options, args) - print 'Generating %s product in %s...' % (args[1], args[2]) - self.generateProduct(options, *args) - except GeneratorError, ge: - sys.stderr.write(str(ge)) - sys.stderr.write('\n') - optParser.print_help() - sys.exit(ERROR_CODE) - -# ------------------------------------------------------------------------------ -if __name__ == '__main__': - GeneratorScript().run() # ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/TestMixin.py b/gen/plone25/mixins/TestMixin.py index 87534c2..b305911 100644 --- a/gen/plone25/mixins/TestMixin.py +++ b/gen/plone25/mixins/TestMixin.py @@ -1,3 +1,6 @@ +# ------------------------------------------------------------------------------ +import os, os.path, sys + # ------------------------------------------------------------------------------ class TestMixin: '''This class is mixed in with any PloneTestCase.''' @@ -12,16 +15,81 @@ class TestMixin: self.logout() self.login(userId) + def getNonEmptySubModules(self, moduleName): + '''Returns the list fo sub-modules of p_app that are non-empty.''' + res = [] + try: + exec 'import %s' % moduleName + exec 'moduleObj = %s' % moduleName + moduleFile = moduleObj.__file__ + if moduleFile.endswith('.pyc'): + moduleFile = moduleFile[:-1] + except ImportError, ie: + return res + except SyntaxError, se: + return res + # Include the module if not empty. "Emptyness" is determined by the + # absence of names beginning with other chars than "__". + for elem in moduleObj.__dict__.iterkeys(): + if not elem.startswith('__'): + print 'Element found in this module!!!', moduleObj, elem + res.append(moduleObj) + break + # Include sub-modules if any + if moduleFile.find("__init__.py") != -1: + # Potentially, sub-modules exist. + moduleFolder = os.path.dirname(moduleFile) + for elem in os.listdir(moduleFolder): + if elem.startswith('.'): continue + subModuleName, ext = os.path.splitext(elem) + if ((ext == '.py') and (subModuleName != '__init__')) or \ + os.path.isdir(os.path.join(moduleFolder, subModuleName)): + # Submodules may be sub-folders or Python files + subModuleName = '%s.%s' % (moduleName, subModuleName) + res += self.getNonEmptySubModules(subModuleName) + return res + + def getCovFolder(self): + '''Returns the folder where to put the coverage folder if needed.''' + for arg in sys.argv: + if arg.startswith('[coverage'): + return arg[10:].strip(']') + return None + # Functions executed before and after every test ------------------------------- def beforeTest(test): + '''Is executed before every test.''' g = test.globs g['tool'] = test.app.plone.get('portal_%s' % g['appName'].lower()).appy() - g['appFolder'] = g['tool'].o.getProductConfig().diskFolder + cfg = g['tool'].o.getProductConfig() + g['appFolder'] = cfg.diskFolder moduleOrClassName = g['test'].name # Not used yet. # Initialize the test test.createUser('admin', ('Member','Manager')) test.login('admin') g['t'] = g['test'] + # Must we perform test coverage ? + covFolder = test.getCovFolder() + if covFolder: + try: + print 'COV!!!!', covFolder + import coverage + app = getattr(cfg, g['tool'].o.getAppName()) + from coverage import coverage + cov = coverage() + g['cov'] = cov + g['covFolder'] = covFolder + cov.start() + except ImportError: + print 'You must install the "coverage" product.' -def afterTest(test): pass +def afterTest(test): + '''Is executed after every test.''' + g = test.globs + if g.has_key('covFolder'): + cov = g['cov'] + cov.stop() + # Dumps the coverage report + appModules = test.getNonEmptySubModules(g['tool'].o.getAppName()) + cov.html_report(directory=g['covFolder'], morfs=appModules) # ------------------------------------------------------------------------------ diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/macros.pt index 7a10b03..a3a5248 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/macros.pt @@ -190,7 +190,7 @@ - + diff --git a/gen/plone25/templates/Styles.css.dtml b/gen/plone25/templates/Styles.css.dtml index 8506da0..b42f7c0 100644 --- a/gen/plone25/templates/Styles.css.dtml +++ b/gen/plone25/templates/Styles.css.dtml @@ -7,7 +7,7 @@ } .appyList { - line-height: 0; + line-height: 1.1em; margin: 0 0 0.5em 1.2em; padding: 0; } diff --git a/gen/plone25/wrappers/ToolWrapper.py b/gen/plone25/wrappers/ToolWrapper.py index 10145ac..f1ee015 100644 --- a/gen/plone25/wrappers/ToolWrapper.py +++ b/gen/plone25/wrappers/ToolWrapper.py @@ -1,6 +1,5 @@ # ------------------------------------------------------------------------------ class ToolWrapper: - def getInitiator(self): '''Retrieves the object that triggered the creation of the object being currently created (if any).''' @@ -13,4 +12,8 @@ class ToolWrapper: def getObject(self, uid): '''Allow to retrieve an object from its unique identifier p_uid.''' return self.o.getObject(uid, appy=True) + + def getDiskFolder(self): + '''Returns the disk folder where the Appy application is stored.''' + return self.o.getProductConfig().diskFolder # ------------------------------------------------------------------------------ diff --git a/shared/sap.py b/shared/sap.py new file mode 100644 index 0000000..71a3b41 --- /dev/null +++ b/shared/sap.py @@ -0,0 +1,171 @@ +'''This module allows to call RFC functions exposed by a distant SAP system. + It requires the "pysap" module available at http://pysaprfc.sourceforge.net + and the library librfccm.so that one can download from the "SAP MarketPlace", + a website by SAP requiring a login/password.''' + +# ------------------------------------------------------------------------------ +from appy.gen.utils import sequenceTypes + +class SapError(Exception): pass +SAP_MODULE_ERROR = 'Module pysap was not found (you can get it at ' \ + 'http://pysaprfc.sourceforge.net)' +SAP_CONNECT_ERROR = 'Error while connecting to SAP (conn_string: %s). %s' +SAP_FUNCTION_ERROR = 'Error while calling function "%s". %s' +SAP_DISCONNECT_ERROR = 'Error while disconnecting from SAP. %s' +SAP_TABLE_PARAM_ERROR = 'Param "%s" does not correspond to a valid table ' \ + 'parameter for function "%s".' +SAP_FUNCTION_NOT_FOUND = 'Function "%s" does not exist.' +SAP_FUNCTION_INFO_ERROR = 'Error while asking information about function ' \ + '"%s". %s' +SAP_GROUP_NOT_FOUND = 'Group of functions "%s" does not exist or is empty.' + +# Is the pysap module present or not ? +hasSap = True +try: + import pysap +except ImportError: + hasSap = False + +# ------------------------------------------------------------------------------ +class Sap: + '''Represents a remote SAP system. This class allows to connect to a distant + SAP system and perform RFC calls.''' + def __init__(self, host, sysnr, client, user, password): + self.host = host # Hostname or IP address of SAP server + self.sysnr = sysnr # The system number of SAP server/gateway + self.client = client # The instance/client number + self.user = user + self.password = password + self.sap = None # Will hold the handler to the SAP distant system. + if not hasSap: raise SapError(SAP_MODULE_ERROR) + + def connect(self): + '''Connects to the SAP system.''' + params = 'ASHOST=%s SYSNR=%s CLIENT=%s USER=%s PASSWD=%s' % (self.host, + self.sysnr, self.client, self.user, self.password) + try: + self.sap = pysap.Rfc_connection(conn_string = params) + self.sap.open() + except pysap.BaseSapRfcError, se: + # Put in the error message the connection string without the + # password. + connNoPasswd = params[:params.index('PASSWD')] + 'PASSWD=********' + raise SapError(SAP_CONNECT_ERROR % (connNoPasswd, str(se))) + + def call(self, functionName, **params): + '''Calls a function on the SAP server.''' + try: + function = self.sap.get_interface(functionName) + # Specify the parameters + for name, value in params.iteritems(): + if type(value) == dict: + # The param corresponds to a SAP/C "struct" + v = self.sap.get_structure(name)() + v.from_dict(value) + elif type(value) in sequenceTypes: + # The param must be a SAP/C "table" (a list of structs) + # Retrieve the name of the struct type related to this + # table. + fDesc = self.sap.get_interface_desc(functionName) + tableTypeName = '' + for tDesc in fDesc.tables: + if tDesc.name == name: + # We have found the correct table param + tableTypeName = tDesc.field_def + break + if not tableTypeName: + raise SapError(\ + SAP_TABLE_PARAM_ERROR % (name, functionName)) + v = self.sap.get_table(tableTypeName) + for dValue in value: + v.append_from_dict(dValue) + #v = v.handle + else: + v = value + function[name] = v + # Call the function + function() + except pysap.BaseSapRfcError, se: + raise SapError(SAP_FUNCTION_ERROR % (functionName, str(se))) + + def getTypeInfo(self, typeName): + '''Returns information about the type (structure) named p_typeName.''' + res = '' + tInfo = self.sap.get_structure(typeName) + for fName, fieldType in tInfo._fields_: + res += ' %s: %s (%s)\n' % (fName, tInfo.sap_def(fName), + tInfo.sap_type(fName)) + return res + + def getFunctionInfo(self, functionName): + '''Returns information about the RFC function named p_functionName.''' + try: + res = '' + usedTypes = set() # Names of type definitions used in parameters. + fDesc = self.sap.get_interface_desc(functionName) + functionDescr = str(fDesc).strip() + if functionDescr: res += functionDescr + # Import parameters + if fDesc.imports: + res += '\nIMPORTS\n' + for iDesc in fDesc.imports: + res += ' %s\n' % str(iDesc) + usedTypes.add(iDesc.field_def) + # Export parameters + if fDesc.exports: + res += '\nEXPORTS\n' + for eDesc in fDesc.exports: + res += ' %s\n' % str(eDesc) + usedTypes.add(eDesc.field_def) + if fDesc.tables: + res += '\nTABLES\n' + for tDesc in fDesc.tables: + res += ' %s\n' % str(tDesc) + usedTypes.add(tDesc.field_def) + if fDesc.exceptions: + res += '\nEXCEPTIONS\n' + for eDesc in fDesc.exceptions: + res += ' %s\n' % str(eDesc) + # Add information about used types + if usedTypes: + res += '\nTypes used by the parameters:\n' + for typeName in usedTypes: + # Dump info only if it is a structure, not a simple type + try: + self.sap.get_structure(typeName) + res += '%s\n%s\n\n' % \ + (typeName, self.getTypeInfo(typeName)) + except pysap.BaseSapRfcError, ee: + pass + return res + except pysap.BaseSapRfcError, se: + if se.value == 'FU_NOT_FOUND': + raise SapError(SAP_FUNCTION_NOT_FOUND % (functionName)) + else: + raise SapError(SAP_FUNCTION_INFO_ERROR % (functionName,str(se))) + + def getGroupInfo(self, groupName): + '''Gets information about the functions that are available in group of + functions p_groupName.''' + if groupName == '_all_': + # Search everything. + functions = self.sap.search_functions('*') + else: + functions = self.sap.search_functions('*', grpname=groupName) + if not functions: + raise SapError(SAP_GROUP_NOT_FOUND % (groupName)) + res = 'Available functions:\n' + for f in functions: + res += ' %s' % f.funcname + if groupName == '_all_': + res += ' (group: %s)' % f.groupname + res += '\n' + return res + + def disconnect(self): + '''Disconnects from SAP.''' + try: + self.sap.close() + except pysap.BaseSapRfcError, se: + raise SapError(SAP_DISCONNECT_ERROR % str(se)) +# ------------------------------------------------------------------------------