Host a WSGI application with Odoo

Posted on October 06, 2015 in blog

Intro

Odoo is a business application which can be separated, from a technical point of view, in three parts:

  • the business application itself (the framework + modules, Root, dispatching requests of the Web client to the Web controllers) ;
  • an XML-RPC web service layer (wsgi_xmlrpc, managing all /xmlrpc/ requests);
  • and the Web application server ;

The first two are WSGI applications, and the last one is the WSGI server (used to serve the WSGI applications). The application server can be replaced by any other server compatible with WSGI, like Gunicorn, uWSGI, mod_wsgi (for Apache)...

In this article we will see how to integrate another WSGI application (a SOAP web service here) inside the Web application server of Odoo 8.0, and take advantage of the Odoo framework from this one. The integration will also be done as a normal module.

The WSGI application

Quoting Wikipedia, "the Web Server Gateway Interface (WSGI) is a specification for simple and universal interface between web servers and web applications or frameworks for the Python programming language.".

Basically, it is an application processing requests provided by the WSGI server. In our case, the WSGI application is a SOAP web service, and we will use the Spyne library to create it.

For the exercise, the purpose of our SOAP web service will be to return the available stock quantity of a given product.

First, let's create the SOAP web service without any Odoo related code in a file my_service.py:

from spyne import Application, ServiceBase, rpc
from spyne import String, Integer
from spyne import Mandatory as M
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication


class MyService(ServiceBase):

    @rpc(M(String), _returns=M(Integer))
    def get_stock_quantity(self, product_code):
        # TODO
        print "PRODUCT:", product_code
        return 0

# Spyne application
application = Application(
    [MyService],
    'http://example.com/soap/',
    in_protocol=Soap11(validator='lxml'),
    out_protocol=Soap11())

# WSGI application
wsgi_application = WsgiApplication(application)

As a test you can run it with the Python's built-in wsgi server, like this:

from wsgiref.simple_server import make_server
from my_service import wsgi_application

server = make_server('0.0.0.0', 8000, wsgi_application)
server.serve_forever()

You can check the WSDL file at http://localhost:8000/soap/?wsdl

Integration with the Odoo Web server

As said before, the Odoo web server is used to serve the Odoo WSGI applications. First, put your my_service.py file into a new module in your addons_path, and integrate the wsgi_application to the Web server like that:

from openerp.service.wsgi_server import module_handlers

from .my_service import wsgi_application

module_handlers.insert(0, wsgi_application)

Here we insert our WSGI app in first position to give it the priority to process HTTP requests. We do not use the openerp.service.wsgi_server.register_wsgi_handler() helper function here because the Root application of Odoo (see: openerp/http.py) try to handle all requests, without giving any chance to the others WSGI applications to process them.

So by inserting it in the first position our WSGI app will handle all requests... and that's not exactly what we want neither: our application should just processing HTTP requests related to the /soap/ URL, but as we have the control of our own code, we will fix this situation by overloading the WsgiApplication class provided by the Spyne library.

# ...

class SOAPWsgiApplication(WsgiApplication):

    def __call__(self, req_env, start_response, wsgi_url=None):
        """Only match URL requests starting with '/soap/'."""
        if req_env['PATH_INFO'].startswith('/soap/'):
            return super(SOAPWsgiApplication, self).__call__(
                req_env, start_response, wsgi_url)
        # return None: let's the other WSGI applications (like the Root one)
        # try to handle the request

# Spyne application
application = Application(
    [MyService],
    'http://example.com/soap/',
    in_protocol=Soap11(validator='lxml'),
    out_protocol=Soap11())

# WSGI application
wsgi_application = SOAPWsgiApplication(application)

Test the SOAP service

After installing your module, write a simple script using the suds library to request the new SOAP Web service:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from suds.client import Client

HOST = 'localhost'
PORT = 8069

client = Client('http://%s:%s/soap/?wsdl' % (HOST, PORT))

response = client.service.get_stock_quantity(product_code='P01')
print response
$ chmod +x test_soap.py
$ ./test_soap.py
0

It works! We get 0, which is the current hard-coded result returned by the get_stock_quantity() method.

Access to the Odoo ORM from your WSGI app

For the simplicity of the exercice here, we are hardcoding the database to use to perform queries (we take the first one) with the ORM, feel free to improve this part (fetch the name of the database from the config file, manage authentication...). In your my_service.py module, insert this piece of code at the top:

# [spyne imports...]

from openerp.service import db
from openerp.modules.registry import RegistryManager
from openerp import SUPERUSER_ID


def get_registry_cr_uid_context():
    if db.exp_list():
        db_name = db.exp_list()[0]
        registry = RegistryManager.get(db_name)
        cr = registry.cursor()
        context = registry['res.users'].context_get(cr, SUPERUSER_ID)
        return registry, cr, SUPERUSER_ID, context
    raise Warning(u"NO DATABASE FOUND")

# ...

We are now able to identify the product in our get_stock_quantity() method and to return its available quantity:

# ...

class MyService(ServiceBase):

    @rpc(M(String), _returns=M(Integer))
    def get_stock_quantity(self, product_code):
        registry, cr, uid, context = get_registry_cr_uid_context()
        product_model = registry['product.product']
        product_id = product_model.search(
            cr, uid, [('default_code', '=', product_code)], context=context)
        if product_id:
            product = product_model.browse(
                cr, uid, product_id, context=context)
            return product.qty_available
        raise ValueError(u"NO PRODUCT FOUND")

# ...

Run again the script to request the SOAP service:

$ ./test_soap.py
12

You're done!

Conclusion

We saw that Odoo was able to host any WSGI application, and you can use this feature to separate concepts by building dedicated and horizontal services alongside your Odoo application. This article shown the integration of a SOAP Web service built with Spyne, but you could host a Django, Flask, Pylons or a Pyramid application as well.