Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: [ "3.11" ]
python-version: [ "3.9", "3.10", "3.11", "3.12" ]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: [ "3.11" ]
python-version: [ "3.9", "3.10", "3.11", "3.12" ]

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ odoolib/__pycache__
MANIFEST
build
dist
odoo_client_lib.egg-info
48 changes: 44 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,11 @@ is equivalent to the following interaction when you are coding an Odoo addon and
executed on the server: ::

user_osv = self.pool.get('res.users')
ids = user_osv.search(cr, uid, [("login", "=", "admin")])
user_info = user_osv.read(cr, uid, ids[0], ["name"])
ids = user_osv.search([("login", "=", "admin")])
user_info = user_osv.read(ids[0], ["name"])

Also note that coding using Model objects offer some syntaxic sugar compared to vanilla addon coding:

- You don't have to forward the "cr" and "uid" to all methods.
- The read() method automatically sort rows according the order of the ids you gave it to.
- The Model objects also provides the search_read() method that combines a search and a read, example: ::

Expand All @@ -62,9 +61,50 @@ Here are also some considerations about coding using the Odoo Client Library:
- The browse() method can not be used. That method returns a dynamic proxy that lazy loads the rows' data from
the database. That behavior is not implemented in the Odoo Client Library.

JSON-RPC
--------

The jsonrpc protocol is available since Odoo version 8.0. It has the exact same methods as the XML-RPC protocol,
but uses a different endpoint: `/jsonrpc/`. The Odoo Client Library provides a `get_connection()` to specify the protocol to use.

The only difference between XML-RPC and JSON-RPC is that the latter uses a JSON payload instead of an XML payload.

JSON2
-----

The Json2 appears with Odoo version 19.0, and requires every method to be called with named parameters.
If the method is called with positional arguments, the library will try to introspect the method
and convert the positional arguments into named parameters. This introspection is done only once per model.
Introspection means a server call, which can slow down the first method call for a given model. Using named parameters
is recommended for performance reasons.

With this new `/json/2/` endpoint, this library is less useful than before, but makes it easy to move older scripts
to the new endpoint.

Compatibility
-------------

- XML-RPC: OpenERP version 6.1 and superior

- JSON-RPC: Odoo version 8.0 (upcoming) and superior
- JSON-RPC: Odoo version 8.0 and superior

- JSON2: Odoo version 19.0 and superior

SSL Communication
-----------------

The Odoo Client Library supports both XML-RPC and JSON-RPC over SSL. The difference is that it uses
the HTTPS protocol, which means that the communication is encrypted. This is useful when you want to
communicate with an Odoo server over the internet, as it prevents eavesdropping and man-in-the-middle attacks.
To use XML-RPC over SSL, you can specify the protocol when creating the connection: ::

connection = odoolib.get_connection(hostname="localhost", protocol="xmlrpcs", ...)

the possible values for the protocol parameter are:
- `xmlrpc`: standard XML-RPC over HTTP (Odoo version 6.1 to 19.0)
- `xmlrpcs`: XML-RPC over HTTPS (Odoo version 6.1 to 19.0)
- `jsonrpc`: standard JSON-RPC over HTTP (Odoo version 8.0 to 19.0)
- `jsonrpcs`: JSON-RPC over HTTPS (Odoo version 8.0 to 19.0)
- `json2`: JSON2 over HTTP (Odoo version 19.0 and superior)
- `json2s`: JSON2 over HTTPS (Odoo version 19.0 and superior)

15 changes: 15 additions & 0 deletions licence.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Copyright (C) Stephane Wirtel
Copyright (C) 2011 Nicolas Vanhoren
Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>)
Copyright (C) 2018 Nicolas Seinlet
Copyright (C) 2018 Odoo s.a. (<http://odoo.com>)

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 changes: 0 additions & 31 deletions odoolib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) Stephane Wirtel
# Copyright (C) 2011 Nicolas Vanhoren
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>)
# Copyright (C) 2018 Odoo s.a. (<http://odoo.com>).
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
##############################################################################

