Search code examples
pythonsoapzeepspyne

Spyne - Using nested classes for an Array of ComplexModel


With regards to Spyne Models and Native Python Types, let's assume I have two models, Company and Employee:

# server.py
from spyne import (
    Iterable, ComplexModel, Unicode, Integer,
)


class Employee(ComplexModel):
    name = Unicode
    salary = Integer

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary


class Company(ComplexModel):
    name = Unicode
    employees = Iterable(Employee)


    def __init__(self, name, employees):
        self.name = name
        self.employees = employees

Now I can create a web service that returns this data:

# server.py
from spyne import (
    Application, ServiceBase, rpc
)
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication
from wsgiref.simple_server import make_server
import logging


class Service(ServiceBase):
    @rpc(_returns=Company)
    def get_company(ctx):
        company = {
            "name": "My Company",
            "employees": [
                Employee("Me", 0),
                Employee("My friend", 10000),
            ]
        }

        return Company(**company)


application = Application(
    [Service],
    "targetnamespace",
    in_protocol=Soap11(validator="lxml"),
    out_protocol=Soap11(),
)

wsgi_application = WsgiApplication(application)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    logging.getLogger("spyne.protocol.xml").setLevel(logging.DEBUG)

    logging.info("Listening to http://127.0.0.1:8000")
    logging.info("WSDL is at: http://localhost:8000/?wsdl")
    server = make_server("127.0.0.1", 8000, wsgi_application)
    server.serve_forever()

I run this server and it waits for requests:

$ python server.py
INFO:root:Listening to http://127.0.0.1:8000
INFO:root:WSDL is at: http://localhost:8000/?wsdl

Now I can use zeep to send a request:

# client.py
from zeep.client import Client


client = Client("http://localhost:8000/?wsdl")
service = client.service
response = service.get_company()
print(response)

And the result is just as expected:

$ python client.py
{
    'name': 'My Company',
    'employees': {
        'Employee': [
            {
                'name': 'Me',
                'salary': 0
            },
            {
                'name': 'My friend',
                'salary': 10000
            }
        ]
    }
}

Now, since Employee belongs to Company, it makes sense for it to be inside the company's class, and it's cleaner and more maintainable:

class Company(ComplexModel):
    class Employee(ComplexModel):
        name = Unicode
        salary = Integer

        def __init__(self, name, salary):
            self.name = name
            self.salary = salary


    name = Unicode
    employees = Iterable(Employee)


    def __init__(self, name, employees):
        self.name = name
        self.employees = employees

But now when I create the service:

class Service(ServiceBase):
    @rpc(_returns=Company)
    def get_company(ctx):
        company = {
            "name": "My Company",
            "employees": [
                Company.Employee("Me", 0),
                Company.Employee("My friend", 10000),
            ]
        }

        return Company(**company)

The client (python client.py) gives this error:

TypeError: 'NoneType' object is not iterable

And the server (python server.py) gives this error:

TypeError: Argument must be bytes or unicode, got 'ModelBaseMeta'

How can I fix this?


Solution

  • You don't. This is not supposed to work.

    The following:

    class Company(ComplexModel):
        class Employee(ComplexModel):
            # ...
        # ...
    
    

    is the same as the following pseudocode:

    class EmployeeTopLevel(ComplexModel):
        # ...
    
    class Company(ComplexModel):
        Employee = EmployeeTopLevel
        # ...
    
    

    You now probably see why this is a bad idea. ModelBase metaclass does not support nesting of ModelBase children. You are trying to use classes for what packages and modules were designed to do. You should define your classes at the module level.

    Here's a workaround though:

    class Company(ComplexModel):
        class Employee(ComplexModel):
            name = Unicode
            salary = Integer
    
            def __init__(self, name, salary):
                self.name = name
                self.salary = salary
    
        _type_info = [
            ('name', Unicode),
            ('employees', Iterable(Employee)),
        ]
    
        def __init__(self, name, employees):
            self.name = name
            self.employees = employees
    
    

    This works because it skips the field detection logic of the model metaclass and uses directly the user-supplied _type_info list.