Added a SAP connector and integrated the test system with coverage.py

This commit is contained in:
Gaetan Delannay 2009-12-01 20:36:59 +01:00
parent b541ecb651
commit 01487db688
8 changed files with 450 additions and 115 deletions

87
bin/asksap.py Normal file
View file

@ -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()
# ------------------------------------------------------------------------------

116
bin/generate.py Normal file
View file

@ -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
<yourZopeInstance>/lib/python, but not within the
generated product (typically stored in
<yourZopeInstance>/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 <appyPath>/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()
# ------------------------------------------------------------------------------

View file

@ -1,6 +1,5 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import os, os.path, sys, parser, symbol, token import os, os.path, sys, parser, symbol, token
from optparse import OptionParser
from appy.gen import Type, State, Config, Tool, Flavour from appy.gen import Type, State, Config, Tool, Flavour
from appy.gen.descriptors import * from appy.gen.descriptors import *
from appy.gen.utils import produceNiceMessage from appy.gen.utils import produceNiceMessage
@ -322,113 +321,4 @@ class Generator:
for wfDescr in self.workflows: self.generateWorkflow(wfDescr) for wfDescr in self.workflows: self.generateWorkflow(wfDescr)
self.finalize() self.finalize()
print 'Done.' 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
<yourZopeInstance>/lib/python, but not within the
generated product (typically stored in
<yourZopeInstance>/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 <appyPath>/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()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -1,3 +1,6 @@
# ------------------------------------------------------------------------------
import os, os.path, sys
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class TestMixin: class TestMixin:
'''This class is mixed in with any PloneTestCase.''' '''This class is mixed in with any PloneTestCase.'''
@ -12,16 +15,81 @@ class TestMixin:
self.logout() self.logout()
self.login(userId) 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 ------------------------------- # Functions executed before and after every test -------------------------------
def beforeTest(test): def beforeTest(test):
'''Is executed before every test.'''
g = test.globs g = test.globs
g['tool'] = test.app.plone.get('portal_%s' % g['appName'].lower()).appy() 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. moduleOrClassName = g['test'].name # Not used yet.
# Initialize the test # Initialize the test
test.createUser('admin', ('Member','Manager')) test.createUser('admin', ('Member','Manager'))
test.login('admin') test.login('admin')
g['t'] = g['test'] 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)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -190,7 +190,7 @@
</tal:editField> </tal:editField>
<tal:viewField tal:condition="not: isEdit"> <tal:viewField tal:condition="not: isEdit">
<tal:fileField condition="python: (appyType['type'] == 'File')"> <tal:fileField condition="python: (appyType['type'] == 'File')">
<span tal:condition="showLabel" tal:content="label"></span> <span tal:condition="showLabel" tal:content="label" class="appyLabel"></span>
<metal:viewField use-macro="python: contextObj.widget(field.getName(), 'view', use_label=0)"/> <metal:viewField use-macro="python: contextObj.widget(field.getName(), 'view', use_label=0)"/>
</tal:fileField> </tal:fileField>
<tal:date condition="python: appyType['type'] == 'Date'"> <tal:date condition="python: appyType['type'] == 'Date'">

View file

@ -7,7 +7,7 @@
} }
.appyList { .appyList {
line-height: 0; line-height: 1.1em;
margin: 0 0 0.5em 1.2em; margin: 0 0 0.5em 1.2em;
padding: 0; padding: 0;
} }

View file

@ -1,6 +1,5 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class ToolWrapper: class ToolWrapper:
def getInitiator(self): def getInitiator(self):
'''Retrieves the object that triggered the creation of the object '''Retrieves the object that triggered the creation of the object
being currently created (if any).''' being currently created (if any).'''
@ -13,4 +12,8 @@ class ToolWrapper:
def getObject(self, uid): def getObject(self, uid):
'''Allow to retrieve an object from its unique identifier p_uid.''' '''Allow to retrieve an object from its unique identifier p_uid.'''
return self.o.getObject(uid, appy=True) 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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

171
shared/sap.py Normal file
View file

@ -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))
# ------------------------------------------------------------------------------