Skip to content
Open
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: 2 additions & 0 deletions odoorpc/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions odoorpc/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
20 changes: 19 additions & 1 deletion odoorpc/odoo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -65,6 +77,7 @@ def __init__(
timeout=120,
version=None,
opener=None,
**kwargs,
):
if protocol not in ['jsonrpc', 'jsonrpc+ssl']:
txt = (
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 39 additions & 3 deletions odoorpc/rpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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
Comment on lines +60 to +66
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't particularly like this kind of magic. But in case it were, then you can default the autoretry arg to None, enabling this magic, insteado of relying in the existence of the kwarg. It would be the same result, but simplify the methods signature.

Making things obvious and predictable is good IMHO.

In any case, this (and the rest of the feature) deserves an entry in the docs.

self._autoretry_factor = kwargs.get('autoretry_factor', 0.2)
self._autoretry_max = kwargs.get('autoretry_max', 10)

@property
def ssl(self):
Expand Down Expand Up @@ -199,15 +227,19 @@ 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
if opener is None:
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):
Expand All @@ -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,
Expand Down Expand Up @@ -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()

Expand Down
46 changes: 43 additions & 3 deletions odoorpc/rpc/jsonrpclib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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))
Expand All @@ -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
Expand Down