diff --git a/__init__.py b/__init__.py index b90cfe4..8f793ea 100644 --- a/__init__.py +++ b/__init__.py @@ -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' ' % attrName + res = res.strip() + '>' + return res.encode('utf-8') +# ------------------------------------------------------------------------------ diff --git a/shared/__init__.py b/shared/__init__.py index b7dc15e..9a3dff2 100644 --- a/shared/__init__.py +++ b/shared/__init__.py @@ -18,28 +18,9 @@ mimeTypesExts = { 'image/jpeg' : 'jpg', 'image/gif' : 'gif' } -xmlPrologue = '\n' +xmlPrologue = '\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' ' % attrName - res = res.strip() + '>' - return res.encode('utf-8') - class UnmarshalledFile: '''Used for producing file objects from a marshalled Python object.''' def __init__(self): diff --git a/shared/csv_parser.py b/shared/csv_parser.py index ff37558..8ea0e79 100644 --- a/shared/csv_parser.py +++ b/shared/csv_parser.py @@ -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 diff --git a/shared/dav.py b/shared/dav.py index bea9b0d..0aae621 100644 --- a/shared/dav.py +++ b/shared/dav.py @@ -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 = '' \ # '%s' \ # '' % 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 # ------------------------------------------------------------------------------ + diff --git a/shared/xml_parser.py b/shared/xml_parser.py index 6a5b005..e68120f 100644 --- a/shared/xml_parser.py +++ b/shared/xml_parser.py @@ -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('') + 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('') # Close the previous part - res.write(''%nextPartNumber) + res.write('' % 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('') + res.write('' % 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('') 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('') + 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('') + res.write('<%s type="object">' % eventTag) for k, v in event.iteritems(): self.dumpField(res, k, v) - res.write('') - res.write('') + res.write('' % eventTag) + res.write('' % histTag) self.marshallSpecificElements(instance, res) - res.write('') + res.write('') else: self.dumpField(res, self.rootElementName, instance) # Return the result