from .main import *
31 changes: 0 additions & 31 deletions odoolib/dates.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,3 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) Stephane Wirtel
# Copyright (C) 2011 Nicolas Vanhoren
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>)
# Copyright (C) 2018 Odoo s.a. (<http://odoo.com>).
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
##############################################################################

import datetime

DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d"
Expand Down
128 changes: 128 additions & 0 deletions odoolib/json2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import logging
import httpx

from http import HTTPStatus

from .tools import AuthenticationError, RemoteModel, _getChildLogger

_logger = logging.getLogger(__name__)
DEFAULT_TIMEOUT = 60


class JsonModel(RemoteModel):
def __init__(self, connection, model_name):
res = super().__init__(connection, model_name)
self.__logger = _getChildLogger(_getChildLogger(_logger, 'object'), model_name or "")
self.methods = {}
self.model_methods = []
return res

def __getattr__(self, method):
"""
Provides proxy methods that will forward calls to the model on the remote Odoo server.

:param method: The method for the linked model (search, read, write, unlink, create, ...)
"""
def proxy(*args, **kwargs):
"""
:param args: A list of values for the method
"""
# self.__logger.debug(args)
data = kwargs
if args:
# Should convert args list into dict of args
self._introspect()
offset = 0
if method not in self.model_methods and 'ids' not in kwargs.keys():
data['ids'] = args[0]
offset = 1
for i in range(offset, len(args)):
if i-offset < len(self.methods[method]):
data[self.methods[method][i-offset]] = args[i]
else:
_logger.warning(f"Method {method} called with too many arguments: {args}")

result = httpx.post(
self._url(method),
headers=self.connection.bearer_header,
json=data,
timeout=DEFAULT_TIMEOUT,
)

if result.status_code == HTTPStatus.UNAUTHORIZED:
raise AuthenticationError("Authentication failed. Please check your API key.")
if result.status_code == 422:
raise ValueError(f"Invalid request: {result.text} for data {data}")
if result.status_code != 200:
raise ValueError(f"Unexpected status code {result.status_code}: {result.text}")
return result.json()
return proxy

def _introspect(self):
if not self.methods:
url = f"{self.connection.connector.url.removesuffix('/json/2/')}/doc-bearer/{self.model_name}.json"
response = httpx.get(url, headers=self.connection.bearer_header)
response.raise_for_status()
m = response.json().get('methods', {})
self.methods = {k: tuple(m[k]['parameters'].keys()) for k in m.keys()}
self.model_methods = [ k for k in m.keys() if 'model' in m[k].get('api', []) ]

def read(self, *args, **kwargs):
res = self.__getattr__('read')(*args, **kwargs)
if len(res) == 1:
return res[0]
return res

def _url(self, method):
"""
Returns the URL of the Odoo server.
"""
return f"{self.connection.connector.url}{self.model_name}/{method}"


class Json2Connector(object):
def __init__(self, hostname, port="8069"):
"""
Initialize by specifying the hostname and the port.
:param hostname: The hostname of the computer holding the instance of Odoo.
:param port: The port used by the Odoo instance for JsonRPC (default to 8069).
"""
if port != 80:
self.url = f'http://{hostname}:{port}/json/2/'
else:
self.url = f'http://{hostname}/json/2/'


class Json2SConnector(Json2Connector):
def __init__(self, hostname, port="443"):
super().__init__(hostname, port)
if port != 443:
self.url = f'https://{hostname}:{port}/json/2/'
else:
self.url = f'https://{hostname}/json/2/'


class Json2Connection(object):
"""
A class representing a connection to an Odoo server.
"""

def __init__(self, connector, database, api_key):
self.connector = connector
self.database = database
self.bearer_header = {"Authorization": f"Bearer {api_key}", 'Content-Type': 'application/json; charset=utf-8', "X-Odoo-Database": database}
self.user_context = None

def get_model(self, model_name):
return JsonModel(self, model_name)

def get_connector(self):
return self.connector

def get_user_context(self):
"""
Query the default context of the user.
"""
if not self.user_context:
self.user_context = self.get_model('res.users').context_get()
return self.user_context
Loading
Loading