Search code examples
soappython-2.7lxmldjango-1.5spyne

Overriding namespace in array content


I have following:

from spyne.service import ServiceBase
from spyne.util import xml
from spyne.model import complex, primitive


class ComplexModel(complex.ComplexModelBase):
    __namespace__ = 'http://xml.candyshop.com/ns/candies/'
    __metaclass__ = complex.ComplexModelMeta


class CandyModel(ComplexModel):
    __type_name__ = 'candy'
    flavor = complex.XmlAttribute(primitive.Unicode)


class BagModel(ComplexModel):
    __type_name__ = 'bag'
    candies = complex.Array(CandyModel)


class CandyShop(ServiceBase):
    __tns__ = 'http://xml.candyshop.com/ns/shop/'

    @rpc(_returns=primitive.AnyXml)
    def get_my_bag(ctx):
        bag = BagModel()
        bag.candies = [CandyModel(flavor='choco')]
        return xml.get_object_as_xml(
            bag,
            cls=BagModel,
            root_tag_name='bag',
        )

    @classmethod
    def dispatch(cls):
        from django.views.decorators.csrf import csrf_exempt
        from spyne.application import Application
        from spyne.server.django import DjangoApplication

        application = Application([cls],
            tns=cls.__tns__,
            in_protocol=Soap11(validator='lxml'),
            out_protocol=Soap11(cleanup_namespaces=True)
        )
        return csrf_exempt(DjangoApplication(application))


shop_service = CandyShop.dispatch()

And result for get_my_bag is like:

<tns:get_my_bagResult xmlns:tns="http://xml.candyshop.com/ns/shop/">
    <ns0:bag xmlns:ns0="http://xml.candyshop.com/ns/candies/">
        <ns0:candies>
            <ns1:candy xmlns:ns1="None" flavor="choco"/>
        </ns0:candies>
    </ns0:bag>
</tns:get_my_bagResult>

But I want following:

<tns:get_my_bagResult xmlns:tns="http://xml.candyshop.com/ns/shop/">
    <ns0:bag xmlns:ns0="http://xml.candyshop.com/ns/candies/">
        <ns0:candies>
            <ns0:specialCandy flavor="choco"/>
        </ns0:candies>
    </ns0:bag>
</tns:get_my_bagResult>

So, how to customize type name for array content without definning new subclass? I tried complex.Array(CandyModel.customize(type_name='specialCandy'))

but this not works. Using of static alias method gives an empty <ns0:candies/>, maybe for that I'm still put CandyModel instances to the candies list, but this is my goal.

Second, why there is xmlns:ns1="None" and how to fix it for ns0?

BTW. Is there a way to customize namespace prefixes?


EDIT

class Candies(complex.Array):
    __namespace__ = 'http://xml.candyshop.com/ns/candies/'

and

candies = Candies(CandyModel)

solves problem with namespaces, but its a workaround rather than solution. I prefer inline customization or some of mixin with my namespaced ComplexModel.


Solution

  • Why are you returning AnyXml instead of returning a proper object?

    Here's your code refactored to return the XML document that you want:

    import logging
    logging.basicConfig(level=logging.DEBUG)
    logging.getLogger('spyne.protocol.xml').setLevel(logging.DEBUG)
    
    from spyne.decorator import rpc
    from spyne.service import ServiceBase
    from spyne.util import xml
    from spyne.model import complex, primitive
    from spyne.application import Application
    from spyne.protocol.soap import Soap11
    from spyne.server.wsgi import WsgiApplication
    from lxml import etree
    
    
    class ComplexModel(complex.ComplexModelBase):
        __namespace__ = 'http://xml.candyshop.com/ns/candies/'
        __metaclass__ = complex.ComplexModelMeta
    
    
    class CandyModel(ComplexModel):
        __type_name__ = 'candy'
        flavor = complex.XmlAttribute(primitive.Unicode)
    
    
    class BagModel(ComplexModel):
        __type_name__ = 'bag'
        candies = complex.Array(CandyModel)
    
    class Bag(ComplexModel):
        bag = BagModel
    
    class CandyShop(ServiceBase):
        __tns__ = 'http://xml.candyshop.com/ns/shop/'
    
        @rpc(_returns=Bag)
        def get_my_bag(ctx):
            bag = BagModel()
            bag.candies = [CandyModel(flavor='choco')]
            return Bag(bag=bag)
    
        @classmethod
        def dispatch(cls):
            application = Application([cls],
                tns=cls.__tns__,
                in_protocol=Soap11(validator='lxml'),
                out_protocol=Soap11(cleanup_namespaces=True)
            )
            return application
    
    # In case you need to extract parts of your response, you can use hooks:
    
    def _on_method_return_document(ctx):
        ns = ctx.app.interface.nsmap
        elt = ctx.out_document.xpath('//s0:bag',namespaces=ns)[0]
        output = etree.tostring(elt, pretty_print=True)
    
        print output # do what you want with it.
    
    CandyShop.event_manager.add_listener('method_return_document',
                                                    _on_method_return_document)
    
    if __name__ == '__main__':
        from wsgiref.simple_server import make_server
    
        service_app = CandyShop.dispatch()
    
        application = WsgiApplication(service_app)
    
        server = make_server('0.0.0.0', 8080, application)
        server.serve_forever()
    

    Here's the output:

    <?xml version='1.0' encoding='ASCII'?>
    <senv:Envelope xmlns:tns="http://xml.candyshop.com/ns/shop/" 
                   xmlns:s0="http://xml.candyshop.com/ns/candies/" 
                   xmlns:senv="http://schemas.xmlsoap.org/soap/envelope/">
      <senv:Body>
        <tns:get_my_bagResponse>
          <tns:get_my_bagResult>
            <s0:bag>
              <s0:candies>
                <s0:candy flavor="choco"/>
              </s0:candies>
            </s0:bag>
          </tns:get_my_bagResult>
        </tns:get_my_bagResponse>
      </senv:Body>
    </senv:Envelope>
    

    EDIT:

    In case you need to extract parts of your response, you can use hooks. I've updated the code. There are also many event examples here: https://github.com/arskom/spyne/blob/master/examples/events.py