From 14c7c9ae50300ac8b34605c240924364abd4bf75 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Thu, 15 Feb 2024 17:23:39 +0100 Subject: [PATCH 01/10] fix(exportable): v17 fields are sometimes non exportable --- odoorpc/env.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/odoorpc/env.py b/odoorpc/env.py index 536dbd4..fa70fd1 100644 --- a/odoorpc/env.py +++ b/odoorpc/env.py @@ -322,6 +322,8 @@ def _create_model_class(self, model): fields_get = self._odoo.execute(model, 'fields_get') for field_name, field_data in fields_get.items(): if field_name not in FIELDS_RESERVED: + if 'exportable' in field_data and not field_data['exportable']: + continue Field = fields.generate_field(field_name, field_data) attrs['_columns'][field_name] = Field attrs[field_name] = Field From 801504605d63e2cff175400114954ba460c6b67b Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Fri, 10 Jun 2022 16:01:28 +0200 Subject: [PATCH 02/10] feat(auto-retry): When 429 error, auto retry the requests. --- odoorpc/odoo.py | 20 +++++++++++++++++- odoorpc/rpc/__init__.py | 42 ++++++++++++++++++++++++++++++++++--- odoorpc/rpc/jsonrpclib.py | 44 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/odoorpc/odoo.py b/odoorpc/odoo.py index c86df55..021ed13 100644 --- a/odoorpc/odoo.py +++ b/odoorpc/odoo.py @@ -44,6 +44,18 @@ class ODOO(object): >>> opener = urllib.request.build_opener(auth_handler) >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener) + You can also configure an autoretry (enable by default for *.odoo.com host) + using autoretry boolean option. + Number of iterations and backoff factor can also be specified + + .. doctest:: + :options: +SKIP + + >>> import odoorpc + >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener, autoretry=True) + >>> # Or + >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener, autoretry=True, autoretry_factor=0.2, autoretry_max=3) + *Python 2:* :raise: :class:`odoorpc.error.InternalError` @@ -65,6 +77,7 @@ def __init__( timeout=120, version=None, opener=None, + **kwargs, ): if protocol not in ['jsonrpc', 'jsonrpc+ssl']: txt = ( @@ -93,7 +106,12 @@ def __init__( # Instanciate the server connector try: self._connector = rpc.PROTOCOLS[protocol]( - self._host, self._port, timeout, version, opener=opener + self._host, + self._port, + timeout, + version, + opener=opener, + **kwargs, ) except rpc.error.ConnectorError as exc: raise error.InternalError(exc.message) diff --git a/odoorpc/rpc/__init__.py b/odoorpc/rpc/__init__.py index ecceace..1407d0a 100644 --- a/odoorpc/rpc/__init__.py +++ b/odoorpc/rpc/__init__.py @@ -6,6 +6,9 @@ Web controllers of `Odoo` expose two kinds of methods: `json` and `http`. These methods can be accessed from the connectors of this module. + +An autoretry for 429 error is also provided, and turn on by default +for *.odoo.com hosts. """ import sys @@ -24,9 +27,25 @@ class Connector(object): """Connector base class defining the interface used to interact with a server. + + You can also configure an autoretry (enable by default for *.odoo.com host) + using the ``autoretry`` boolean option. If a 429 HTTP error is raised, + the script will automatically retry after ``backoff_factor * math.pow(2, iteration - 1)`` + + Number of maximum iterations and backoff factor can also be specified using kwargs + ``autoretry_factor`` default to 0.2 + ``autoretry_max`` default to 10 + + .. doctest:: + :options: +SKIP + + >>> import odoorpc + >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener, autoretry=True) + >>> # Or + >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener, autoretry=True, autoretry_factor=0.2, autoretry_max=3) """ - def __init__(self, host, port=8069, timeout=120, version=None): + def __init__(self, host, port=8069, timeout=120, version=None, **kwargs): self.host = host try: int(port) @@ -38,6 +57,15 @@ def __init__(self, host, port=8069, timeout=120, version=None): self.port = int(port) self._timeout = timeout self.version = version + # Default autoretry for .odoo.com (saas) hosts + if 'autoretry' in kwargs: + self._autoretry = kwargs['autoretry'] + elif host.endswith('.odoo.com'): + self._autoretry = True + else: + self._autoretry = False + self._autoretry_factor = kwargs.get('autoretry_factor', 0.2) + self._autoretry_max = kwargs.get('autoretry_max', 10) @property def ssl(self): @@ -199,8 +227,11 @@ def __init__( version=None, deserialize=True, opener=None, + **kwargs, ): - super(ConnectorJSONRPC, self).__init__(host, port, timeout, version) + super(ConnectorJSONRPC, self).__init__( + host, port, timeout, version, **kwargs + ) self.deserialize = deserialize # One URL opener (with cookies handling) shared between # JSON and HTTP requests @@ -208,6 +239,7 @@ def __init__( cookie_jar = CookieJar() opener = build_opener(HTTPCookieProcessor(cookie_jar)) self._opener = opener + self._proxy_json, self._proxy_http = self._get_proxies() def _get_proxies(self): @@ -222,6 +254,9 @@ def _get_proxies(self): ssl=self.ssl, deserialize=self.deserialize, opener=self._opener, + autoretry=self._autoretry, + autoretry_factor=self._autoretry_factor, + autoretry_max=self._autoretry_max, ) proxy_http = jsonrpclib.ProxyHTTP( self.host, @@ -284,9 +319,10 @@ def __init__( version=None, deserialize=True, opener=None, + **kwargs, ): super(ConnectorJSONRPCSSL, self).__init__( - host, port, timeout, version, opener=opener + host, port, timeout, version, opener=opener, **kwargs ) self._proxy_json, self._proxy_http = self._get_proxies() diff --git a/odoorpc/rpc/jsonrpclib.py b/odoorpc/rpc/jsonrpclib.py index c0350a7..6804f33 100644 --- a/odoorpc/rpc/jsonrpclib.py +++ b/odoorpc/rpc/jsonrpclib.py @@ -5,8 +5,10 @@ import copy import json import logging +import math import random import sys +from time import sleep # Python 2 if sys.version_info[0] < 3: @@ -88,10 +90,22 @@ class ProxyJSON(Proxy): """ def __init__( - self, host, port, timeout=120, ssl=False, opener=None, deserialize=True + self, + host, + port, + timeout=120, + ssl=False, + opener=None, + deserialize=True, + autoretry=False, + autoretry_factor=0.2, + autoretry_max=10, ): Proxy.__init__(self, host, port, timeout, ssl, opener) self._deserialize = deserialize + self.autoretry = autoretry + self.autoretry_factor = autoretry_factor + self.autoretry_max = autoretry_max def __call__(self, url, params=None): if params is None: @@ -110,7 +124,7 @@ def __call__(self, url, params=None): data_json = json.dumps(data) request = Request(url=full_url, data=encode_data(data_json)) request.add_header('Content-Type', 'application/json') - response = self._opener.open(request, timeout=self._timeout) + response = self._get_response(request) if not self._deserialize: return response result = json.load(decode_data(response)) @@ -120,6 +134,32 @@ def __call__(self, url, params=None): ) return result + def _get_response(self, request): + if not self.autoretry: + return self._opener.open(request, timeout=self._timeout) + + stop = False + backoff_factor = self.autoretry_factor + iteration = 1 + + while not stop: + try: + response = self._opener.open(request, timeout=self._timeout) + return response + except HTTPError as e: + if e.code == 429: + sleep_time = backoff_factor * math.pow(2, iteration - 1) + logger.debug( + 'Error "Too Many Requests", retrying in %s', sleep_time + ) + sleep(sleep_time) + if iteration >= self.autoretry_max: + raise + else: + iteration += 1 + else: + raise + class ProxyHTTP(Proxy): """The :class:`ProxyHTTP` class provides a dynamic access From 67dbc963f8cd37a13433ab856ab4e6c0a611f31d Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Fri, 15 Mar 2024 10:18:37 +0100 Subject: [PATCH 03/10] fix(import) --- odoorpc/rpc/jsonrpclib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/odoorpc/rpc/jsonrpclib.py b/odoorpc/rpc/jsonrpclib.py index 6804f33..03f66cb 100644 --- a/odoorpc/rpc/jsonrpclib.py +++ b/odoorpc/rpc/jsonrpclib.py @@ -13,7 +13,7 @@ # Python 2 if sys.version_info[0] < 3: from cookielib import CookieJar - from urllib2 import HTTPCookieProcessor, Request, build_opener + from urllib2 import HTTPCookieProcessor, Request, build_opener, HTTPError def encode_data(data): return data @@ -27,6 +27,7 @@ def decode_data(data): import io from http.cookiejar import CookieJar from urllib.request import HTTPCookieProcessor, Request, build_opener + from urllib.error import HTTPError def encode_data(data): try: From 38bbf1e0ba46213ac2c167af8faf4fc1bf7c2b37 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Wed, 10 Apr 2024 13:49:10 +0200 Subject: [PATCH 04/10] fix(check_value) --- odoorpc/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/odoorpc/fields.py b/odoorpc/fields.py index e8f9831..de512a6 100644 --- a/odoorpc/fields.py +++ b/odoorpc/fields.py @@ -335,7 +335,7 @@ def __set__(self, instance, value): def check_required(self, value): # Accept 0 values - return super(Float, self).check_required() or value == 0 + return super(Float, self).check_required(value) or value == 0 class Integer(BaseField): @@ -361,7 +361,7 @@ def __set__(self, instance, value): def check_required(self, value): # Accept 0 values - return super(Float, self).check_required() or value == 0 + return super(Float, self).check_required(value) or value == 0 class Selection(BaseField): From a46c9ac4ccd9d6914980a00226e7b64ee5d25b4d Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Mon, 20 May 2024 13:15:03 +0200 Subject: [PATCH 05/10] Update odoorpc/odoo.py Co-authored-by: Jairo Llopis <973709+yajo@users.noreply.github.com> --- odoorpc/odoo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoorpc/odoo.py b/odoorpc/odoo.py index 021ed13..d923ad0 100644 --- a/odoorpc/odoo.py +++ b/odoorpc/odoo.py @@ -44,7 +44,7 @@ class ODOO(object): >>> opener = urllib.request.build_opener(auth_handler) >>> odoo = odoorpc.ODOO('example.net', port=80, opener=opener) - You can also configure an autoretry (enable by default for *.odoo.com host) + You can also configure an autoretry (enabled by default for *.odoo.com host) using autoretry boolean option. Number of iterations and backoff factor can also be specified From 9e219c674bc4a3c59391a78528f871756f00e341 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Mon, 20 May 2024 13:15:11 +0200 Subject: [PATCH 06/10] Update odoorpc/rpc/__init__.py Co-authored-by: Jairo Llopis <973709+yajo@users.noreply.github.com> --- odoorpc/rpc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoorpc/rpc/__init__.py b/odoorpc/rpc/__init__.py index 1407d0a..0709148 100644 --- a/odoorpc/rpc/__init__.py +++ b/odoorpc/rpc/__init__.py @@ -7,7 +7,7 @@ Web controllers of `Odoo` expose two kinds of methods: `json` and `http`. These methods can be accessed from the connectors of this module. -An autoretry for 429 error is also provided, and turn on by default +An autoretry for 429 error is also provided, turned on by default for *.odoo.com hosts. """ import sys From 2347749945ce54cdd85aaaaf0ca375cc47b272e5 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Mon, 20 May 2024 13:15:20 +0200 Subject: [PATCH 07/10] Update odoorpc/rpc/__init__.py Co-authored-by: Jairo Llopis <973709+yajo@users.noreply.github.com> --- odoorpc/rpc/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoorpc/rpc/__init__.py b/odoorpc/rpc/__init__.py index 0709148..509c51e 100644 --- a/odoorpc/rpc/__init__.py +++ b/odoorpc/rpc/__init__.py @@ -28,7 +28,7 @@ class Connector(object): """Connector base class defining the interface used to interact with a server. - You can also configure an autoretry (enable by default for *.odoo.com host) + You can also configure an autoretry (enabled by default for *.odoo.com host) using the ``autoretry`` boolean option. If a 429 HTTP error is raised, the script will automatically retry after ``backoff_factor * math.pow(2, iteration - 1)`` From 5941734b8e95266b2349eddf9034851398b0cba9 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Mon, 20 May 2024 13:15:31 +0200 Subject: [PATCH 08/10] Update odoorpc/rpc/jsonrpclib.py Co-authored-by: Jairo Llopis <973709+yajo@users.noreply.github.com> --- odoorpc/rpc/jsonrpclib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/odoorpc/rpc/jsonrpclib.py b/odoorpc/rpc/jsonrpclib.py index 03f66cb..c9e6db9 100644 --- a/odoorpc/rpc/jsonrpclib.py +++ b/odoorpc/rpc/jsonrpclib.py @@ -139,11 +139,10 @@ def _get_response(self, request): if not self.autoretry: return self._opener.open(request, timeout=self._timeout) - stop = False backoff_factor = self.autoretry_factor iteration = 1 - while not stop: + while True: try: response = self._opener.open(request, timeout=self._timeout) return response From 1c5c27892db8cea170302b5c69a96ccd38b3cbf7 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Mon, 20 May 2024 13:15:53 +0200 Subject: [PATCH 09/10] Update odoorpc/odoo.py Co-authored-by: Jairo Llopis <973709+yajo@users.noreply.github.com> --- odoorpc/odoo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoorpc/odoo.py b/odoorpc/odoo.py index d923ad0..ae76f39 100644 --- a/odoorpc/odoo.py +++ b/odoorpc/odoo.py @@ -77,7 +77,7 @@ def __init__( timeout=120, version=None, opener=None, - **kwargs, + autoretry=False, ): if protocol not in ['jsonrpc', 'jsonrpc+ssl']: txt = ( From 9c365ac238c6ced9323fada8b8f8040e54be7530 Mon Sep 17 00:00:00 2001 From: Anthony Martinet Date: Wed, 29 May 2024 14:50:17 +0200 Subject: [PATCH 10/10] fix(kwargs): all the way --- odoorpc/odoo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoorpc/odoo.py b/odoorpc/odoo.py index ae76f39..d923ad0 100644 --- a/odoorpc/odoo.py +++ b/odoorpc/odoo.py @@ -77,7 +77,7 @@ def __init__( timeout=120, version=None, opener=None, - autoretry=False, + **kwargs, ): if protocol not in ['jsonrpc', 'jsonrpc+ssl']: txt = (