Skip to content

Commit 7295069

Browse files
committed
[IMP] support the new /json/2 odoo 19.0 route
Odoo 19.0 comes with a new `json/2/` endpoint. this commit makes the library compatible with this new endpoint.
1 parent f03ae5d commit 7295069

File tree

7 files changed

+621
-318
lines changed

7 files changed

+621
-318
lines changed

README.rst

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,11 @@ is equivalent to the following interaction when you are coding an Odoo addon and
4242
executed on the server: ::
4343

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

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

50-
- You don't have to forward the "cr" and "uid" to all methods.
5150
- The read() method automatically sort rows according the order of the ids you gave it to.
5251
- The Model objects also provides the search_read() method that combines a search and a read, example: ::
5352
@@ -62,9 +61,52 @@ Here are also some considerations about coding using the Odoo Client Library:
6261
- The browse() method can not be used. That method returns a dynamic proxy that lazy loads the rows' data from
6362
the database. That behavior is not implemented in the Odoo Client Library.
6463

64+
JSON-RPC
65+
--------
66+
67+
The jsonrpc protocol is available since Odoo version 8.0. It has the exact same methods as the XML-RPC protocol,
68+
but uses a different endpoint: `/jsonrpc/`. The Odoo Client Library provides a `get_connection()` to specify the protocol to use.
69+
70+
The only difference between XML-RPC and JSON-RPC is that the latter uses a JSON payload instead of an XML payload.
71+
72+
JSON2
73+
-----
74+
75+
The Json2 appears with Odoo version 19.0, and requires every method to be called with named parameters.
76+
If the method is called with positional arguments, the library will try to introspect the method
77+
and convert the positional arguments into named parameters. This introspection is done only once per model.
78+
Introspection means a server call, which can slow down the first method call for a given model. Using named parameters
79+
is recommended for performance reasons.
80+
81+
With this new `/json/2/` endpoint, this library is less useful than before, but makes it easy to move older scripts
82+
to the new endpoint.
83+
6584
Compatibility
6685
-------------
6786

6887
- XML-RPC: OpenERP version 6.1 and superior
6988

70-
- JSON-RPC: Odoo version 8.0 (upcoming) and superior
89+
- JSON-RPC: Odoo version 8.0 and superior
90+
91+
- JSON2: Odoo version 19.0 and superior
92+
93+
SSL Communication
94+
-----------------
95+
96+
The Odoo Client Library supports both XML-RPC and JSON-RPC over SSL. The difference is that it uses
97+
the HTTPS protocol, which means that the communication is encrypted. This is useful when you want to
98+
communicate with an Odoo server over the internet, as it prevents eavesdropping and man-in-the-middle attacks.
99+
To use XML-RPC over SSL, you can specify the protocol when creating the connection: ::
100+
101+
```
102+
connection = odoolib.get_connection(hostname="localhost", protocol="xmlrpcs", ...)
103+
```
104+
105+
the possible values for the protocol parameter are:
106+
- `xmlrpc`: standard XML-RPC over HTTP (Odoo version 6.1 to 19.0)
107+
- `xmlrpcs`: XML-RPC over HTTPS (Odoo version 6.1 to 19.0)
108+
- `jsonrpc`: standard JSON-RPC over HTTP (Odoo version 8.0 to 19.0)
109+
- `jsonrpcs`: JSON-RPC over HTTPS (Odoo version 8.0 to 19.0)
110+
- `json2`: JSON2 over HTTP (Odoo version 19.0 and superior)
111+
- `json2s`: JSON2 over HTTPS (Odoo version 19.0 and superior)
112+

