The Resource object in shared/dav.py is now able to send SOAP requests, with marshalled and unmarshalled Python objects.

This commit is contained in:
Gaetan Delannay 2010-11-08 11:40:41 +01:00
parent ca6dd26906
commit 3d87036f85
5 changed files with 206 additions and 106 deletions

View file

@ -1,5 +1,9 @@
'''Appy allows you to create easily complete applications in Python.'''
# ------------------------------------------------------------------------------
import os.path
# ------------------------------------------------------------------------------
def getPath(): return os.path.dirname(__file__)
def versionIsGreaterThanOrEquals(version):
'''This method returns True if the current Appy version is greater than or
@ -12,3 +16,24 @@ def versionIsGreaterThanOrEquals(version):
paramVersion = [int(i) for i in version.split('.')]
currentVersion = [int(i) for i in appy.version.short.split('.')]
return currentVersion >= paramVersion
# ------------------------------------------------------------------------------
class Object:
'''At every place we need an object, but without any requirement on its
class (methods, attributes,...) we will use this minimalist class.'''
def __init__(self, **fields):
for k, v in fields.iteritems():
setattr(self, k, v)
def __repr__(self):
res = u'<Object '
for attrName, attrValue in self.__dict__.iteritems():
v = attrValue
if hasattr(v, '__repr__'):
v = v.__repr__()
try:
res += u'%s=%s ' % (attrName, v)
except UnicodeDecodeError:
res += u'%s=<encoding problem> ' % attrName
res = res.strip() + '>'
return res.encode('utf-8')
# ------------------------------------------------------------------------------

View file

@ -18,28 +18,9 @@ mimeTypesExts = {
'image/jpeg' : 'jpg',
'image/gif' : 'gif'
}
xmlPrologue = '<?xml version="1.0" encoding="utf-8"?>\n'
xmlPrologue = '<?xml version="1.0" encoding="utf-8" ?>\n'
# ------------------------------------------------------------------------------
class UnmarshalledObject:
'''Used for producing objects from a marshalled Python object (in some files
like a CSV file or an XML file).'''
def __init__(self, **fields):
for k, v in fields.iteritems():
setattr(self, k, v)
def __repr__(self):
res = u'<PythonObject '
for attrName, attrValue in self.__dict__.iteritems():
v = attrValue
if hasattr(v, '__repr__'):
v = v.__repr__()
try:
res += u'%s = %s ' % (attrName, v)
except UnicodeDecodeError:
res += u'%s = <encoding problem> ' % attrName
res = res.strip() + '>'
return res.encode('utf-8')
class UnmarshalledFile:
'''Used for producing file objects from a marshalled Python object.'''
def __init__(self):

View file

@ -17,7 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,USA.
# ------------------------------------------------------------------------------
from appy.shared import UnmarshalledObject
from appy import Object
# ------------------------------------------------------------------------------
WRONG_LINE = 'Line number %d in file %s does not have the right number of ' \
@ -61,9 +61,9 @@ class CsvParser:
# list): string, integer, float, boolean.
self.references = references
self.klass = klass # If a klass is given here, instead of creating
# UnmarshalledObject instances we will create instances of this class.
# Object instances we will create instances of this class.
# But be careful: we will not call the constructor of this class. We
# will simply create instances of UnmarshalledObject and dynamically
# will simply create instances of Object and dynamically
# change the class of created instances to this class.
def identifySeparator(self, line):
@ -125,10 +125,10 @@ class CsvParser:
def parse(self):
'''Parses the CSV file named self.fileName and creates a list of
corresponding Python objects (UnmarshalledObject instances). Among
object fields, some may be references. If it is the case, you may
specify in p_references a dict of referred objects. The parser will
then replace string values of some fields (which are supposed to be
corresponding Python objects (Object instances). Among object fields,
some may be references. If it is the case, you may specify in
p_references a dict of referred objects. The parser will then
replace string values of some fields (which are supposed to be
ids of referred objects) with corresponding objects in p_references.
How does this work? p_references must be a dictionary:
@ -154,7 +154,7 @@ class CsvParser:
firstLine = False
else:
# Add an object corresponding to this line.
lineObject = UnmarshalledObject()
lineObject = Object()
if self.klass:
lineObject.__class__ = self.klass
i = -1

View file

@ -4,11 +4,13 @@ from urllib import quote
from StringIO import StringIO
from mimetypes import guess_type
from base64 import encodestring
from appy import Object
from appy.shared.utils import copyData
from appy.gen.utils import sequenceTypes
from appy.shared.xml_parser import XmlUnmarshaller, XmlMarshaller
# ------------------------------------------------------------------------------
class DataEncoder:
class FormDataEncoder:
'''Allows to encode form data for sending it through a HTTP request.'''
def __init__(self, data):
self.data = data # The data to encode, as a dict
@ -34,6 +36,33 @@ class DataEncoder:
res.append(self.marshalValue(name, value))
return '&'.join(res)
# ------------------------------------------------------------------------------
class SoapDataEncoder:
'''Allows to encode SOAP data for sending it through a HTTP request.'''
namespaces = {'SOAP-ENV': 'http://schemas.xmlsoap.org/soap/envelope/',
'xsd' : 'http://www.w3.org/2001/XMLSchema',
'xsi' : 'http://www.w3.org/2001/XMLSchema-instance'}
namespacedTags = {'Envelope': 'SOAP-ENV', 'Body': 'SOAP-ENV', '*': 'py'}
def __init__(self, data, namespace='http://appyframework.org'):
self.data = data
# p_data can be:
# - a string already containing a complete SOAP message
# - a Python object, that we will convert to a SOAP message
# Define the namespaces for this request
self.ns = self.namespaces.copy()
self.ns['py'] = namespace
def encode(self):
# Do nothing if we have a SOAP message already
if isinstance(self.data, basestring): return self.data
# self.data is here a Python object. Wrap it a SOAP Body.
soap = Object(Body=self.data)
# Marshall it.
marshaller = XmlMarshaller(rootTag='Envelope', namespaces=self.ns,
namespacedTags=self.namespacedTags)
return marshaller.marshall(soap)
# ------------------------------------------------------------------------------
class HttpResponse:
'''Stores information about a HTTP response.'''
@ -42,13 +71,13 @@ class HttpResponse:
self.text = text # Textual description of the code
self.headers = headers # A dict-like object containing the headers
self.body = body # The body of the HTTP response
# p_duration, if given, is the time, in seconds, we have waited, before
# getting this response after having sent the request.
self.duration = duration
# The following attribute may contain specific data extracted from
# the previous fields. For example, when response if 302 (Redirect),
# self.data contains the URI where we must redirect the user to.
self.data = self.extractData()
# p_duration, if given, is the time, in seconds, we have waited, before
# getting this response after having sent the request.
self.duration = duration
def __repr__(self):
duration = ''
@ -58,9 +87,15 @@ class HttpResponse:
def extractData(self):
'''This method extracts, from the various parts of the HTTP response,
some useful information. For example, it will find the URI where to
redirect the user to if self.code is 302.'''
redirect the user to if self.code is 302, or will unmarshall XML
data into Python objects.'''
if self.code == 302:
return urlparse.urlparse(self.headers['location'])[2]
elif self.headers.has_key('content-type') and \
self.headers['content-type'].startswith('text/xml'):
# Return an unmarshalled version of the XML content, for easy use
# in Python.
return XmlUnmarshaller().parse(self.body)
# ------------------------------------------------------------------------------
urlRex = re.compile(r'http://([^:/]+)(:[0-9]+)?(/.+)?', re.I)
@ -109,7 +144,7 @@ class Resource:
headers['Accept'] = '*/*'
return headers
def sendRequest(self, method, uri, body=None, headers={}, bodyType=None):
def send(self, method, uri, body=None, headers={}, bodyType=None):
'''Sends a HTTP request with p_method, for p_uri.'''
conn = httplib.HTTP()
conn.connect(self.host, self.port)
@ -143,13 +178,13 @@ class Resource:
#body = '<d:propertyupdate xmlns:d="DAV:"><d:set><d:prop>' \
# '<d:displayname>%s</d:displayname></d:prop></d:set>' \
# '</d:propertyupdate>' % name
return self.sendRequest('MKCOL', folderUri)
return self.send('MKCOL', folderUri)
def delete(self, name):
'''Deletes a file or a folder (and all contained files if any) named
p_name within this resource.'''
toDeleteUri = self.uri + '/' + name
return self.sendRequest('DELETE', toDeleteUri)
return self.send('DELETE', toDeleteUri)
def add(self, content, type='fileName', name=''):
'''Adds a file in this resource. p_type can be:
@ -178,45 +213,44 @@ class Resource:
headers = {'Content-Length': str(size)}
if fileType: headers['Content-Type'] = fileType
if encoding: headers['Content-Encoding'] = encoding
res = self.sendRequest('PUT', fileUri, body, headers, bodyType=bodyType)
res = self.send('PUT', fileUri, body, headers, bodyType=bodyType)
# Close the file when relevant
if type =='fileName': body.close()
return res
def _encodeFormData(self, data):
'''Returns the encoded form p_data.'''
res = []
for name, value in data.items():
n = name.rfind( '__')
if n > 0:
tag = name[n+2:]
key = name[:n]
else: tag = 'string'
func = varfuncs.get(tag, marshal_string)
res.append(func(name, value))
return '&'.join(res)
def get(self, uri=None, headers={}):
'''Perform a HTTP GET on the server.'''
if not uri: uri = self.uri
return self.sendRequest('GET', uri, headers=headers)
return self.send('GET', uri, headers=headers)
def post(self, data=None, uri=None, headers={}, type='form'):
'''Perform a HTTP POST on the server. If p_type is:
- "form", p_data is a dict representing form data that will be
form-encoded;
- "soap", p_data is a XML request that will be wrapped in a SOAP
message.'''
def post(self, data=None, uri=None, headers={}, encode='form'):
'''Perform a HTTP POST on the server. If p_encode is "form", p_data is
considered to be a dict representing form data that will be
form-encoded. Else, p_data will be considered as the ready-to-send
body of the HTTP request.'''
if not uri: uri = self.uri
# Prepare the data to send
if type == 'form':
headers['Host'] = self.host
if encode == 'form':
# Format the form data and prepare headers
body = DataEncoder(data).encode()
body = FormDataEncoder(data).encode()
headers['Content-Type'] = 'application/x-www-form-urlencoded'
elif type =='soap':
else:
body = data
headers['SOAPAction'] = self.url
headers['Content-Type'] = 'text/xml'
headers['Content-Length'] = str(len(body))
return self.sendRequest('POST', uri, headers=headers, body=body)
return self.send('POST', uri, headers=headers, body=body)
def soap(self, data, uri=None, headers={}, namespace=None):
'''Sends a SOAP message to this resource. p_namespace is the URL of the
server-specific namespace.'''
if not uri: uri = self.uri
# Prepare the data to send
data = SoapDataEncoder(data, namespace).encode()
headers['SOAPAction'] = self.url
headers['Content-Type'] = 'text/xml'
res = self.post(data, uri, headers=headers, encode=None)
# Unwrap content from the SOAP envelope
res.data = res.data.Body
return res
# ------------------------------------------------------------------------------

View file

@ -140,7 +140,8 @@ class XmlParser(ContentHandler, ErrorHandler):
return self.res
# ------------------------------------------------------------------------------
from appy.shared import UnmarshalledObject, UnmarshalledFile
from appy.shared import UnmarshalledFile
from appy import Object
try:
from DateTime import DateTime
except ImportError:
@ -161,14 +162,14 @@ class XmlUnmarshaller(XmlParser):
XmlParser.__init__(self)
# self.classes below is a dict whose keys are tag names and values are
# Python classes. During the unmarshalling process, when an object is
# encountered, instead of creating an instance of UnmarshalledObject,
# we will create an instance of the class specified in self.classes.
# encountered, instead of creating an instance of Object, we will create
# an instance of the class specified in self.classes.
# Root tag is named "xmlPythonData" by default by the XmlMarshaller.
# This will not work if the object in the specified tag is not a
# UnmarshalledObject instance (ie it is a list or tuple or simple
# value). Note that we will not call the constructor of the specified
# class. We will simply create an instance of UnmarshalledObject and
# dynamically change the class of the created instance to this class.
# This will not work if the object in the specified tag is not an
# Object instance (ie it is a list or tuple or simple value). Note that
# we will not call the constructor of the specified class. We will
# simply create an instance of Objects and dynamically change the class
# of the created instance to this class.
if not isinstance(classes, dict) and classes:
# The user may only need to define a class for the root tag
self.classes = {'xmlPythonData': classes}
@ -198,12 +199,14 @@ class XmlUnmarshaller(XmlParser):
def convertAttrs(self, attrs):
'''Converts XML attrs to a dict.'''
res = {}
for k, v in attrs.items(): res[str(k)] = v
for k, v in attrs.items():
if ':' in k: # An attr prefixed with a namespace. Remove this.
k = k.split(':')[-1]
res[str(k)] = v
return res
def startDocument(self):
self.res = None # The resulting web of Python objects
# (UnmarshalledObject instances).
self.res = None # The resulting web of Python objects (Object instances)
self.env.containerStack = [] # The stack of current "containers" where
# to store the next parsed element. A container can be a list, a tuple,
# an object (the root object of the whole web or a sub-object).
@ -214,6 +217,10 @@ class XmlUnmarshaller(XmlParser):
containerTags = ('tuple', 'list', 'object', 'file')
numericTypes = ('bool', 'int', 'float', 'long')
def startElement(self, elem, attrs):
# Remember the name of the previous element
previousElem = None
if self.env.currentElem:
previousElem = self.env.currentElem.name
e = XmlParser.startElement(self, elem, attrs)
# Determine the type of the element.
elemType = 'unicode' # Default value
@ -224,7 +231,7 @@ class XmlUnmarshaller(XmlParser):
if elemType in self.containerTags:
# I must create a new container object.
if elemType == 'object':
newObject = UnmarshalledObject(**self.convertAttrs(attrs))
newObject = Object(**self.convertAttrs(attrs))
elif elemType == 'tuple': newObject = [] # Tuples become lists
elif elemType == 'list': newObject = []
elif elemType == 'file':
@ -233,20 +240,31 @@ class XmlUnmarshaller(XmlParser):
newObject.name = attrs['name']
if attrs.has_key('mimeType'):
newObject.mimeType = attrs['mimeType']
else: newObject = UnmarshalledObject(**self.convertAttrs(attrs))
else: newObject = Object(**self.convertAttrs(attrs))
# Store the value on the last container, or on the root object.
self.storeValue(elem, newObject)
# Push the new object on the container stack
e.containerStack.append(newObject)
else:
# If we are already parsing a basic type, it means that we were
# wrong for our diagnotsic of the containing element: it was not
# basic. We will make the assumption that the containing element is
# then an object.
if e.currentBasicType:
# Previous elem was an object: create it on the stack.
newObject = Object()
self.storeValue(previousElem, newObject)
e.containerStack.append(newObject)
e.currentBasicType = elemType
def storeValue(self, name, value):
'''Stores the newly parsed p_value (contained in tag p_name) on the
current container in environment self.env.'''
e = self.env
# Remove namespace prefix when relevant
if ':' in name: name = name.split(':')[-1]
# Change the class of the value if relevant
if (name in self.classes) and isinstance(value, UnmarshalledObject):
if (name in self.classes) and isinstance(value, Object):
value.__class__ = self.classes[name]
# Where must I store this value?
if not e.containerStack:
@ -344,7 +362,8 @@ class XmlMarshaller:
atFiles = ('image', 'file') # Types of archetypes fields that contain files.
def __init__(self, cdata=False, dumpUnicode=False, conversionFunctions={},
dumpXmlPrologue=True, rootTag='xmlPythonData'):
dumpXmlPrologue=True, rootTag='xmlPythonData', namespaces={},
namespacedTags={}):
# If p_cdata is True, all string values will be dumped as XML CDATA.
self.cdata = cdata
# If p_dumpUnicode is True, the result will be unicode.
@ -363,6 +382,41 @@ class XmlMarshaller:
self.dumpXmlPrologue = dumpXmlPrologue
# The name of the root tag
self.rootElementName = rootTag
# The namespaces that will be defined at the root of the XML message.
# It is a dict whose keys are namespace prefixes and whose values are
# namespace URLs.
self.namespaces = namespaces
# The following dict will tell which XML tags will get which namespace
# prefix ({s_tagName: s_prefix}). Special optional dict entry
# '*':s_prefix will indicate a default prefix that will be applied to
# any tag that does not have it own key in this dict.
self.namespacedTags = namespacedTags
self.objectType = None # Will be given by method m_marshal
def getTagName(self, name):
'''Returns the name of tag p_name as will be dumped. It can be p_name,
or p_name prefixed with a namespace prefix (will depend on
self.prefixedTags).'''
# Determine the prefix
prefix = ''
if name in self.namespacedTags: prefix = self.namespacedTags[name]
elif '*' in self.namespacedTags: prefix = self.namespacedTags['*']
if prefix: return '%s:%s' % (prefix, name)
return name
def dumpRootTag(self, res, instance):
'''Dumps the root tag.'''
# Dumps the name of the tag.
tagName = self.getTagName(self.rootElementName)
res.write('<'); res.write(tagName)
# Dumps namespace definitions if any
for prefix, url in self.namespaces.iteritems():
res.write(' xmlns:%s="%s"' % (prefix, url))
# Dumps Appy- or Plone-specific attributed
if self.objectType != 'popo':
res.write(' type="object" id="%s"' % instance.UID())
res.write('>')
return tagName
def dumpString(self, res, s):
'''Dumps a string into the result.'''
@ -382,7 +436,8 @@ class XmlMarshaller:
if not v: return
# p_value contains the (possibly binary) content of a file. We will
# encode it in Base64, in one or several parts.
res.write('<part type="base64" number="1">')
partTag = self.getTagName('part')
res.write('<%s type="base64" number="1">' % partTag)
if hasattr(v, 'data'):
# The file is an Archetypes file.
valueType = v.data.__class__.__name__
@ -393,8 +448,9 @@ class XmlMarshaller:
nextPart = v.data.next
nextPartNumber = 2
while nextPart:
res.write('</part>') # Close the previous part
res.write('<part type="base64" number="%d">'%nextPartNumber)
res.write('</%s>' % partTag) # Close the previous part
res.write('<%s type="base64" number="%d">' % \
(partTag, nextPartNumber))
res.write(nextPart.data.encode('base64'))
nextPart = nextPart.next
nextPartNumber += 1
@ -402,7 +458,7 @@ class XmlMarshaller:
res.write(v.data.encode('base64'))
else:
res.write(v.encode('base64'))
res.write('</part>')
res.write('</%s>' % partTag)
def dumpValue(self, res, value, fieldType, isRef=False):
'''Dumps the XML version of p_value to p_res.'''
@ -432,19 +488,23 @@ class XmlMarshaller:
res.write(self.trueFalse[value])
elif fieldType == 'object':
if hasattr(value, 'absolute_url'):
# Dump the URL to the object only
res.write(value.absolute_url())
else:
res.write(value)
# We could dump the entire object content, too. Maybe we could add a
# parameter to the marshaller to know how to marshall objects
# (produce an ID, an URL, include the entire tag but we need to take
# care of circular references,...)
# Dump the entire object content
for k, v in value.__dict__.iteritems():
if not k.startswith('__'):
self.dumpField(res, k, v)
# Maybe we could add a parameter to the marshaller to know how
# to marshall objects (produce an ID, an URL, include the entire
# tag but we need to take care of circular references,...)
else:
res.write(value)
def dumpField(self, res, fieldName, fieldValue, fieldType='basic'):
'''Dumps in p_res, the value of the p_field for p_instance.'''
res.write('<'); res.write(fieldName);
fieldTag = self.getTagName(fieldName)
res.write('<'); res.write(fieldTag)
# Dump the type of the field as an XML attribute
fType = None # No type will mean "unicode".
if fieldType == 'file': fType = 'file'
@ -457,10 +517,11 @@ class XmlMarshaller:
elif isinstance(fieldValue, list): fType = 'list'
elif fieldValue.__class__.__name__ == 'DateTime': fType = 'DateTime'
elif self.isAnObject(fieldValue): fType = 'object'
if fType: res.write(' type="%s"' % fType)
# Dump other attributes if needed
if type(fieldValue) in self.sequenceTypes:
res.write(' count="%d"' % len(fieldValue))
if self.objectType != 'popo':
if fType: res.write(' type="%s"' % fType)
# Dump other attributes if needed
if type(fieldValue) in self.sequenceTypes:
res.write(' count="%d"' % len(fieldValue))
if fType == 'file':
if hasattr(fieldValue, 'content_type'):
res.write(' mimeType="%s"' % fieldValue.content_type)
@ -471,7 +532,7 @@ class XmlMarshaller:
res.write('>')
# Dump the field value
self.dumpValue(res, fieldValue, fType, isRef=(fieldType=='ref'))
res.write('</'); res.write(fieldName); res.write('>')
res.write('</'); res.write(fieldTag); res.write('>')
def isAnObject(self, instance):
'''Returns True if p_instance is a class instance, False if it is a
@ -484,6 +545,7 @@ class XmlMarshaller:
return True
elif iType.__class__.__name__ == 'ExtensionClass':
return True
return False
def marshall(self, instance, objectType='popo', conversionFunctions={}):
@ -494,6 +556,7 @@ class XmlMarshaller:
a Appy object, specify "appy" as p_objectType. If p_instance is not
an instance at all, but another Python data structure or basic type,
p_objectType is ignored.'''
self.objectType = objectType
# Call the XmlMarshaller constructor if it hasn't been called yet.
if not hasattr(self, 'cdata'):
XmlMarshaller.__init__(self)
@ -505,14 +568,9 @@ class XmlMarshaller:
if self.dumpXmlPrologue:
res.write(xmlPrologue)
if self.isAnObject(instance):
# Determine object ID
if objectType in ('archetype', 'appy'):
objectId = instance.UID() # ID in DB
else:
objectId = str(id(instance)) # ID in RAM
res.write('<'); res.write(self.rootElementName)
res.write(' type="object" id="');res.write(objectId);res.write('">')
# Dump the object ID and the value of the fields that must be dumped
# Dump the root tag
rootTagName = self.dumpRootTag(res, instance)
# Dump the fields of this root object
if objectType == 'popo':
for fieldName, fieldValue in instance.__dict__.iteritems():
mustDump = False
@ -565,18 +623,20 @@ class XmlMarshaller:
self.dumpField(res, field.name,field.getValue(instance),
fieldType=fieldType)
# Dump the object history.
res.write('<history type="list">')
histTag = self.getTagName('history')
eventTag = self.getTagName('event')
res.write('<%s type="list">' % histTag)
wfInfo = instance.portal_workflow.getWorkflowsFor(instance)
if wfInfo:
history = instance.workflow_history[wfInfo[0].id]
for event in history:
res.write('<event type="object">')
res.write('<%s type="object">' % eventTag)
for k, v in event.iteritems():
self.dumpField(res, k, v)
res.write('</event>')
res.write('</history>')
res.write('</%s>' % eventTag)
res.write('</%s>' % histTag)
self.marshallSpecificElements(instance, res)
res.write('</'); res.write(self.rootElementName); res.write('>')
res.write('</'); res.write(rootTagName); res.write('>')
else:
self.dumpField(res, self.rootElementName, instance)
# Return the result