Tutorial ======== This tutorial assumes some understanding of XML, XSD, WSDL and SOAP. Introduction ------------ The main purpose of this library is a neat implementation of the SOAP protocol, but the `soapfish.xsd` module can used for any XML as it gives a means of mapping XML to an object. The object description generally is similar to fields in the Django ORM - the static fields that define instance fields. The main difference would be that type is passed as first parameter, rather than being a field e.g. .. code-block:: python # Django: tail_number = models.CharField() # Soapfish: tail_number = xsd.Element(xsd.String) `xsd.Element` reflects the nature of the field, elements are fields that will be wrapped with tags. Other options are `xsd.Attribute`, `xsd.Ref` and `xsd.ListElement`. For more detail see the documentation string for `xsd.Element`. As SOAP, WSDL and XSD files are also XML documents the `soapfish.xsd` module was also used to describe them. The descriptions are located in `soapfish.xsdspec`, `soapfish.soap` and `soapfish.wsdl`. The `soapfish.soap` module 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. Relevant modules include `soapfish.py2xsd`, `soapfish.xsd2py`, `soapfish.wsdl2py` and `soapfish.py2wsdl`. `soapfish.utils` mostly contains helper functions for Jinja2. Jinja2 is templating engine used for code generation. 1. Working with XML ------------------- The main building blocks are `xsd.ComplexType`, `xsd.Element`, `xsd.Attribute` and the simple types defined in the `soapfish.xsd` module. `xsd.ComplexType` is a class that can be extended to define custom types. The main methods defined for types are `xml()` - translates object into XML - and `parsexml()` - builds object from XML. **Example 1: Rendering an object to XML** .. code-block:: python from soapfish 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')) .. code-block:: xml IATA WAW Note that `xml()` method takes one parameter - the name of the root tag. **Example 2: Parsing XML to an object** .. code-block:: python from soapfish import xsd class Airport(xsd.ComplexType): type = xsd.Element(xsd.String) code = xsd.Element(xsd.String) xml = 'IATAWAW' airport = Airport.parsexml(xml) print(f'Type: {airport.type}') print(f'Code: {airport.code}') .. code-block:: text Type: IATA Code: WAW **Example 3: Nested complex types with attributes** .. code-block:: python from datetime import datetime from soapfish 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) obj = Flight(tail_number='G-ABCD') obj.takeoff_airport = Airport(type='IATA', code='WAW') obj.landing_airport = Airport(type='ICAO', code='EGLL') obj.takeoff_datetime = datetime.now() print(obj.xml('flight')) .. code-block:: xml IATA WAW 2011-05-06T11:11:23 ICAO EGLL 2. Schema --------- `xsd.Schema` is an object that aggregates all information stored in XSD file. There two main use cases for this object. It can be used to generate an XSD file or it can be generated from such file. For detail field description see the documentation string for `xsd.Schema`. A schema instance is required for validation and because SOAP webservice performs validation is required for service configuration too. 2.1. Generating code from XSD file '''''''''''''''''''''''''''''''''' `soapfish.py2xsd` generates a Python representation of an XML from an XSD file. **Example:** `python -m soapfish.xsd2py examples/ops.xsd` .. code-block:: python from soapfish 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)}, ) Redirect the output to a python file: `python -m soapfish.xsd2py examples/ops.xsd > /tmp/ops.py`. Now calling `python -m soapfish.py2xsd /tmp/ops.py` will generate the equivalent XSD from the Python code. The `soapfish.xsd2py` script expects a schema instance to be defined in global scope called "Schema", in a way similar to one in generated code. 3. Web Service -------------- When a WSDL file is provided server or client code can be generated using the `soapfish.wsdl2py` script. If not, it is advised to write code first a then use a browser to request the specification. Accessing your service with the query string `?wsdl` appended will give the current WSDL with XSD embedded. 3.1. Generating code from WSDL file ''''''''''''''''''''''''''''''''''' `soapfish.wsdl2py` can generate either client or server code: `python -m soapfish.wsdl2py -c examples/ops.wsdl` `python -m soapfish.wsdl2py -s examples/ops.wsdl` 3.1.1. Server ^^^^^^^^^^^^^ **Example:** `python -m soapfish.wsdl2py -s examples/ops.wsdl` .. code-block:: python # XML Schema Removed... PutOps_method = xsd.Method( function=PutOps, soapAction='http://www.example.com/ws/ops/PutOps', input='ops', # Pointer to Schema.elements output='status', # Pointer to Schema.elements operationName='PutOps', ) SERVICE = soap.Service( targetNamespace='http://www.example.com/ops.wsdl', location='http://www.example.com/ws/ops', schema=Schema, methods=[PutOps_method], ) Generated code includes methods descriptions, service description, dispatcher and Django `urls.py` binding. `xsd.Method` describes one method for service (that can consist from more than 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 the documentation string for `xsd.Method`. `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 a `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. 3.1.2. Client ^^^^^^^^^^^^^ **Example:** `python -m soapfish.wsdl2py -c examples/ops.wsdl` .. code-block:: python # XML Schema Removed... PutOps_method = xsd.Method( soapAction='http://www.example.com/ws/ops/PutOps', input='ops', # Pointer to Schema.elements output='status', # Pointer to Schema.elements operationName='PutOps', ) SERVICE = soap.Service( targetNamespace='http://www.example.com/ops.wsdl', location='http://www.example.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 the remote webservice. Calling one of those methods - 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 encountering any problems. For more examples see `examples/client.py` 3.2. Building Webservice '''''''''''''''''''''''' The build a webservice we need to define few things: * Classes that would be send via SOAP * Schema instance that aggregates all classes with name space etc. * Web service functions and all related informations * Service instance to put everything together * Binding to a URL Lets build the stock web service that will give a stock price for provided company code and datetime. 3.2.1. Stack classes ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python class GetStockPrice(xsd.ComplexType): company = xsd.Element(xsd.String, minOccurs=1) datetime = xsd.Element(xsd.DateTime) class StockPrice(xsd.ComplexType): price = xsd.Element(xsd.Integer) Schema = xsd.Schema( targetNamespace='http://code.google.com/p/soapfish/stock.xsd', # should be unique, can be any string. complexTypes=[GetStockPrice, StockPrice], elements={ 'getStockPrice': xsd.Element(GetStockPrice), 'stockPrice': xsd.Element(StockPrice), }, ) Note the elements in schema - for this version it is required to create an element of a specific type and use its string element name as input/output in Service definitions. WSDL specifications allows also direct use of the type, which is not covered yet. 3.2.2. Method definition ^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python def get_stock_price(request, gsp): print(gsp.company) return StockPrice(price=139) get_stock_price_method = xsd.Method( function=get_stock_price, soapAction='http://code.google.com/p/soapfish/stock/get_stock_price', input='getStockPrice', output='stockPrice', operationName='GetStockPrice', ) 3.2.3. Putting it all together ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python SERVICE = soap.Service( targetNamespace='http://code.google.com/p/soapfish/stock.wsdl', location='http://127.0.0.1:8000/stock', # where request should be sent. schema=Schema, methods=[get_stock_price_method], ) .. code-block:: python from wsgiref.simple_server import make_server from soapfish import soap_dispatch from service_gen import SERVICE dispatcher = soap_dispatch.SOAPDispatcher(SERVICE) app = soap_dispatch.WsgiSoapApplication({'/ChargePoint/services/chargePointService': dispatcher}) print('Serving HTTP on port 8000...') httpd = make_server('', 8000, app) httpd.serve_forever() Now requesting `http://127.0.0.1:8000/stock?wsdl` will give service specification and SOAP messages like: .. code-block:: xml Google 2010-08-20T21:39:59 can be sent to http://127.0.0.1:8000/stock. *The full working example can be found in examples/stock.*