odoolib/json2.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# -*- coding: utf-8 -*-
2+
##############################################################################
3+
#
4+
# Copyright (C) Stephane Wirtel
5+
# Copyright (C) 2011 Nicolas Vanhoren
6+
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>)
7+
# Copyright (C) 2018 Odoo s.a. (<http://odoo.com>).
8+
# All rights reserved.
9+
#
10+
# Redistribution and use in source and binary forms, with or without
11+
# modification, are permitted provided that the following conditions are met:
12+
#
13+
# 1. Redistributions of source code must retain the above copyright notice, this
14+
# list of conditions and the following disclaimer.
15+
# 2. Redistributions in binary form must reproduce the above copyright notice,
16+
# this list of conditions and the following disclaimer in the documentation
17+
# and/or other materials provided with the distribution.
18+
#
19+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
#
30+
##############################################################################
31+
32+
import logging
33+
import requests
34+
35+
from http import HTTPStatus
36+
37+
from .tools import AuthenticationError, RemoteModel, _getChildLogger
38+
39+
_logger = logging.getLogger(__name__)
40+
DEFAULT_TIMEOUT = 60
41+
42+
43+
class JsonModel(RemoteModel):
44+
def __init__(self, connection, model_name):
45+
res = super().__init__(connection, model_name)
46+
self.__logger = _getChildLogger(_getChildLogger(_logger, 'object'), model_name or "")
47+
self.methods = {}
48+
self.model_methods = []
49+
return res
50+
51+
def __getattr__(self, method):
52+
"""
53+
Provides proxy methods that will forward calls to the model on the remote Odoo server.
54+
55+
:param method: The method for the linked model (search, read, write, unlink, create, ...)
56+
"""
57+
def proxy(*args, **kwargs):
58+
"""
59+
:param args: A list of values for the method
60+
"""
61+
# self.__logger.debug(args)
62+
data = kwargs
63+
if args:
64+
# Should convert args list into dict of args
65+
self._introspect()
66+
offset = 0
67+
if method not in self.model_methods and 'ids' not in kwargs.keys():
68+
data['ids'] = args[0]
69+
offset = 1
70+
for i in range(offset, len(args)):
71+
if i-offset < len(self.methods[method]):
72+
data[self.methods[method][i-offset]] = args[i]
73+
else:
74+
_logger.warning(f"Method {method} called with too many arguments: {args}")
75+
76+
result = requests.post(
77+
self._url(method),
78+
headers=self.connection.bearer_header,
79+
json=data,
80+
timeout=DEFAULT_TIMEOUT,
81+
)
82+
83+
if result.status_code == HTTPStatus.UNAUTHORIZED:
84+
raise AuthenticationError("Authentication failed. Please check your API key.")
85+
if result.status_code == 422:
86+
raise ValueError(f"Invalid request: {result.text} for data {data}")
87+
if result.status_code != 200:
88+
raise ValueError(f"Unexpected status code {result.status_code}: {result.text}")
89+
return result.json()
90+
return proxy
91+
92+
def _introspect(self):
93+
if not self.methods:
94+
url = f"{self.connection.connector.url.strip('/json/2/')}/doc/{self.model_name }.json"
95+
response = requests.get(url, headers=self.connection.bearer_header)
96+
response.raise_for_status()
97+
m = response.json().get('methods', {})
98+
self.methods = {k: tuple(m[k]['parameters'].keys()) for k in m.keys()}
99+
self.model_methods = [ k for k in m.keys() if 'model' in m[k].get('api', []) ]
100+
101+
def read(self, *args, **kwargs):
102+
res = self.__getattr__('read')(*args, **kwargs)
103+
if len(res) == 1:
104+
return res[0]
105+
return res
106+
107+
def _url(self, method):
108+
"""
109+
Returns the URL of the Odoo server.
110+
"""
111+
return f"{self.connection.connector.url}{self.model_name}/{method}"
112+
113+
114+
class Json2Connector(object):
115+
def __init__(self, hostname, port="8069"):
116+
"""
117+
Initialize by specifying the hostname and the port.
118+
:param hostname: The hostname of the computer holding the instance of Odoo.
119+
:param port: The port used by the Odoo instance for JsonRPC (default to 8069).
120+
"""
121+
if port != 80:
122+
self.url = f'http://{hostname}:{port}/json/2/'
123+
else:
124+
self.url = f'http://{hostname}/json/2/'
125+
126+
127+
class Json2SConnector(Json2Connector):
128+
def __init__(self, hostname, port="443"):
129+
super().__init__(hostname, port)
130+
if port != 443:
131+
self.url = f'https://{hostname}:{port}/json/2/'
132+
else:
133+
self.url = f'https://{hostname}/json/2/'
134+
135+
136+
class Json2Connection(object):
137+
"""
138+
A class representing a connection to an Odoo server.
139+
"""
140+
141+
def __init__(self, connector, database, api_key):
142+
self.connector = connector
143+
self.database = database
144+
self.bearer_header = {"Authorization": f"Bearer {api_key}", 'Content-Type': 'application/json; charset=utf-8', "X-Odoo-Database": database}
145+
self.user_context = None
146+
147+
def get_model(self, model_name):
148+
return JsonModel(self, model_name)
149+
150+
def get_connector(self):
151+
return self.connector
152+
153+
def get_user_context(self):
154+
"""
155+
Query the default context of the user.
156+
"""
157+
if not self.user_context:
158+
self.user_context = self.get_model('res.users').context_get()
159+
return self.user_context

0 commit comments

Comments
 (0)