diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000..58a01737 --- /dev/null +++ b/.hgignore @@ -0,0 +1,6 @@ +# use glob syntax. +syntax: glob + +*.pyc +.project +.pydevproject \ No newline at end of file diff --git a/docs/tutorial.txt b/docs/tutorial.txt new file mode 100644 index 00000000..d4ed31e0 --- /dev/null +++ b/docs/tutorial.txt @@ -0,0 +1,274 @@ +About +===== + +The main purpose of this library is neat implementation of SOAP protocol, +but xsd package can used for any XML as it gives means of mapping XML +to object. + The object description generally is similar to Django database models - the static +fields that define instance fields. The main difference would be that type is +passed as first parameter, rather then being a field e.g +Django: tail_number = models.CharField() +ws: tail_number = xsd.Element(xsd.String) +xsd.Element reflects the nature of the field, elements are fields that +will be wrapped with tag, other options are xsd.Attribute, xsd.Ref and +xsd.ListElement. For more detail see xsd.Element pydoc. + As SOAP, WSDL and XSD files are also XMLs the xsd package was also +used to describe them. The descriptions are located in xsdspec.py, soap.py +and wsdl.py. soap package also provides dispatcher and client Stub. + Other elements included in this tool are translators, that can generate python +code from formal description, or formal description from code. Related files: +py2xsd.py, xsd2py.py, wsdl2py.py, py2wsdl.py. + utils.py is mostly jinja2 helper functions. jinja2 is templating engine used +for code generation. + +1. Working with XML +================ + The main building block are xsd.ComplexType, xsd.Element, xsd.Attribute and +simple types defined in xsd package. xsd.ComplexType is a parent to extend to define +own type. Main methods for types are xml - translates object into XML, and parsexml +builds object from XML. + +#Example 1. Rendering object to XML. +from ws import xsd + +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) + +airport = Airport() +airport.type = "IATA" +airport.code = "WAW" +print airport.xml("takeoff_airport") + +Note that xml method takes one parameter - root tag name. + +#Example 2. Parsing XML to object. +from ws import xsd +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) +XML = """ + IATA + WAW +""" +airpport = Airport.parsexml(XML) +print "Type:", airport.type#prints Type: IATA +print "Code:", airport.code#Code: WAW + +#Example 3. Nested ComplexTypes with attributes. +from datetime import datetime +from ws import xsd +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) + +class Flight(xsd.ComplexType): + tail_number = xsd.Attribute(xsd.String) + type = xsd.Attribute(xsd.Integer, use=xsd.Use.OPTIONAL) + takeoff_airport = xsd.Element(Airport) + takeoff_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + landing_airport = xsd.Element(Airport) + landing_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + +flight = Flight(tail_number="LN-KKA")#Constructor handles field inititailization. +flight.takeoff_airport = Airport(type="IATA", code="WAW") +flight.landing_airport = Airport(type="ICAO", code="EGLL") + +print flight.xml("flight") +#datetime field types will accept, datetime object or string, +#that parses correctly to such object. +flight.takeoff_datetime = datetime.now() +print flight.xml("flight") + +will produce: + + + IATA + WAW + + 2011-05-06T11:11:23 + + ICAO + EGLL + + + +2. Schema +================= + xsd.Schema is an object that aggregates all informations stored in XSD file. +There two main use cases for this object. It can be used to generate XSD file or +it can be generated from such file. For detail field description see: xsd.Schema +pydoc. Schema instance is required for validation and because SOAP webservice +performs validation is required for service configuration too: See documentation +Defining webservice. + +2.1 Generating code from XSD file. +================================= + py2xsd.py generates Python representation of XML from XSD file. +Example: xsd2py.py Specifications\ops.xsd will generate: + +from ws import xsd + +class Pilot(xsd.String): + enumeration = [ "CAPTAIN", "FIRST_OFFICER", ] + +class Airport(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + code_type = xsd.Element(xsd.String( enumeration = + [ "ICAO", "IATA", "FAA",]) ) + code = xsd.Element(xsd.String) + + +class Weight(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + value = xsd.Element(xsd.Integer) + unit = xsd.Element(xsd.String( enumeration = + [ "kg", "lb",]) ) + + +class Ops(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + aircraft = xsd.Element(xsd.String) + flight_number = xsd.Element(xsd.String) + type = xsd.Element(xsd.String( enumeration = + [ "COMMERCIAL", "INCOMPLETE", "ENGINE_RUN_UP", "TEST", "TRAINING", "FERRY", +"POSITIONING", "LINE_TRAINING",]) ) + takeoff_airport = xsd.Element(Airport) + takeoff_gate_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + takeoff_datetime = xsd.Element(xsd.DateTime) + takeoff_fuel = xsd.Element(Weight, minOccurs=0) + takeoff_gross_weight = xsd.Element(Weight, minOccurs=0) + takeoff_pilot = xsd.Element(Pilot, minOccurs=0) + landing_airport = xsd.Element(Airport) + landing_gate_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + landing_datetime = xsd.Element(xsd.DateTime) + landing_fuel = xsd.Element(Weight, minOccurs=0) + landing_pilot = xsd.Element(Pilot, minOccurs=0) + destination_airport = xsd.Element(Airport, minOccurs=0) + captain_code = xsd.Element(xsd.String, minOccurs=0) + first_officer_code = xsd.Element(xsd.String, minOccurs=0) + V2 = xsd.Element(xsd.Integer, minOccurs=0) + Vref = xsd.Element(xsd.Integer, minOccurs=0) + Vapp = xsd.Element(xsd.Integer, minOccurs=0) + + +class Status(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + action = xsd.Element(xsd.String( enumeration = + [ "INSERTED", "UPDATED", "EXISTS",]) ) + id = xsd.Element(xsd.Long) + +Schema = xsd.Schema( + targetNamespace = "http://flightdataservices.com/ops.xsd", + elementFormDefault = "unqualified", + simpleTypes = [ Pilot,], + attributeGroups = [], + groups = [], + complexTypes = [ Airport, Weight, Ops, Status,], + elements = { "status":xsd.Element(Status), "ops":xsd.Element(Ops),}) + + +Let redirect output to the python file. +{{xsd2py.py Specifications\ops.xsd > tmp\ops.py}} +Now calling {{py2xsd.py tmp\ops.py}} will generate equivalent XSD from Python +code. xsd2py script expects schema instance to be defined in global scope +called "Schema", in way similar to one in generated code. + +2. Web service. +======================== + + When WSDL file is provided code can be generated from it. If not, advised +would be to write to code first a then use browser to request it. Accessing +?wsdl with browser will give current WSDL with XSD +embaded. + +2.1 Generating code from WSDL file +================================== + wsdl2py can generate either client or server code. For server use -s, client -c +flag. +Server example: wsdl2py.py -s Specifications\ops.wsdl + +{{{ +...XSD part truncated... +PutOps_method = xsd.Method(function = PutOps, + soapAction = "http://polaris.flightdataservices.com/ws/ops/PutOps", + input = "ops",#Pointer to Schema.elements + output = "status",#Pointer to Schema.elements + operationName = "PutOps") + +SERVICE = soap.Service( + targetNamespace = "http://flightdataservices.com/ops.wsdl", + location = "http://polaris.flightdataservices.com/ws/ops", + schema = Schema, + methods = [PutOps_method, ]) + + +#Comment this lines for py2xsd generation to avoid error message about +#DJANGO_SETTINGS_MODULE not being set. If authorization is required +#dispatch can be wrapped with login required in way similar to csrf_exempt. +from django.views.decorators.csrf import csrf_exempt +dispatch = csrf_exempt(soap.get_django_dispatch(SERVICE)) + +#Put this lines in your urls.py: +#urlpatterns += patterns('', +# (r"^ws/ops$", ".dispatch") +#) +}}} + + Generated code has four main items: methods descriptions, Service description, +dispatcher and Django ulrs.py binding. + Method description describes one method for service(that can consist from +more then one method). Methods give dispatcher informations required for method +distinction - soapAction and operationName, and function to call on incoming SOAP message. +For detail field meaning see xsd.Method pydoc. + SERVICE aggregates all informations required for WSDL generation and correct +dispatching. + get_django_dispatch returns a function binded to SERVICE that pointed from +urls.py will call appropriate function on incoming SOAP message. The called function, +in this example PutOps, is expected to return object from XSD that could be translated +to correct and valid response - for this example this would be Status instance. + URLs binding it is commented out, paste this code into your urls.py and change + to point file where to code was generated. + +2.1 Client +========== + Client can be generated with flag -c: wsdl2py.py -c Specifications\ops.wsdl + + Generated code: + {{{ + ...XSD Part truncated ... + PutOps_method = xsd.Method( + soapAction = "http://polaris.flightdataservices.com/ws/ops/PutOps", + input = "ops",#Pointer to Schema.elements + output = "status",#Pointer to Schema.elements + operationName = "PutOps") + +SERVICE = soap.Service( + targetNamespace = "http://flightdataservices.com/ops.wsdl", + location = "http://polaris.flightdataservices.com/ws/ops", + schema = Schema, + methods = [PutOps_method, ]) + + +class ServiceStub(soap.Stub): + SERVICE = SERVICE + + def PutOps(self, ops): + return self.call("PutOps", ops) + }}} + + ServiceStub is a proxy object that defines methods available on remote webservice. + Calling one of those method, in the example there is only one PutOps, will produce SOAP +call to remote server defined in SERVICE. The methods will return appropriate object +from XSD description or raise an exception on any problems. + +For more real example: See docs/example_client.py + + + + + diff --git a/examples/client.py b/examples/client.py new file mode 100644 index 00000000..09dee458 --- /dev/null +++ b/examples/client.py @@ -0,0 +1,100 @@ +#This was was generated by wsdl2py, try to not edit. +from soapbox import soap, xsd + +class Pilot(xsd.String): + enumeration = [ "CAPTAIN", "FIRST_OFFICER", ] + + +class Airport(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + code_type = xsd.Element(xsd.String( enumeration = + [ "ICAO", "IATA", "FAA",]) ) + code = xsd.Element(xsd.String) + + +class Weight(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + value = xsd.Element(xsd.Integer) + unit = xsd.Element(xsd.String( enumeration = + [ "kg", "lb",]) ) + + +class Ops(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + aircraft = xsd.Element(xsd.String) + flight_number = xsd.Element(xsd.String) + type = xsd.Element(xsd.String( enumeration = + [ "COMMERCIAL", "INCOMPLETE", "ENGINE_RUN_UP", "TEST", "TRAINING", "FERRY", "POSITIONING", "LINE_TRAINING",]) ) + takeoff_airport = xsd.Element(Airport) + takeoff_gate_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + takeoff_datetime = xsd.Element(xsd.DateTime) + takeoff_fuel = xsd.Element(Weight, minOccurs=0) + takeoff_gross_weight = xsd.Element(Weight, minOccurs=0) + takeoff_pilot = xsd.Element(Pilot, minOccurs=0) + landing_airport = xsd.Element(Airport) + landing_gate_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + landing_datetime = xsd.Element(xsd.DateTime) + landing_fuel = xsd.Element(Weight, minOccurs=0) + landing_pilot = xsd.Element(Pilot, minOccurs=0) + destination_airport = xsd.Element(Airport, minOccurs=0) + captain_code = xsd.Element(xsd.String, minOccurs=0) + first_officer_code = xsd.Element(xsd.String, minOccurs=0) + V2 = xsd.Element(xsd.Integer, minOccurs=0) + Vref = xsd.Element(xsd.Integer, minOccurs=0) + Vapp = xsd.Element(xsd.Integer, minOccurs=0) + + +class Status(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + action = xsd.Element(xsd.String( enumeration = + [ "INSERTED", "UPDATED", "EXISTS",]) ) + id = xsd.Element(xsd.Long) + +Schema = xsd.Schema( + targetNamespace = "http://flightdataservices.com/ops.xsd", + elementFormDefault = "unqualified", + simpleTypes = [ Pilot,], + attributeGroups = [], + groups = [], + complexTypes = [ Airport, Weight, Ops, Status,], + elements = { "ops":xsd.Element(Ops), "status":xsd.Element(Status),}) + + + +PutOps_method = xsd.Method( + soapAction = "http://polaris.flightdataservices.com/ws/ops/PutOps", + input = "ops",#Pointer to Schema.elements + output = "status",#Pointer to Schema.elements + operationName = "PutOps") + +SERVICE = soap.Service( + targetNamespace = "http://flightdataservices.com/ops.wsdl", + location = "http://127.0.0.1:8000/ws/ops", + schema = Schema, + methods = [PutOps_method, ]) + + +class ServiceStub(soap.Stub): + SERVICE = SERVICE + + def PutOps(self, ops): + return self.call("PutOps", ops) + +if __name__ == "__main__": + from datetime import datetime + stub = ServiceStub() + ops = Ops() + ops.aircraft = "LN-KKU" + ops.flight_number = "1234" + ops.type = "COMMERCIAL" + ops.takeoff_airport = Airport(code_type="IATA",code="WAW") + ops.takeoff_datetime = datetime.now() + ops.landing_airport = Airport(code_type="ICAO", code="EGLL") + ops.landing_datetime = datetime.now() + status = stub.PutOps(ops) + print status.action, status.id + diff --git a/examples/ops.wsdl b/examples/ops.wsdl new file mode 100644 index 00000000..1838f9e2 --- /dev/null +++ b/examples/ops.wsdl @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Register Flight Ops + + + + + diff --git a/examples/ops.xsd b/examples/ops.xsd new file mode 100644 index 00000000..df3bd069 --- /dev/null +++ b/examples/ops.xsd @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/xml_complex_types.py b/examples/xml_complex_types.py new file mode 100644 index 00000000..50600a33 --- /dev/null +++ b/examples/xml_complex_types.py @@ -0,0 +1,25 @@ +#Example 3. Nested ComplexTypes with attributes. +from datetime import datetime +from soapbox import xsd + +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) + +class Flight(xsd.ComplexType): + tail_number = xsd.Attribute(xsd.String) + type = xsd.Attribute(xsd.Integer, use=xsd.Use.OPTIONAL) + takeoff_airport = xsd.Element(Airport) + takeoff_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + landing_airport = xsd.Element(Airport) + landing_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + +flight = Flight(tail_number="LN-KKA")#Constructor handles field inititailization. +flight.takeoff_airport = Airport(type="IATA", code="WAW") +flight.landing_airport = Airport(type="ICAO", code="EGLL") + +print flight.xml("flight") +#datetime field types will accept, datetime object or string, +#that parses correctly to such object. +flight.takeoff_datetime = datetime.now() +print flight.xml("flight") \ No newline at end of file diff --git a/examples/xml_parsing.py b/examples/xml_parsing.py new file mode 100644 index 00000000..89b89a63 --- /dev/null +++ b/examples/xml_parsing.py @@ -0,0 +1,15 @@ +#Example 2. Parsing XML to object. +from soapbox import xsd + +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) + +XML = """ + IATA + WAW +""" + +airport = Airport.parsexml(XML) +print "Type:", airport.type +print "Code:", airport.code \ No newline at end of file diff --git a/examples/xml_rendering.py b/examples/xml_rendering.py new file mode 100644 index 00000000..dd6047ad --- /dev/null +++ b/examples/xml_rendering.py @@ -0,0 +1,10 @@ +#Example 1. Rendering object to XML. +from soapbox import xsd +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) + +airport = Airport() +airport.type = "IATA" +airport.code = "WAW" +print airport.xml("takeoff_airport") \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..4730be7f --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup + +setup( + name="soapbox", + version="0.1", + author="Damian Powazka", + author_email="dpowazka@gmail.com", + url="http://code.google.com/p/soapbox/", + description="", + long_description="", + download_url="", + license="New BSD", + install_requires=['lxml','jinja2'], + packages=["soapbox"], + platforms="Python 2.6 and later.", + classifiers=[ + "Development Status :: 5 - Stable", + "License :: OSI Approved :: New BSD", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Intended Audience :: Science/Research", + ] + ) \ No newline at end of file diff --git a/soapbox/__init__.py b/soapbox/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/soapbox/py2wsdl.py b/soapbox/py2wsdl.py new file mode 100644 index 00000000..363c415e --- /dev/null +++ b/soapbox/py2wsdl.py @@ -0,0 +1,99 @@ +import sys +import imp +import wsdl +from lxml import etree +from py2xsd import generate_xsdspec + +def build_service(definitions, service): + wsdl_service = wsdl.Service() + for method in service.methods: + wsdl_port = wsdl.Port() + wsdl_port.name = method.operationName+"Port" + wsdl_port.binding = "tns:" + method.operationName+"Binding" + wsdl_port.address = wsdl.SOAP_Address(location=service.location) + wsdl_service.port = wsdl_port + definitions.services.append(wsdl_service) + +def build_bindings(definitions, service): + for method in service.methods: + binding = wsdl.Binding() + binding.name = method.operationName +"Binding" + binding.type = "tns:" + method.operationName + "PortType" + binding.binding = wsdl.SOAP_Binding() + binding.binding.style = "document" + binding.binding.transport = "http://schemas.xmlsoap.org/soap/http" + + operation = wsdl.Operation() + operation.name = method.operationName + operation.operation = wsdl.SOAP_Operation() + operation.operation.soapAction = method.soapAction + operation.input = wsdl.Input(body=wsdl.SOAP_Body(use="literal")) + operation.output = wsdl.Input(body=wsdl.SOAP_Body(use="literal")) + binding.operation = operation + + definitions.bindings.append(binding) + + +def build_portTypes(definitions, service): + for method in service.methods: + portType = wsdl.PortType() + portType.name = method.operationName + "PortType" + operation = wsdl.Operation() + operation.name = method.operationName + operation.input = wsdl.Input(message="tns:" +method.operationName+"Input") + operation.output = wsdl.Input(message="tns:" +method.operationName+"Output") + portType.operation = operation + + definitions.portTypes.append(portType) + +def build_messages(definitions, service): + for method in service.methods: + inputMessage = wsdl.Message(name=method.operationName+"Input") + inputMessage.part = wsdl.Part() + inputMessage.part.name = "body" + if isinstance(method.input, str): + inputMessage.part.element = "sns:"+method.input + else: + inputMessage.part.type = "sns:"+method.input.__name__.lower() + definitions.messages.append(inputMessage) + + outputMessage = wsdl.Message(name=method.operationName+"Output") + outputMessage.part = wsdl.Part() + outputMessage.part.name = "body" + if isinstance(method.output, str): + outputMessage.part.element = "sns:"+method.output + else: + outputMessage.part.type = "sns:"+method.output.__name__.lower() + definitions.messages.append(outputMessage) + +def build_types(definitions, schema): + xsd_schema = generate_xsdspec(schema) + definitions.types = wsdl.Types(schema=xsd_schema) + + + +def generate_wsdl(service): + definitions = wsdl.Definitions(targetNamespace=service.targetNamespace) + build_types(definitions, service.schema) + build_service(definitions, service) + build_bindings(definitions, service) + build_portTypes(definitions, service) + build_messages(definitions, service) + + xmlelement = etree.Element("{http://schemas.xmlsoap.org/wsdl/}definitions", + nsmap = {"xsd" : "http://www.w3.org/2001/XMLSchema", + "wsdl" : "http://schemas.xmlsoap.org/wsdl/", + "soap" : "http://schemas.xmlsoap.org/wsdl/soap/", + "sns" : service.schema.targetNamespace, + "tns" : service.targetNamespace}) + definitions.render(xmlelement, definitions, "http://schemas.xmlsoap.org/wsdl/") + return etree.tostring(xmlelement, pretty_print=True) + +if __name__ == "__main__": + path = sys.argv[1] + globals = imp.load_source("", path) + service = getattr(globals,"SERVICE") + print generate_wsdl(service) + + + \ No newline at end of file diff --git a/soapbox/py2xsd.py b/soapbox/py2xsd.py new file mode 100644 index 00000000..ca40f355 --- /dev/null +++ b/soapbox/py2xsd.py @@ -0,0 +1,120 @@ +import sys +import imp +from lxml import etree +import xsd +import xsdspec +from utils import uncapitalize + +def get_xsd_type(_type): + """Check is basic type from XSD scope, else it must be user + defined type.""" + base_class = _type.__class__.__bases__[0] + if base_class == xsd.SimpleType or _type.__class__ == xsd.Long: + return "xsd:" + uncapitalize(_type.__class__.__name__) + else: + return "sns:" + uncapitalize(_type.__class__.__name__) + +def xsd_attribute(attribute): + xsdattr = xsdspec.Attribute() + xsdattr.name = attribute._name + xsdattr.use = attribute.use + xsdattr.type = get_xsd_type(attribute._type) + return xsdattr + +def create_xsd_element(element): + xsd_element = xsdspec.Element() + xsd_element.name = element._name + if element._minOccurs == 0: + xsd_element.minOccurs = 0 + # SimpleType defined in place. + parent_type = element._type.__class__.__bases__[0] + if hasattr(element._type, "enumeration") and element._type.enumeration\ + and parent_type == xsd.SimpleType: + xsd_element.simpleType = xsdspec.SimpleType() + xsd_element.simpleType.restriction = xsdspec.Restriction() + xsd_element.simpleType.restriction.base = get_xsd_type(element._type) + for value in element._type.enumeration: + enum = xsdspec.Enumeration.create(value) + xsd_element.simpleType.restriction.enumerations.append(enum) + else: + xsd_element.type = get_xsd_type(element._type) + return xsd_element + +def xsd_complexType(complexType): + xsd_ct = xsdspec.ComplexType() + xsd_ct.name = uncapitalize(complexType.__name__) + + for attribute in complexType._meta.attributes: + xsd_attr = xsd_attribute(attribute) + xsd_ct.attributes.append(xsd_attr) + + #Elements can be wrapped with few type of containers: + # sequence, all, choice or it can be a complexContent with + # extension or restriction. + if hasattr(complexType, "INDICATOR") and complexType.INDICATOR: + xsd_sequence = xsdspec.Sequence() + xsd_ct.sequence = xsd_sequence + container = xsd_sequence + else: + container = xsd_ct + + for element in complexType._meta.fields: + xsd_element = create_xsd_element(element) + container.elements.append(xsd_element) + return xsd_ct + +def xsd_simpleType(st): + xsd_simpleType = xsdspec.SimpleType() + xsd_simpleType.name = st.__name__.lower() + xsd_restriction = xsdspec.Restriction() + xsd_restriction.base = get_xsd_type(st.__bases__[0]()) + if hasattr(st,"enumeration") and st.enumeration: + for enum in st.enumeration: + xsd_restriction.enumerations.append(xsdspec.Enumeration.create(enum)) + elif hasattr(st, "pattern") and st.pattern: + xsd_restriction.pattern = st.pattern + xsd_simpleType.restriction = xsd_restriction + return xsd_simpleType + +def generate_xsdspec(schema): + xsd_schema = xsdspec.Schema() + xsd_schema.targetNamespace = schema.targetNamespace + for st in schema.simpleTypes: + xsd_st = xsd_simpleType(st) + xsd_schema.simpleTypes.append(xsd_st) + for ct in schema.complexTypes: + xsd_ct = xsd_complexType(ct) + xsd_schema.complexTypes.append(xsd_ct) + generate_elements(xsd_schema, schema) + return xsd_schema + +def generate_elements(xsd_schema, schema): + for name, element in schema.elements.iteritems(): + xsd_element = xsdspec.Element() + xsd_element.name = name + xsd_element.type = get_xsd_type(element._type) + xsd_schema.elements.append(xsd_element) + + +def generate_xsd(schema): + xsd_schema = generate_xsdspec(schema) + xmlelement = etree.Element("{http://www.w3.org/2001/XMLSchema}schema", + nsmap = {"xsd" : "http://www.w3.org/2001/XMLSchema", + "sns" : schema.targetNamespace}) + xsd_schema.render(xmlelement, xsd_schema) + return xmlelement + + +if __name__ == "__main__": + import os + print os.getcwd() + if len(sys.argv) != 2: + print "Use: py2wsld.py " + sys.exit() + module = sys.argv[1] + globals = imp.load_source("module.name", module) + schema = getattr(globals,"Schema") + schemaelement = generate_xsd(schema) + print etree.tostring(schemaelement, pretty_print=True) + + diff --git a/soapbox/soap.py b/soapbox/soap.py new file mode 100644 index 00000000..95d21c81 --- /dev/null +++ b/soapbox/soap.py @@ -0,0 +1,208 @@ +#SOAP Protocol implementation, dispatchers and client stub. +from lxml import etree +import xsd +import re +import py2wsdl +import httplib2 + +class SOAPVersion: + SOAP11 = "SOAP 1.1" + SOAP12 = "SOAP 1.2" + +#SOAP messages description objects. +class Header(xsd.ComplexType): + """SOAP Envelope Header.""" + pass + +class Fault(xsd.ComplexType): + """SOAP Envelope Fault.""" + faultcode = xsd.Element(xsd.String) + faultstring = xsd.Element(xsd.String) + detail = xsd.Element(xsd.String) + +class Body(xsd.ComplexType): + """SOAP Envelope Body.""" + message = xsd.ClassNamedElement(xsd.ComplexType, minOccurs=0) + Fault = xsd.Element(Fault, minOccurs=0) + def content(self): + return etree.tostring(self._xmlelement[0], pretty_print=True) + +class Envelope(xsd.ComplexType): + """SOAP Envelope.""" + Header = xsd.Element(Header, nilable=True) + Body = xsd.Element(Body) + + @classmethod + def reponse(cls, return_object): + envelope = Envelope() + envelope.Body = Body() + envelope.Body.message = return_object + return envelope.xml("Envelope") + +Schema = xsd.Schema( + targetNamespace = "http://schemas.xmlsoap.org/soap/envelope/", + elementFormDefault = xsd.ElementFormDefault.QUALIFIED, + complexTypes = [Header, Body, Envelope]) + +class SOAPError(Exception): + pass + +class Service(object): + """Describes service aggregating informations required for dispatching + and WSDL generation. """ + def __init__(self, targetNamespace, location, schema, methods, + version=SOAPVersion.SOAP11): + """:param targetNamespace: string + :param location: string, endpoint url. + :param schema: xsd.Schema instance. + :param methods: list of xsd.Methods""" + self.targetNamespace = targetNamespace + self.location = location + self.schema = schema + self.methods = methods + self.version = version + + def get_method(self, operationName): + return filter(lambda m:m.operationName ==operationName, self.methods)[0] + + +#TODO: +#1. Fault code could use Client.Authentication notation to indicate error type. + +def get_django_dispatch(service): + """Returns dispatch method for specified service. Dispatch method can be + pointed by urls.py, it will capture incoming SOAP message, translate it into + object and call appropriate method. Expecting return object that can be + translated to valid SOAP response for this service. + On any excpetion raised from method the response will be SOAP Fault message. + ValueError are translated into fault code Client, other to Server. + Incoming and outgoing XMLs are validated against XSD generated from service + schema. Incorrect or missing values will cause Fault response. + """ + def get_soap_action(request): + """Finds soapAction information in HTTP header. First tries SOAP 1.1 + soapAction and action header key, then looks into content type + for SOAP 1.2 action key. SOAP action is important for establishing + which method is called in document style calls where method name + is not wrapping the message content.""" + if request.META.get("HTTP_SOAPACTION"): + return request.META.get("HTTP_SOAPACTION").replace('"','') + elif request.META.get("HTTP_ACTION"): + return request.META.get("HTTP_ACTION").replace('"','') + else: + content_types = request.META["CONTENT_TYPE"].split(";") + for content_type in content_types: + if content_type.strip(" ").startswith("action="): + return content_type.split("=")[1] + return None + + def build_soap_message(o): + try: + o.xml(o.__class__.__name__.lower(), service.schema)#Validation. + except: + raise ValueError(e) + + return Envelope.reponse(o) + + def django_dispatch(request): + "Dispatch method tied to service." + #We don't want to import this in main context as the project may be + #using different way of dispatching. Django would be unnessesery + #dependecy which is sensible to assume to be true in Django dispatch only. + from django.http import HttpResponse + if request.method == "GET" and request.GET.has_key("wsdl"): + wsdl = py2wsdl.generate_wsdl(service) + return HttpResponse(wsdl,mimetype="text/xml") + + try: + xml = request.raw_post_data + envelope = Envelope.parsexml(xml) + message = envelope.Body.content() + soap_action = get_soap_action(request) + + for method in service.methods: + if soap_action != method.soapAction: + continue + if isinstance(method.input,str): + element = service.schema.elements[method.input] + input_object = element._type.parsexml(message,service.schema) + else: + input_object = method.input.parsexml(message,service.schema) + return_object = method.function(request, input_object) + return HttpResponse(build_soap_message(return_object)) + raise ValueError("Method not found!") + except (ValueError,etree.XMLSyntaxError), e: + fault = Fault(faultcode="Client", faultstring=str(e), detail=str(e)) + except Exception, e: + #Presents of detail element indicates that the problem is related + #to procesing Body element. See 4.4 SOAP Fault on + #http://www.w3.org/TR/2000/NOTE-SOAP-20000508/ + fault = Fault(faultcode="Server", faultstring=str(e), detail=str(e)) + envelope = Envelope() + envelope.Body = Body(Fault=fault) + return HttpResponse(envelope.xml("Envelope")) + return django_dispatch + + +class Stub(object): + """Client stub. Handles only document style calls.""" + SERVICE = None + + def __init__(self, username=None, password=None): + self.username = username + self.password = password + + def _build_header(self, method): + if self.SERVICE.version == SOAPVersion.SOAP11: + return {"content-type" : 'text/xml', + "SOAPAction" : method.soapAction} + elif self.SERVICE.version == SOAPVersion.SOAP12: + return {"content-type" : "application/soap+xml;action=%s" % method.soapAction} + else: + raise ValueError("SOAP Version not supported %s" % self.SERVICE.version) + + def _handle_response(self, method, response, content): + envelope = Envelope.parsexml(content) + if envelope.Body.Fault: + raise SOAPError("Fault Code:%s, Fault String: %s" % + (envelope.Body.Fault.faultcode, + envelope.Body.Fault.faultstring)) + message = envelope.Body.content() + + if isinstance(method.output, str): + element = self.SERVICE.schema.elements[method.output] + _type = element._type + else: + _type = method.output + + if self.SERVICE.schema: + return _type.parsexml(message, self.SERVICE.schema) + else: + return _type.parsexml(message) + + + def call(self, operationName, parameter): + #Will raise: lxml.etree.XMLSyntaxError on validation problems. + parameter.xml(parameter.__class__.__name__.lower(), self.SERVICE.schema) + + h = httplib2.Http() + if self.username: + h.add_credentials(self.username, self.password) + + method = self.SERVICE.get_method(operationName) + headers = self._build_header(method) + envelope = Envelope.reponse(parameter) + + response, content = h.request(self.SERVICE.location, "POST", + body=envelope, headers=headers) + return self._handle_response(method, response, content) + + + + + + + + + + \ No newline at end of file diff --git a/soapbox/utils.py b/soapbox/utils.py new file mode 100644 index 00000000..4760a78c --- /dev/null +++ b/soapbox/utils.py @@ -0,0 +1,61 @@ +from urlparse import urlparse + +def removens(full_typename): + if full_typename is None: + return None + + typename = full_typename.split(":") + if len(typename) == 2: + ns, typename = typename + else: + ns = None + return typename + +def classyfiy(value): + return value[0].upper() +value[1:] + + +def get_get_type(XSD_NAMESPACES): + def get_type(full_typename): + if full_typename is None: + return None + + typename = full_typename.split(":") + if len(typename) == 2: + ns, typename = typename + else: + ns = None + typename = typename[0] + if ns in XSD_NAMESPACES: + return "xsd." + classyfiy(typename) + else: + return classyfiy(typename) + return get_type + +def use(usevalue): + if usevalue == xsd.Use.OPTIONAL: + return "xsd.Use.OPTIONAL" + elif usevalue == xsd.Use.REQUIRED: + return "xsd.Use.REQUIRED" + elif usevalue == xsd.Use.PROHIBITED: + return "xsd.Use.PROHIBITED" + else: + raise ValueError + +def find_xsd_namepsace(nsmap): + namespaces = [] + for key, value in nsmap.iteritems(): + if value == "http://www.w3.org/2001/XMLSchema"\ + or value == "http://www.w3.org/2000/10/XMLSchema": + namespaces.append(key) + return namespaces + +def urlcontext(url): + """http://polaris.flightdataservices.com/ws/ops-> ^ws/ops$""" + o = urlparse(url) + path = o.path[1:]#remove trailing / + return "^"+path+"$" #build regex + +def uncapitalize(value): + return value[0].lower() + value[1:] + diff --git a/soapbox/wsdl.py b/soapbox/wsdl.py new file mode 100644 index 00000000..18b578e8 --- /dev/null +++ b/soapbox/wsdl.py @@ -0,0 +1,100 @@ +import xsd +import xsdspec + +class SOAP_Binding(xsd.ComplexType): + NAMESPACE = "http://schemas.xmlsoap.org/wsdl/soap/" + ELEMENT_FORM_DEFAULT = xsd.ElementFormDefault.QUALIFIED + style = xsd.Attribute(xsd.String) + transport = xsd.Attribute(xsd.String) + +class SOAP_Operation(xsd.ComplexType): + NAMESPACE = "http://schemas.xmlsoap.org/wsdl/soap/" + ELEMENT_FORM_DEFAULT = xsd.ElementFormDefault.QUALIFIED + soapAction = xsd.Attribute(xsd.String) + +class SOAP_Body(xsd.ComplexType): + NAMESPACE = "http://schemas.xmlsoap.org/wsdl/soap/" + ELEMENT_FORM_DEFAULT = xsd.ElementFormDefault.QUALIFIED + use = xsd.Attribute(xsd.String) + +class SOAP_Address(xsd.ComplexType): + NAMESPACE = "http://schemas.xmlsoap.org/wsdl/soap/" + ELEMENT_FORM_DEFAULT = xsd.ElementFormDefault.QUALIFIED + location = xsd.Attribute(xsd.String) + +class Types(xsd.ComplexType): + schema = xsd.Element(xsdspec.Schema) + +class Part(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + element = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + type = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + +class Message(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + part = xsd.Element(Part) + +class Input(xsd.ComplexType): + message = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + body = xsd.Element(SOAP_Body, minOccurs=0) + +class Operation(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + input = xsd.Element(Input) + output = xsd.Element(Input) + body = xsd.Element(SOAP_Body) + operation = xsd.Element(SOAP_Operation) + +class PortType(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + operation = xsd.Element(Operation) + +class Binding(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + type = xsd.Attribute(xsd.String) + binding = xsd.Element(SOAP_Binding) + operation = xsd.Element(Operation) + +class Port(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + binding = xsd.Attribute(xsd.String) + address = xsd.Element(SOAP_Address) + +class Service(xsd.ComplexType): + documentation = xsd.Element(xsd.String) + port = xsd.Element(Port) + +class Definitions(xsd.ComplexType): + targetNamespace = xsd.Attribute(xsd.String) + types = xsd.Element(Types) + messages = xsd.ListElement(Message,"message") + portTypes = xsd.ListElement(PortType, "portType") + bindings = xsd.ListElement(Binding, "binding") + services = xsd.ListElement(Service,"service") + + @classmethod + def get_by_name(cls, _list, fullname): + name = fullname.split(":")[-1] + for item in _list: + if item.name == name: + return item + raise ValueError("Item '%s' not found in list:%s" % (name, _list)) + + +SCHEMA = xsd.Schema( + targetNamespace = "http://schemas.xmlsoap.org/wsdl/", + elementFormDefault = xsd.ElementFormDefault.QUALIFIED, + simpleTypes = [], + attributeGroups = [], + groups = [], + complexTypes = [Types, Part, Message, Input, Operation, PortType, Binding, + Port, Service, Definitions], + elements = {}) + + + + + + + + diff --git a/soapbox/wsdl2py.py b/soapbox/wsdl2py.py new file mode 100644 index 00000000..ee5ff826 --- /dev/null +++ b/soapbox/wsdl2py.py @@ -0,0 +1,105 @@ +import sys +from optparse import OptionParser +from lxml import etree +from jinja2 import Template,Environment +from wsdl import Definitions +from utils import removens, classyfiy, get_get_type, use, find_xsd_namepsace,urlcontext +from xsd2py import TEMPLATE as SCHEMA_TEMPLATE + +environment = Environment() +environment.filters["class"] = classyfiy +environment.filters["removens"] = removens +environment.filters["use"] = use +environment.filters["urlcontext"] = urlcontext + +TEMPLATE = """#This was was generated by wsdl2py, try to not edit. +from soapbox import soap +{%- if schema %} +{{schema}} +{%- else %} +from soapbox import xsd +{%- endif %} + +{%- for service in definitions.services %} + +{%- set binding = definitions.get_by_name(definitions.bindings, service.port.binding) %} +{%- set portType = definitions.get_by_name(definitions.portTypes, binding.type) %} +{%- set inputMessage = definitions.get_by_name(definitions.messages, portType.operation.input.message) %} +{%- set outputMessage = definitions.get_by_name(definitions.messages, portType.operation.output.message) %} + +{% if is_server %} +def {{binding.operation.name}}({{inputMessage.part.element|removens}}): + #Put your implementation here. + return {{outputMessage.part.element|removens}} +{%- endif %} + +{{binding.operation.name}}_method = xsd.Method( + {%- if is_server %}function = {{binding.operation.name}},{% endif %} + soapAction = "{{binding.operation.operation.soapAction}}", + {%- if inputMessage.part.element %} + input = "{{inputMessage.part.element|removens}}",#Pointer to Schema.elements + {%- else %} + input = {{inputMessage.part.type|removens|class}}, + {%- endif %} + {%- if inputMessage.part.element %} + output = "{{outputMessage.part.element|removens}}",#Pointer to Schema.elements + {%- else %} + input = {{outputMessage.part.type|removens|class}}, + {%- endif %} + operationName = "{{binding.operation.name}}") + +SERVICE = soap.Service( + targetNamespace = "{{definitions.targetNamespace}}", + location = "{{service.port.address.location}}", + schema = Schema, + methods = [{{binding.operation.name}}_method, ]) + +{% if is_server %} +#Comment this lines for py2xsd generation to avoid error message about +#DJANGO_SETTINGS_MODULE not being set. If authorization is required +#dispatch can be wrapped with login required in way similar to csrf_exempt. +from django.views.decorators.csrf import csrf_exempt +dispatch = csrf_exempt(soap.get_django_dispatch(SERVICE)) + +#Put this lines in your urls.py: +#urlpatterns += patterns('', +# (r"{{service.port.address.location|urlcontext}}", ".dispatch") +#) +{%- else %} +class ServiceStub(soap.Stub): + SERVICE = SERVICE + + def {{binding.operation.name}}(self, {{inputMessage.part.element|removens}}): + return self.call("{{binding.operation.name}}", {{inputMessage.part.element|removens}}) +{%- endif %} +{% endfor %} +""" + +def main(is_server, path): + xml = open(path).read() + xmlelement = etree.fromstring(xml) + XSD_NAMESPACE = find_xsd_namepsace(xmlelement.nsmap) + environment.filters["type"] = get_get_type(XSD_NAMESPACE) + definitions = Definitions.parse_xmlelement(xmlelement) + schema = definitions.types.schema + schemaxml = environment.from_string(SCHEMA_TEMPLATE).render(schema=schema) + print environment.from_string(TEMPLATE).render( + definitions=definitions, + schema=schemaxml, + is_server=is_server) + +if __name__ == "__main__": + parser = OptionParser(usage = "usage: %prog [-c|-s] path_to_wsdl") + parser.add_option("-c", "--client", dest="client", + help="Generate webservice http client code.") + parser.add_option("-s", "--server", dest="server", + help="Generate webservice Django server code.") + (options, args) = parser.parse_args() + if options.client and options.server: + parser.error("Options -c and -s are mutually exclusive") + elif options.client: + main(False, options.client) + elif options.server: + main(True, options.server) + else: + parser.print_help() \ No newline at end of file diff --git a/soapbox/xsd.py b/soapbox/xsd.py new file mode 100644 index 00000000..4765faa3 --- /dev/null +++ b/soapbox/xsd.py @@ -0,0 +1,668 @@ +from lxml import etree +from datetime import datetime +from copy import copy +#http://lxml.de/validation.html#xmlschema + +#Design Decision Log: +#0. I have decided to not use dews/dexml approach to field description +# as it doesn't give good distinction between element and attribute. +# It is not a problem when parsing a XML, but it is quite important +# for rendering and XSD generation. The new syntax will look like: +# tail_number = xsd.Attribute(xsd.String) +# flight_number = xsd.Element(xsd.Interger) +# which makes this distinction clear. +# +#1. render will take value/instance as parameter +# More obvious would be if render just rendered current object, +# but this approach doesn't work with Python simple types like string. +# Where you can not call "x".render() so type method render must +# take a value as a parameter, which may same odd for complex types. +# +#2. Due to render taking a value as parameter it could be implemented +# as a static/class method, but it is not. +# xsd.Element takes a class or an instance, but if class was passed +# it will create an instance - so parameterless constructor is required +# Reason for that is to keep API consistent. There are two syntaxes +# a) xsd.Element(xsd.String) +# b) xsd.Element(xsd.String(enumeration=["A","B"]) +# Because instance if required in case b) creating it from class in case +# a) makes other methods independent from this two syntaxes. + + +class Use: + OPTIONAL = "optional" + REQUIRED = "required" + PROHIBITED = "prohibited" + +class Inheritance: + RESTRICTION = "RESTRICTION" + EXTENSION = "EXTENSION" + +class ElementFormDefault: + QUALIFIED = "qualified" + UNQUALIFIED = "unqualified" + +class Type(object): + """Abstract.""" + def accept(self, value): + raise NotImplementedError + + def parse_xmlelement(self, xmlelement): + raise NotImplementedError + + def parsexml(self, xml): + raise NotImplementedError + + def rander(self, parent, value): + raise NotImplementedError + +class SimpleType(Type): + """Defines an interface for simple types.""" + def render(self, parent, value, namespace): + parent.text = self.xmlvalue(value) + + def parse_xmlelement(self, xmlelement): + return self.pythonvalue(xmlelement.text) + + def xmlvalue(self, value): + raise NotImplementedError + + def pythonvalue(self, xmlavalue): + raise NotImplementedError + + +class String(SimpleType): + enumeration = None#To be defined in child. + def __init__(self, enumeration=None): + if enumeration: + self.enumeration = enumeration + + def accept(self,value): + if value is None: + return value + if not isinstance(value,str): + raise ValueError("Value '%s' for class '%s'." % (str(value),self.__class__.__name__)) + if self.enumeration: + if value in self.enumeration: + return value + else: + raise ValueError("Value '%s' not in list %s." % (str(value), self.enumeration)) + else: + return value + + def xmlvalue(self, value): + return value + + def pythonvalue(self, xmlvalue): + return xmlvalue + +class Boolean(SimpleType): + def accept(self, value): + if value in [True, False, None]: + return value + else: + raise ValueError("Value '%s' for class '%s'." % (str(value),self.__class__.__name__)) + + def xmlvalue(self, value): + if value == True: + return "true" + elif value == False: + return "false" + elif value is None: + return "nil" + else: + raise ValueError("Value '%s' for class '%s'." % (str(value),self.__class__.__name__)) + + def pythonvalue(self,value): + if value == 'false': + return False + elif value == 'true': + return True + elif value == 'nil' or value is None: + return None + else: + raise ValueError + +class DateTime(SimpleType): + """Example text value: 2001-10-26T21:32:52""" + FORMTA = "%Y-%m-%dT%H:%M:%S" + def accept(self, value): + if value is None: + return None + elif isinstance(value, datetime): + return value + elif isinstance(value, str): + return datetime.strptime(value, self.FORMTA) + raise ValueError("Incorrect type value '%s' for Datetime field." % value) + + def xmlvalue(self, value): + if value is None: + return "nil" + else: + return value.strftime(self.FORMTA) + + def pythonvalue(self, value): + if value is None or value == 'nil': + return None + else: + return datetime.strptime(value, self.FORMTA) + +class Integer(SimpleType): + def __init__(self, enumeration = None, fractionDigits=None, maxExclusive=None, + maxInclusive=None, minExclusive=None, minInclusive=None, + pattern=None, totalDigits=None): + pass + + def accept(self, value): + if value is None: + return None + elif isinstance(value, int): + return value + elif isinstance(value, str): + return int(value) + else: + raise ValueError("Incorrect value '%s' for Interger field." % value) + + def xmlvalue(self, value): + return str(value) + + def pythonvalue(self, xmlvalue): + if xmlvalue == 'nil': + return None + else: + return self.accept(xmlvalue) + +class Long(Integer): + def accept(self, value): + value = super(Long, self).accept(value) + if value is None: + return None + else: + if -9223372036854775808 < value < 9223372036854775807: + return value + else: + raise ValueError("Value '%s' out of range for Long type: -9223372036854775808 and 9223372036854775807.") + + + + +class Element(object): + """Basic building block, represents a XML element that can appear one or zero + times in XML that should be rendered as subelement e.g. + LN-KKY + Tail number is element. + For elements that can appear multiple times use ListElement.""" + _creation_counter = 0 + + def __init__(self, _type, minOccurs = 1, tagname = None, nilable = False, + default = None): + """:param _type: Class or instance of class that inherits from Type, + usually a child of SimpleType from xsd package, + or user defined class that inherits from ComplexType. + :param minOccurs: int, how many times this object can appear in valid XML + can be 0 or 1. See: difference between Element and + ListElement. + :param tagname: str, name of tag when different to field declared in + ComplexType, important when tag name is python reserved + work e.g. import + :param nilable: bool, is object nilable. + """ + if not minOccurs in [0,1]: raise "minOccurs for Element can by only 0 or 1, use ListElement insted." + self._creation_number = Element._creation_counter + Element._creation_counter += 1 + if isinstance(_type, Type): + self._type = _type + else: + self._type = _type() + self._minOccurs = minOccurs + self.tagname = tagname + self.default = default + + def empty_value(self): + """Empty value methods is used when new object is constructed for + field initialization in most cases this should be None, but for lists + and other types of aggregates this should by an empty aggregate.""" + return self.default + + def accept(self,value): + """Checks is the value correct from type defined in constructions.""" + return self._type.accept(value) + + def render(self, parent, field_name, value, namespace=None): + if value is None: + return + #This allows complexType to redefine the name space a.k.a. + #to use name space different then parent's one. + if hasattr(self._type,"NAMESPACE"): + namespace = self._type.NAMESPACE + if namespace and self._type.ELEMENT_FORM_DEFAULT == ElementFormDefault.QUALIFIED : + field_name = "{%s}%s" % (namespace, field_name) + xmlelement = etree.Element(field_name) + self._type.render(xmlelement, value, namespace) + parent.append(xmlelement) + + + def parse(self, instance, field_name, xmlelement): + value = self._type.parse_xmlelement(xmlelement) + setattr(instance, field_name, value) + + def __repr__(self): + return "%s<%s>" % (self.__class__.__name__,self._type.__class__.__name__) + +class ClassNamedElement(Element): + """Use this element when tagname should be based on class name in rendering time.""" + def __init__(self,_type, minOccurs = 1, nilable = False): + super(ClassNamedElement, self).__init__(_type, minOccurs,None,nilable) + + def render(self, parent, field_name, value, namespace=None): + if value is None: + return + if hasattr(value,"NAMESPACE"): + namespace = value.NAMESPACE + + if namespace: + tagname = "{%s}%s" % (namespace, value.__class__.__name__.lower()) + else: + tagname = value.__class__.__name__ + xmlelement = etree.Element(tagname) + self._type.render(xmlelement, value) + parent.append(xmlelement) + + +class Attribute(Element): + """Represents a field that is a XML attribute. e.g. + + Programmer + + name and surname are attributes. Attribute type can be only simple types.""" + def __init__(self,type_clazz, use=Use.REQUIRED, tagname = None,nilable = False, + default=None): + """ + :param type_clazz: Only simple tapes are accepted: String, Integer etc. + """ + if use == Use.REQUIRED: + minOccurs = 1 + else: + minOccurs = 0 + super(Attribute, self).__init__(type_clazz, tagname=tagname, minOccurs = minOccurs) + self.nilable = nilable + self.use = use + self.default = default + + def render(self, parent, field_name, value, namespace=None): + if value is None: + if self._minOccurs: + raise ValueError("Value None is not acceptable for required field.") + elif not self.nilable: + return + xmlvalue = self._type.xmlvalue(value) + parent.set(field_name, xmlvalue) + + def parse(self, instance, field_name, xmlelement): + xmlvalue = xmlelement.get(field_name) + if xmlvalue is None: + xmlvalue = self.default + value = self._type.pythonvalue(xmlvalue) + setattr(instance, field_name, value) + + +class Ref(Element): + """References are not fields, they point to type that has them - usually groups. + With Ref fields will be rendered directly into parent object. e.g. + class Person(xsd.Group): + name = xsd.Element(xsd.String) + surname = xsd.Element(xsd.String) + class Job(xsd.ComplexType): + title = xsd.Element(xsd.String) + person = xsd.Ref(Person) + The valid XML will be: + + Programmer + An + Brown + + Note that name and surname are not wrapped with tag. + """ + def empty_value(self): + return copy(self._type) + + def render(self, parent, field_name, value, namespace=None): + if value is None: + if self._required: + raise ValueError("Value None is not acceptable for required field.") + else: + return + self._type.render(parent, value, namespace) + +class Content(Ref): + """Direct access to element.text. Note that <> will be escaped.""" + def empty_value(self): + return None + + +class ListElement(Element): + """Tag element that can appear many times in valid XML. e.g. + + G-ABCD + John Backus + Kent Beck + Larry Breed + + passenger is an example of ListElement, the definition would look: + passengers = xsd.ListElement(xsd.String, "passenger") + Note that tag name is required for this field, as the field name should + be in plural form, and tag usually is not. + """ + def __init__(self, clazz, tagname, minOccurs=None,maxOccurs=None): + super(ListElement,self).__init__(clazz) + self.minOccurs = minOccurs + self.maxOccurs = maxOccurs + self.tagname = tagname + + def accept(self,value): + return value + + def empty_value(this): + class TypedList(list): + def append(self,value): + accepted_value = this._type.accept(value) + super(TypedList,self).append(accepted_value) + return TypedList() + + + def render(self, parent, field_name, value, namespace=None): + items = value#The value must be list of items. + if self.minOccurs and len(items) < self.minOccurs: + raise ValueError("For %s minOccurs=%d but list length %d." %(name, self.minOccurs, len(items))) + if self.maxOccurs and len(items) > self.maxOccurs: + raise ValueError("For %s maxOccurs=%d but list length %d." % (name, self.maxOccurs)) + + for item in items: + if namespace: + tagname = "{%s}%s" % (namespace,self.tagname) + else: + tagname = self.tagname + xmlelement = etree.Element(tagname) + self._type.render(xmlelement, item, namespace) + parent.append(xmlelement) + + def parse(self, instance, field_name, xmlelement): + value = self._type.parse_xmlelement(xmlelement) + _list = getattr(instance, field_name) + _list.append(value) + +class ComplexTypeMetaInfo(object): + def __init__(self,cls): + self.cls = cls + self.fields = [] + self.attributes = [] + self.groups = [] + for attr in dir(cls): + item = getattr(cls,attr) + if isinstance(getattr(cls,attr),Attribute): + item._name = attr + self.attributes.append(item) + elif isinstance(item, Ref): + item._name = attr + self.groups.append(item) + elif isinstance(item, Element): + item._name = attr + self.fields.append(item) + self.fields = sorted(self.fields, key=lambda f: f._creation_number) + self.attributes = sorted(self.attributes, key=lambda f: f._creation_number) + self.groups = sorted(self.groups, key=lambda f: f._creation_number) + self.allelements = sorted(self.fields+self.groups, key=lambda f: f._creation_number) + self.all = sorted(self.fields+self.groups+self.attributes, key=lambda f: f._creation_number) + +class Complex_PythonType(type): + """Python type for ComplexType, builds _meta object for every class that + inherit from ComplexType. """ + def __new__(cls,name,bases,attrs): + newcls = super(Complex_PythonType,cls).__new__(cls,name,bases,attrs) + if name != 'Complex': + newcls._meta = ComplexTypeMetaInfo(newcls) + return newcls + +class ComplexType(Type): + """Parent for XML elements that have sub-elements.""" + INDICATOR = None#Indicator see: class Indicators. To be defined in sub-type. + INHERITANCE = None#Type of inheritance see: class Inheritance, to be defined in sub-type. + NAMESPACE = None#String, preferably URL with name space for this element. Is set be Scheme instance. + ELEMENT_FORM_DEFAULT = None#String, one of two values. + + __metaclass__ = Complex_PythonType + + def __new__(cls,*args,**kwargs): + instance = super(ComplexType,cls).__new__(cls) + for field in instance._meta.all: + setattr(instance,field._name,field.empty_value()) + return instance + + def __init__(self,**kwargs): + for key,value in kwargs.items(): + setattr(self,key,value) + + def __setattr__(self, attr, value): + if attr == "_xmlelement": + super(ComplexType,self).__setattr__(attr,value) + else: + try: + field = self._find_field(self._meta.all, attr) + except IndexError: + raise AttributeError("Model '%s' doesn't have attribute '%s'." % (self.__class__.__name__,attr)) + super(ComplexType,self).__setattr__(attr,field.accept(value)) + + def accept(self, value): + """Instance methods that valid other instance.""" + if value is None: + return None + elif isinstance(value,self.__class__): + return value + else: + raise ValueError('!!') + + + def render(self, parent, instance, namespace=None): + if instance is None: return None + for field in instance._meta.all: + if self.ELEMENT_FORM_DEFAULT == ElementFormDefault.QUALIFIED: + field.render( + parent = parent, + field_name = field._name, + value = getattr(instance, field._name), + namespace = self.NAMESPACE) + else: + field.render( + parent = parent, + field_name = field._name, + value = getattr(instance, field._name)) + + @classmethod + def _find_field(cls, fields, name): + return filter(lambda f:f._name == name,fields)[0] + + @classmethod + def _get_field_by_name(cls, fields, field_name): + for field in fields: + if field.tagname == field_name or field._name == field_name: + return field + raise ValueError("Field not found '%s', fields: %s" %(field_name, fields)) + + + @classmethod + def _find_subelement(cls, field, xmlelement): + def gettagns(tag): + """Translates tag string in format {namespace} + tag to tuple (namespace,tag).""" + if tag[:1] == "{": + return tag[1:].split("}", 1) + else: + return (None,tag) + #-------------------------------------------- + subelements = [] + for subelement in xmlelement: + if isinstance(subelement, etree._Comment): + continue + ns, tag = gettagns(subelement.tag) + if tag == field._name or tag == field.tagname: + subelements.append(subelement) + return subelements + + @classmethod + def parse_xmlelement(cls, xmlelement): + instance = cls() + instance._xmlelement = xmlelement + for attribute in instance._meta.attributes: + attribute.parse(instance, attribute._name, xmlelement) + + for field in instance._meta.fields: + subelements = cls._find_subelement(field, xmlelement) + for subelement in subelements: + field.parse(instance, field._name, subelement) + + for group in instance._meta.groups: + group.parse(instance, group._name, xmlelement) + + return instance + + @classmethod + def __parse_with_validation(cls, xml, schema): + from py2xsd import generate_xsd + schemaelement = etree.XMLSchema(generate_xsd(schema)) + parser = etree.XMLParser(schema = schemaelement) + xmlelement = etree.fromstring(xml, parser) + return xmlelement + + @classmethod + def parsexml(cls, xml, schema=None): + if schema: + xmlelement = cls.__parse_with_validation(xml, schema) + else: + xmlelement = etree.fromstring(xml) + return cls.parse_xmlelement(xmlelement) + + def xml(self, tagname, schema=None): + if self.NAMESPACE: + tagname = "{%s}%s" % (self.NAMESPACE, tagname) + xmlelement = etree.Element(tagname) + self.render(xmlelement, self, self.NAMESPACE) + xml = etree.tostring(xmlelement, pretty_print=True) + if schema: + self.__parse_with_validation(xml, schema) + return xml + +class Group(ComplexType): + """Parent object for XSD Groups. Marker. Must be use with Ref.""" + pass + +class AttributeGroup(Group): + """Parent object for XSD Attribute Groups. Marker. Must be use with Ref.""" + pass + +class Document(ComplexType): + """Represents whole xml, is expected to have only one field the root.""" + class MockElement(object): + def __init__(self): + self.element = None + def append(self, element): + self.element = element + + def render(self): + field = self._meta.fields[0]#The only field + mockelement = Document.MockElement() + instance = getattr(self, field._name) + field.render(mockelement, field._name, instance, self.NAMESPACE) + return etree.tostring(mockelement.element, pretty_print=True) + + #TODO:schema support + @classmethod + def parsexml(cls, xml): + field = self._meta.fields[0]#The only field + xmlelement = etree.fromstring(xml) + field.parse(self, field._name, xmlelement) + + + +class Indicator(object): + def __init__(self, fields): + self.fields = fields + +class Sequence(Indicator): + pass + +class Choice(Indicator): + pass + +class All(Indicator): + pass + +class List(SimpleType): + pass + +class AnyURI(String): + pass + +class QName(String): + pass + +class NMTOKEN(String): + pass + +class NMTOKENS(String): + pass + + +class Schema(object): + """Main object for XSD schema. This object is required for XSD and WSDLgeneration + and correct namespaces as it propagates targetNamespace to all objects. + Instance of this is expected to be named Schema. """ + def __init__(self,targetNamespace, elementFormDefault=ElementFormDefault.UNQUALIFIED ,simpleTypes=[], attributeGroups=[], groups=[], complexTypes=[], elements={}): + """ + :param targetNamespace: string, xsd namespace URL. + :param elementFormDefault: unqualified/qualified Defines should namespace + be used in child elements. Suggested: qualified. Default: unqualified as + it is default in XSD. + :param simpleTypes: List of objects that extend xsd.SimpleType. + :param attributeGroups: List of objects that extend xsd.AttributeGroup. + :param groups: List of objects that extend xsd.Group. + :param complexTypes: List of complexTypes class. + :param elements: dict of xsd.Elements that are direct schema elements. + """ + self.targetNamespace = targetNamespace + self.elementFormDefault = elementFormDefault + self.simpleTypes = simpleTypes + self.attributeGroups = attributeGroups + self.groups = groups + self.complexTypes = complexTypes + self.elements = elements + + self.__init_namespace(self.simpleTypes) + self.__init_namespace(self.groups) + self.__init_namespace(self.attributeGroups) + self.__init_namespace(self.complexTypes) + + def __init_namespace(self, types): + for _type in types: + _type.NAMESPACE = self.targetNamespace + _type.ELEMENT_FORM_DEFAULT = self.elementFormDefault + + +class Method(object): + """Method description. The main information is mapping soapAction and operationName + to function for dispatcher. input and output mapping informs how and which + objects should be created on incoming/outgoing messages.""" + def __init__(self, operationName, soapAction, input, output, function=None): + """:param function: The function that should be called. Required only for server.""" + self.operationName = operationName + self.soapAction = soapAction + self.input = input + self.output = output + self.function = function + + + + + + + + \ No newline at end of file diff --git a/soapbox/xsd2py.py b/soapbox/xsd2py.py new file mode 100644 index 00000000..223d241d --- /dev/null +++ b/soapbox/xsd2py.py @@ -0,0 +1,133 @@ +import sys +from jinja2 import Template,Environment +from xsdspec import * +from utils import removens, classyfiy, get_get_type, use, find_xsd_namepsace + + +environment = Environment() +environment.filters["class"] = classyfiy +environment.filters["removens"] = removens +environment.filters["use"] = use + + +TEMPLATE = """from soapbox import xsd +{# ------------------ SimpleType Generation ---------------------#} +{% for st in schema.simpleTypes %} + {%- if st.restriction %} +class {{st.name|class}}({{st.restriction.base|type}}): + {%- if st.restriction.enumerations %} + enumeration = [{% for enum in st.restriction.enumerations %} "{{enum.value}}", {% endfor %}] + {%- elif st.restriction.pattern %} + pattern = r"{{st.restriction.pattern.value}}" + {%- endif %} + {% endif %} + + {%- if st.list %} +class {{st.name|class}}(xsd.List): + pass + {%- endif %} +{%- endfor %} +{# ---------------End of SimpleType Generation -----------------#} + +{# ------------------------- GROUOPS ----------------------------------------#} +{%- for attrGroup in schema.attributeGroups %} +class {{attrGroup.name|class}}(xsd.AttributeGroup): + {%- for attribute in attrGroup.attributes %} + {{attribute.name}} = xsd.Attribute({{attribute.type|type}}{% if attribute.use %}, use={{attribute.use|use}}{% endif %}) + {%- endfor %} +{% endfor %} + +{%- for group in schema.groups %} +class {{group.name|class}}(xsd.Group): + {%- for element in group.sequence.elements %} + {%- if element.ref %} + {{element.ref|removens}} = xsd.Element({{element.ref|type}}) + {%- else %} + {{element.name}} = xsd.Element({{element.type|type}}) + {%- endif %} + {%- endfor %} +{% endfor %} + +{# ---------------------------------------------------------------------------#} + +{# -------------------------- ComplexTypes -----------------------------------#} +{% for ct in schema.complexTypes %} +{% set content = ct %} + +{%- if not ct.sequence and not ct.complexContent %} +class {{ct.name|class}}(xsd.ComplexType): +{%- endif %} + +{%- if ct.complexContent %} + {%- if ct.complexContent.restriction %} +class {{ct.name|class}}({{ct.complexContent.restriction.base|type}}): + INHERITANCE = xsd.Inheritance.RESTRICTION + {%- set content = ct.complexContent.restriction %} + {%- else %} +class {{ct.name|class}}({{ct.complexContent.extension.base|type}}): + INHERITANCE = xsd.Inheritance.EXTENSION + {%- set content = ct.complexContent.extension %} + {%- endif %} +{%- elif ct.sequence %} +class {{ct.name|class}}(xsd.ComplexType): + INHERITANCE = None + {%- set content = ct %} +{%- endif %} + +{%- if content.sequence %} + INDICATOR = xsd.Sequence + {%- set elements = content.sequence.elements %} +{%- elif content.all %} + INDICATOR = xsd.All + {%- set elements = content.all.elements %} +{%- elif content.choice %} + INDICATOR = xsd.Choice + {%- set elements = content.choice.elements %} +{%- endif %} + +{%- for attribute in content.attributes %} + {%- if attribute.ref %} + {{attribute.ref|removens}} = xsd.Attribute({{attribute.ref|type}}) + {%- else %} + {{attribute.name}} = xsd.Attribute({{attribute.type|type}}{% if attribute.use %}, use={{attribute.use|use}}{% endif %}) + {%- endif %} +{%- endfor %} + +{%- for attrGroupRef in content.attributeGroups %} + {{attrGroupRef.ref|removens}} = xsd.Ref({{attrGroupRef.ref|type}}) +{%- endfor %} + +{%- for element in elements %} + {%- if element.type %} + {{element.name}} = xsd.Element({{element.type|type}}{% if element.minOccurs == 0 %}, minOccurs=0{% endif %}) + {%- endif %} + {%- if element.simpleType %} + {{element.name}} = xsd.Element({{element.simpleType.restriction.base|type}}( enumeration = + [{% for enum in element.simpleType.restriction.enumerations %} "{{enum.value}}",{% endfor %}]) ) + {%- endif %} + {%- if element.ref %} + {{element.ref|removens}} = xsd.Ref({{element.ref|type}}) + {%- endif %} +{%- endfor %} +{% endfor %} +{# ------------------------ End of ComplexTypes -------------------------------#} + +Schema = xsd.Schema( + targetNamespace = "{{schema.targetNamespace}}", + elementFormDefault = "{{schema.elementFormDefault}}", + simpleTypes = [{% for st in schema.simpleTypes %} {{st.name|class}},{% endfor %}], + attributeGroups = [{% for ag in schema.attributeGroups %} {{ag.name|class}},{% endfor %}], + groups = [{% for g in schema.groups %} {{g.name|class}},{% endfor %}], + complexTypes = [{% for ct in schema.complexTypes %} {{ct.name|class}},{% endfor %}], + elements = { {% for e in schema.elements %} "{{e.name}}":xsd.Element({{e.type|type}}),{% endfor %}}) +""" + +XSD_NAMESPACE = None + +if __name__ == "__main__": + xml = open(sys.argv[1]).read() + xmlelement = etree.fromstring(xml) + XSD_NAMESPACE = find_xsd_namepsace(xmlelement.nsmap) + environment.filters["type"] = get_get_type(XSD_NAMESPACE) + schema = Schema.parse_xmlelement(etree.fromstring(xml)) + print environment.from_string(TEMPLATE).render(schema=schema) \ No newline at end of file diff --git a/soapbox/xsdspec.py b/soapbox/xsdspec.py new file mode 100644 index 00000000..25f27cad --- /dev/null +++ b/soapbox/xsdspec.py @@ -0,0 +1,128 @@ +from lxml import etree +import xsd + +class Enumeration(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + value = xsd.Attribute(xsd.String) + + @classmethod + def create(cls, value): + enum= Enumeration() + enum.value = value + return enum + +class Pattern(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + value = xsd.Attribute(xsd.String) + +class Restriction(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + base = xsd.Attribute(xsd.String) + enumerations = xsd.ListElement(Enumeration,"enumeration") + pattern = xsd.Element(Pattern) + + def to_python(self): + enum_values = map(lambda e: '"%s"'%e.value, self.enumerations) + return "[%s]" % ",".join(enum_values) + +class List(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + pass + +class SimpleType(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + name = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + restriction = xsd.Element(Restriction,minOccurs=0) + list = xsd.Element(List,minOccurs=0) + + +class Element(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + name = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + type = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + ref = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + minOccurs = xsd.Attribute(xsd.Integer, use=xsd.Use.OPTIONAL) + simpleType = xsd.Element(SimpleType, minOccurs=0) + + +class Sequence(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + elements = xsd.ListElement(Element, "element") + + +class Attribute(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + name = xsd.Attribute(xsd.String) + ref = xsd.Attribute(xsd.String) + type = xsd.Attribute(xsd.String) + use = xsd.Attribute(xsd.String) + + +class AttributeGroup(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + name = xsd.Attribute(xsd.String) + attributes = xsd.ListElement(Attribute, "attribute") + +class AttributeGroupReference(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + ref = xsd.Attribute(xsd.String) + + def to_python(self): + typename = get_type(self.ref) + data = {"name": typename.lower(), "type" : typename} + return """ %(name)s = xsd.Ref(%(type)s)\n""" % data + +class Extension(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + base = xsd.Attribute(xsd.String) + sequence = xsd.Element(Sequence) + attributes = xsd.ListElement(Attribute, "attribute") + attributeGroups = xsd.ListElement(AttributeGroupReference,"attributeGroup") + +class ComplexContent(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + mixed = xsd.Attribute(xsd.Boolean) + extension = xsd.Element(Extension) + restriction = xsd.Element(Extension) + + +class ComplexType(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + name = xsd.Attribute(xsd.String) + sequence = xsd.Element(Sequence) + all = xsd.Element(Sequence) + complexContent = xsd.Element(ComplexContent) + attributes = xsd.ListElement(Attribute,"attribute") + attributeGroups = xsd.ListElement(AttributeGroupReference, "attributeGroup") + +class Group(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + name = xsd.Attribute(xsd.String) + sequence = xsd.Element(Sequence) + + +class Schema(xsd.ComplexType): + NAMESPACE = "http://www.w3.org/2001/XMLSchema" + targetNamespace = xsd.Attribute(xsd.String) + elementFormDefault = xsd.Attribute(xsd.String(enumeration=["qualified", "unqualified"]), + use=xsd.Use.OPTIONAL, default = "unqualified") + simpleTypes = xsd.ListElement(SimpleType,"simpleType") + groups = xsd.ListElement(Group,"group") + attributeGroups = xsd.ListElement(AttributeGroup,"attributeGroup") + complexTypes = xsd.ListElement(ComplexType,"complexType") + elements = xsd.ListElement(Element,"element") + + +SCHEMA = xsd.Schema( + targetNamespace = "http://www.w3.org/2001/XMLSchema", + elementFormDefault = xsd.ElementFormDefault.QUALIFIED, + simpleTypes = [], + attributeGroups = [], + groups = [], + complexTypes = [Enumeration, Pattern, Restriction, List, SimpleType, Element, + Sequence, Attribute, AttributeGroup, AttributeGroupReference, + Extension, ComplexContent, ComplexType, Group, Schema ], + elements = {}) + + + diff --git a/tests/ops.py b/tests/ops.py new file mode 100644 index 00000000..39a90681 --- /dev/null +++ b/tests/ops.py @@ -0,0 +1,100 @@ +import unittest +from datetime import datetime +from soapbox import xsd + +class Pilot(xsd.String): + enumeration = [ "CAPTAIN", "FIRST_OFFICER", ] + +class Airport(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + code_type = xsd.Element(xsd.String( enumeration = + [ "ICAO", "IATA", "FAA",]) ) + code = xsd.Element(xsd.String) + + +class Weight(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + value = xsd.Element(xsd.Integer) + unit = xsd.Element(xsd.String( enumeration = + [ "kg", "lb",]) ) + + +class Ops(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + aircraft = xsd.Element(xsd.String) + flight_number = xsd.Element(xsd.String) + type = xsd.Element(xsd.String( enumeration = + [ "COMMERCIAL", "INCOMPLETE", "ENGINE_RUN_UP", "TEST", "TRAINING", "FERRY", "POSITIONING", "LINE_TRAINING",]) ) + takeoff_airport = xsd.Element(Airport) + takeoff_gate_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + takeoff_datetime = xsd.Element(xsd.DateTime) + takeoff_fuel = xsd.Element(Weight, minOccurs=0) + takeoff_gross_weight = xsd.Element(Weight, minOccurs=0) + takeoff_pilot = xsd.Element(Pilot, minOccurs=0) + landing_airport = xsd.Element(Airport) + landing_gate_datetime = xsd.Element(xsd.DateTime, minOccurs=0) + landing_datetime = xsd.Element(xsd.DateTime) + landing_fuel = xsd.Element(Weight, minOccurs=0) + landing_pilot = xsd.Element(Pilot, minOccurs=0) + destination_airport = xsd.Element(Airport, minOccurs=0) + captain_code = xsd.Element(xsd.String, minOccurs=0) + first_officer_code = xsd.Element(xsd.String, minOccurs=0) + V2 = xsd.Element(xsd.Integer, minOccurs=0) + Vref = xsd.Element(xsd.Integer, minOccurs=0) + Vapp = xsd.Element(xsd.Integer, minOccurs=0) + + +class Status(xsd.ComplexType): + INHERITANCE = None + INDICATOR = xsd.Sequence + action = xsd.Element(xsd.String( enumeration = + [ "INSERTED", "UPDATED", "EXISTS",]) ) + id = xsd.Element(xsd.Long) + +Schema = xsd.Schema( + targetNamespace = "http://flightdataservices.com/ops.xsd", + simpleTypes = [ Pilot,], + attributeGroups = [], + groups = [], + complexTypes = [ Airport, Weight, Ops, Status,], + elements = { "ops":xsd.Element(Ops), "status":xsd.Element(Status),}) + +XML_REQUIRED_ONLY = """ + + N608WB + 123123 + COMMERCIAL + + + ICAO + EGLL + + + 2009-12-30T21:35:59 + 2009-12-30T21:39:59 + + ICAO + EPWA + + + 2009-12-30T23:35:59 + 2009-12-30T23:32:59 +""" + +class OPS_Test(unittest.TestCase): + def test_required_only(self): + ops = Ops.parsexml(XML_REQUIRED_ONLY, Schema) + self.assertEqual("N608WB", ops.aircraft) + self.assertEqual("123123", ops.flight_number) + self.assertEqual("COMMERCIAL", ops.type) + self.assertEqual("ICAO", ops.takeoff_airport.code_type) + self.assertEqual("EGLL", ops.takeoff_airport.code) + self.assertEqual(None, ops.takeoff_pilot) + self.assertEqual(datetime(2009,12,30,23,35,59),ops.landing_gate_datetime) + +if __name__ == "__main__": + unittest.main() + \ No newline at end of file diff --git a/tests/xsd.py b/tests/xsd.py new file mode 100644 index 00000000..55b40923 --- /dev/null +++ b/tests/xsd.py @@ -0,0 +1,548 @@ +import unittest +from datetime import datetime +from lxml import etree +from soapbox import xsd, xsdspec + +class Aircraft(xsd.ComplexType): + tail_number = xsd.Attribute(xsd.String) + +class Airport(xsd.ComplexType): + type = xsd.Element(xsd.String) + code = xsd.Element(xsd.String) + + @classmethod + def create(cls, type, code): + airport = Airport() + airport.type = type + airport.code = code + return airport + +class Pilot(xsd.String): + enumeration = ["CAPTAIN","FIRST_OFFICER"] + +class Flight(xsd.ComplexType): + tail_number = xsd.Element(xsd.String) + takeoff_datetime = xsd.Element(xsd.DateTime, minOccurs = 0) + takeoff_airport = xsd.Element(Airport) + landing_airport = xsd.Element(Airport) + takeoff_pilot = xsd.Element(Pilot, minOccurs = 0) + landing_pilot = xsd.Element(Pilot, minOccurs = 0) + passangers = xsd.ListElement(xsd.String, "passanger", maxOccurs=10,minOccurs=0) + +class ElementTest(unittest.TestCase): +# This logic have been moved to post rendering validation +# uncomment when implemented. +# def test_required(self): +# tail_number = xsd.Element(xsd.String) +# try: +# xmlelement = etree.Element("aircraft") +# tail_number.render(xmlelement, "tail_number", None) +# except ValueError: +# pass +# else: +# raise AssertionError("Should get here") + + def test_string_element(self): + tail_number = xsd.Element(xsd.String()) + xmlelement = etree.Element("aircraft") + tail_number.render(xmlelement,"tail_number", "LN-KKU") + self.assertEqual(""" + LN-KKU + +""", + etree.tostring(xmlelement, pretty_print=True)) + + + def test_complex_type_element(self): + airport = Airport() + airport.type = "IATA" + airport.code = "WAW" + xmlelement = etree.Element("takeoff_airport") + airport.render(xmlelement, airport) + expected_xml = """ + IATA + WAW + +""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + +class ListElementTest(unittest.TestCase): + def test_rendering_simple_type(self): + passangers = xsd.ListElement(xsd.String,"passanger", maxOccurs=10,minOccurs=0) + passangers_list = ["abc", "123"] + xmlelement = etree.Element("flight") + passangers.render(xmlelement, "passanger", passangers_list) + expected_xml = """ + abc + 123 + +""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + +class BooleanTypeTest(unittest.TestCase): + def test_element_true(self): + mixed = xsd.Element(xsd.Boolean,) + xmlelement = etree.Element("complexType") + mixed.render(xmlelement,"mixed", True) + expected_xml = """ + true + +""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + + def test_attribute_false(self): + mixed = xsd.Attribute(xsd.Boolean) + xmlelement = etree.Element("complexType") + mixed.render(xmlelement,"mixed", True) + expected_xml = """\n""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + + def test_attribute_nil(self): + mixed = xsd.Attribute(xsd.Boolean, nilable = True, use=xsd.Use.OPTIONAL) + xmlelement = etree.Element("complexType") + mixed.render(xmlelement,"mixed", None) + expected_xml = """\n""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + +class DatetimeTest(unittest.TestCase): + def test_rendering(self): + dt = datetime(2001, 10, 26, 21, 32, 52) + mixed = xsd.Element(xsd.DateTime) + xmlelement = etree.Element("flight") + mixed.render(xmlelement,"takeoff_datetime", dt) + expected_xml = """ + 2001-10-26T21:32:52 + +""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + + + def test_wrong_type(self): + mixed = xsd.Element(xsd.DateTime,) + xmlelement = etree.Element("flight") + try: + mixed.render(xmlelement,"takeoff_datetime", 1) + except Exception: + pass + else: + self.assertTrue(False) + + + + +class ComplexTest(unittest.TestCase): + def test_rendering(self): + airport = Airport() + airport.type = "IATA" + airport.code = "WAW" + xmlelement = etree.Element("airport") + airport.render(xmlelement, airport) + xml = etree.tostring(xmlelement, pretty_print=True) + expected_xml = """ + IATA + WAW + +""" + self.assertEqual(expected_xml, xml) + + def test_attribute_rendering(self): + aircraft = Aircraft() + aircraft.tail_number = "LN-KKX" + xmlelement = etree.Element("aircraft") + aircraft.render(xmlelement, aircraft) + expected_xml = """\n""" + xml = etree.tostring(xmlelement, pretty_print=True) + self.assertEqual(expected_xml, xml) + + def test_attribute_parsing(self): + XML = """\n""" + aircraft = Aircraft.parsexml(XML) + self.assertEqual("LN-KKX", aircraft.tail_number) + + + def test_mulitylayer_complex(self): + flight = Flight() + flight.tail_number = "LN-KKA" + flight.takeoff_airport = Airport.create("IATA", "WAW") + flight.landing_airport = Airport.create("ICAO", "EGLL") + + try: + flight.takeoff_pilot = "ABC" + except ValueError: + pass + else: + self.assertTrue(False)#should't get here. + flight.takeoff_pilot = "CAPTAIN" + + xmlelement = etree.Element("flight") + flight.render(xmlelement, flight) + xml = etree.tostring(xmlelement, pretty_print=True) + expected_xml = """ + LN-KKA + + IATA + WAW + + + ICAO + EGLL + + CAPTAIN + +""" + self.assertEqual(expected_xml, xml) + + + def test_complex_with_list(self): + flight = Flight() + flight.tail_number = "LN-KKA" + flight.takeoff_airport = Airport.create("IATA", "WAW") + flight.landing_airport = Airport.create("ICAO", "EGLL") + flight.passangers.append("abc") + flight.passangers.append("123") + + xmlelement = etree.Element("flight") + flight.render(xmlelement, flight) + xml = etree.tostring(xmlelement, pretty_print=True) + expected_xml = """ + LN-KKA + + IATA + WAW + + + ICAO + EGLL + + abc + 123 + +""" + self.assertEqual(expected_xml, xml) + + + def test_inheritance_rendering(self): + class A(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + class B(A): + type = xsd.Attribute(xsd.String) + b = B() + b.name = "b" + b.type = "B" + xml = b.xml("inheritance") + EXPECTED_XML = """\n""" + self.assertEqual(EXPECTED_XML, xml) + + + def test_inheritance_parsin(self): + class A(xsd.ComplexType): + name = xsd.Attribute(xsd.String) + class B(A): + type = xsd.Element(xsd.String) + XML = """ + B +\n""" + b = B.parsexml(XML) + self.assertEqual(b.name, "b") + self.assertEqual(b.type, "B") + + + +class XmlParsingTest(unittest.TestCase): + SIMPLE_XML = """ + + EGLL + ICAO + + LN-KKA + 2001-10-26T21:32:52 + + WAW + IATA + + +""" + + def test_simple_parsing(self): + flight = Flight.parse_xmlelement(etree.fromstring(self.SIMPLE_XML)) + self.assertEqual("LN-KKA", flight.tail_number) + self.assertEqual("WAW", flight.takeoff_airport.code) + self.assertEqual("IATA", flight.takeoff_airport.type) + self.assertEqual("EGLL", flight.landing_airport.code) + self.assertEqual("ICAO", flight.landing_airport.type) + self.assertEqual(datetime(2001, 10, 26, 21, 32, 52), flight.takeoff_datetime) + + LIST_XML = """ + + EGLL + ICAO + + abc + 123 + LN-KKA + + WAW + IATA + + +""" + + def test_list_parsing(self): + flight = Flight.parse_xmlelement(etree.fromstring(self.LIST_XML)) + self.assertEqual("LN-KKA", flight.tail_number) + self.assertEqual("WAW", flight.takeoff_airport.code) + self.assertEqual("IATA", flight.takeoff_airport.type) + self.assertEqual("EGLL", flight.landing_airport.code) + self.assertEqual("ICAO", flight.landing_airport.type) + self.assertEqual(["abc", "123"], flight.passangers) + + + +class XSD_Spec_Test(unittest.TestCase): + AIRPORT_XML = """ + + + + + + + + + + + + + + """ + def test_complexType(self): + airport = xsdspec.ComplexType.parse_xmlelement(etree.fromstring(self.AIRPORT_XML)) + self.assertEqual("airport", airport.name) + code_type_element = airport.sequence.elements[0] + code_element = airport.sequence.elements[1] + self.assertEqual("code_type", code_type_element.name) + self.assertEqual("xs:string", code_type_element.simpleType.restriction.base) + self.assertEqual(3, len(code_type_element.simpleType.restriction.enumerations)) + self.assertEqual("ICAO", code_type_element.simpleType.restriction.enumerations[0].value) + self.assertEqual("code", code_element.name) + + +SCHEMA_XML = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + +class SchemaTest(unittest.TestCase): + def test_schema_parsing(self): + schema = xsdspec.Schema.parse_xmlelement(etree.fromstring(SCHEMA_XML)) + self.assertEqual(4, len(schema.complexTypes)) + self.assertEqual(1, len(schema.simpleTypes)) + self.assertEqual(2, len(schema.elements)) + + self.assertEqual("ops", schema.elements[0].name) + self.assertEqual("fds:ops", schema.elements[0].type) + + ops_type = schema.complexTypes[2] + self.assertEqual("ops", ops_type.name) + self.assertEqual("aircraft", ops_type.sequence.elements[0].name) + self.assertEqual("xs:string", ops_type.sequence.elements[0].type) + + + +class RequestResponseOperation(xsd.Group): + input = xsd.Element(xsd.String, minOccurs = 0) + output = xsd.Element(xsd.String, minOccurs = 0) + +class Operation(xsd.ComplexType): + name = xsd.Element(xsd.String) + requestResponseOperation = xsd.Ref(RequestResponseOperation) + +class GroupTest(unittest.TestCase): + XML = """ + TEST-Operation + IN + OUT +\n""" + + def test_rendering(self): + operation = Operation() + operation.name = "TEST-Operation" + operation.requestResponseOperation.input = "IN" + operation.requestResponseOperation.output = "OUT" + xml = operation.xml("operation") + self.assertEqual(self.XML, xml) + + def test_parsing(self): + operation = Operation.parsexml(self.XML) + self.assertEqual(operation.name, "TEST-Operation") + self.assertEqual(operation.requestResponseOperation.input, "IN") + self.assertEqual(operation.requestResponseOperation.output, "OUT") + + + def test_rendering_empty_group(self): + operation = Operation() + operation.name = "TEST-Operation" + xml = operation.xml("operation") + expected_xml = """ + TEST-Operation +\n""" + self.assertEqual(expected_xml, xml) + + +# +# +# +# +# +# +# +class TBodyAttributes(xsd.AttributeGroup): + encodingStyle = xsd.Attribute(xsd.String, use=xsd.Use.OPTIONAL) + use = xsd.Attribute(xsd.String) + namespace = xsd.Attribute(xsd.String) + +class TBody(xsd.ComplexType): + parts = xsd.Attribute(xsd.String) + tBodyAttributes = xsd.Ref(TBodyAttributes) + +class AttributeGroupTest(unittest.TestCase): + def test_rendering(self): + body = TBody() + body.parts = "Parts" + body.tBodyAttributes.use = "required" + body.tBodyAttributes.namespace = "xs" + expected_xml = """\n""" + xml = body.xml("body") + self.assertEqual(expected_xml, xml) + + def test_parsing(self): + xml = """\n""" + body = TBody.parsexml(xml) + self.assertEqual(body.parts,"Parts") + self.assertEqual(body.tBodyAttributes.use, "required") + self.assertEqual(body.tBodyAttributes.namespace, "xs") + self.assertEqual(body.tBodyAttributes.encodingStyle, None) + +class AirporttDocument(xsd.Document): + airport = xsd.Element(Airport) + +class DocumentTest(unittest.TestCase): + def test_document_rendering(self): + document = AirporttDocument() + document.airport = Airport(code="XXX", type="IATA") + xml = document.render() + expected_xml = """ + IATA + XXX +\n""" + self.assertEqual(xml, expected_xml) + + def test_document_parsing(self): + XML = """ + IATA + XXX + """ + document = AirporttDocument + +if __name__ == "__main__": + unittest.main() + + + + + + + + + + + \ No newline at end of file