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 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): diff --git a/odoorpc/odoo.py b/odoorpc/odoo.py index c86df55..d923ad0 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 (enabled 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..509c51e 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, turned 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 (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)`` + + 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..c9e6db9 100644 --- a/odoorpc/rpc/jsonrpclib.py +++ b/odoorpc/rpc/jsonrpclib.py @@ -5,13 +5,15 @@ import copy import json import logging +import math import random import sys +from time import sleep # 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 @@ -25,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: @@ -88,10 +91,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 +125,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 +135,31 @@ 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) + + backoff_factor = self.autoretry_factor + iteration = 1 + + while True: + 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