[FIX] tools: provide our own zeep Client
The API of zeep changes according to the version of zeep. We want to limit the number of used methods and attributes of the zeep client by our developers. Hence, we provide our own zeep Client limited to the attribute and method we really need. In addition, a timeout for GET/POST requests should be applied by default when creating a new Client, which is not the case by default. We override that behavior to always provide a default timeout, and an easier API for developers wanting to change the default timeout. Before it was needed to import `Transport` from `zeep`, instanciate that `Transport`, with a timeout and optionally a session, and then pass that transport instance to the creation of the Client. We provide a way to directly pass these timeout parameters through the Client constructor. In addition, we serialize the returned values of Zeep service operation calls, to make sure we return simple types in methods of models e.g. bools, integers, string, ...
This commit is contained in:
@@ -4,9 +4,9 @@ from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools import float_repr, find_xml_value
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.tools.misc import formatLang
|
||||
from odoo.tools.zeep import Client
|
||||
|
||||
from markupsafe import Markup
|
||||
from zeep import Client
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# UNIT OF MEASURE
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import datetime
|
||||
import string
|
||||
import zeep
|
||||
import re
|
||||
import stdnum
|
||||
from stdnum.eu.vat import check_vies
|
||||
@@ -11,6 +10,7 @@ from stdnum import luhn
|
||||
import logging
|
||||
|
||||
from odoo import api, models, fields, tools, _
|
||||
from odoo.tools import zeep
|
||||
from odoo.tools.misc import ustr
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
@@ -4,16 +4,14 @@ from collections import defaultdict
|
||||
from urllib3.util.ssl_ import create_urllib3_context, DEFAULT_CIPHERS
|
||||
from urllib3.contrib.pyopenssl import inject_into_urllib3
|
||||
from OpenSSL.crypto import load_certificate, load_privatekey, FILETYPE_PEM
|
||||
from zeep.transports import Transport
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import html_escape
|
||||
from odoo.tools import html_escape, zeep
|
||||
|
||||
import math
|
||||
import json
|
||||
import requests
|
||||
import zeep
|
||||
|
||||
|
||||
# Custom patches to perform the WSDL requests.
|
||||
@@ -475,8 +473,7 @@ class AccountEdiFormat(models.Model):
|
||||
session.cert = company.l10n_es_edi_certificate_id
|
||||
session.mount('https://', PatchedHTTPAdapter())
|
||||
|
||||
transport = Transport(operation_timeout=60, timeout=60, session=session)
|
||||
client = zeep.Client(connection_vals['url'], transport=transport)
|
||||
client = zeep.Client(connection_vals['url'], operation_timeout=60, timeout=60, session=session)
|
||||
|
||||
if invoices[0].is_sale_document():
|
||||
service_name = 'SuministroFactEmitidas'
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from zeep.transports import Transport
|
||||
from zeep.plugins import Plugin
|
||||
from zeep.settings import Settings
|
||||
from zeep.ns import *
|
||||
|
||||
from . import exceptions
|
||||
from .client import Client
|
||||
@@ -0,0 +1,169 @@
|
||||
import zeep
|
||||
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime, timedelta
|
||||
from types import SimpleNamespace, FunctionType
|
||||
|
||||
|
||||
TIMEOUT = 30
|
||||
SERIALIZABLE_TYPES = (type(None), bool, int, float, str, bytes, tuple, list, dict, Decimal, date, datetime, timedelta)
|
||||
|
||||
|
||||
class Client:
|
||||
"""A wrapper for Zeep.Client
|
||||
|
||||
* providing a simpler API to pass timeouts and session,
|
||||
* restricting its attributes to a few, most-commonly used accross Odoo's modules,
|
||||
* serializing the returned values of its methods.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
load_timeout = kwargs.pop('timeout', None)
|
||||
operation_timeout = kwargs.pop('operation_timeout', None)
|
||||
session = kwargs.pop('session', None)
|
||||
|
||||
client = zeep.Client(*args, **kwargs)
|
||||
|
||||
# The timeout for loading wsdl and xsd documents.
|
||||
client.transport.load_timeout = load_timeout or client.transport.load_timeout or TIMEOUT
|
||||
# The timeout for operations (POST/GET)
|
||||
client.transport.operation_timeout = operation_timeout or client.transport.operation_timeout or TIMEOUT
|
||||
if session:
|
||||
client.transport.session = session
|
||||
|
||||
self.__obj = client
|
||||
self.__service = None
|
||||
|
||||
@classmethod
|
||||
def __serialize_object(cls, obj):
|
||||
if isinstance(obj, list):
|
||||
return [cls.__serialize_object(sub) for sub in obj]
|
||||
if isinstance(obj, (dict, zeep.xsd.valueobjects.CompoundValue)):
|
||||
result = SerialProxy(**{key: cls.__serialize_object(obj[key]) for key in obj})
|
||||
return result
|
||||
if type(obj) in SERIALIZABLE_TYPES:
|
||||
return obj
|
||||
raise ValueError(f'{obj} is not serializable')
|
||||
|
||||
@classmethod
|
||||
def __serialize_object_wrapper(cls, method):
|
||||
def wrapper(*args, **kwargs):
|
||||
return cls.__serialize_object(method(*args, **kwargs))
|
||||
return wrapper
|
||||
|
||||
@property
|
||||
def service(self):
|
||||
if not self.__service:
|
||||
self.__service = ReadOnlyMethodNamespace(**{
|
||||
key: self.__serialize_object_wrapper(operation)
|
||||
for key, operation in self.__obj.service._operations.items()
|
||||
})
|
||||
return self.__service
|
||||
|
||||
def type_factory(self, namespace):
|
||||
types = self.__obj.wsdl.types
|
||||
namespace = namespace if namespace in types.namespaces else types.get_ns_prefix(namespace)
|
||||
documents = types.documents.get_by_namespace(namespace, fail_silently=True)
|
||||
types = {
|
||||
key[len(f'{{{namespace}}}'):]: type_
|
||||
for document in documents
|
||||
for key, type_ in document._types.items()
|
||||
}
|
||||
return ReadOnlyMethodNamespace(**{key: self.__serialize_object_wrapper(type_) for key, type_ in types.items()})
|
||||
|
||||
def get_type(self, name):
|
||||
return self.__serialize_object_wrapper(self.__obj.wsdl.types.get_type(name))
|
||||
|
||||
def create_service(self, binding_name, address):
|
||||
service = self.__obj.create_service(binding_name, address)
|
||||
return ReadOnlyMethodNamespace(**{
|
||||
key: self.__serialize_object_wrapper(operation)
|
||||
for key, operation in service._operations.items()
|
||||
})
|
||||
|
||||
|
||||
class ReadOnlyMethodNamespace(SimpleNamespace):
|
||||
"""A read-only attribute-based namespace not prefixed by `_` and restricted to functions.
|
||||
|
||||
By default, `types.SympleNamespace` doesn't implement `__setitem__` and `__delitem__`,
|
||||
no need to implement them to ensure the read-only property of this class.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
assert all(not key.startswith('_') and isinstance(value, FunctionType) for key, value in kwargs.items())
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__dict__[key]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def __delattr__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SerialProxy(SimpleNamespace):
|
||||
"""An attribute-based namespace not prefixed by `_` and restricted to few types.
|
||||
|
||||
It pretends to be a zeep `CompoundValue` so zeep.helpers.serialize_object threats it as such.
|
||||
|
||||
`__getitem__` and `__delitem__` are supported, but `__setitem__` is prevented,
|
||||
e.g.
|
||||
```py
|
||||
proxy = SerialProxy(foo='foo')
|
||||
proxy.foo # Allowed
|
||||
proxy['foo'] # Allowed
|
||||
proxy.foo = 'bar' # Allowed
|
||||
proxy['foo'] = 'bar' # Prevented
|
||||
del proxy.foo # Allowed
|
||||
del proxy['foo'] # Allowed
|
||||
```
|
||||
"""
|
||||
|
||||
# Pretend to be a CompoundValue so zeep can serialize this when sending a request with this object in the payload
|
||||
# https://stackoverflow.com/a/42958013
|
||||
# https://github.com/mvantellingen/python-zeep/blob/a65b4363c48b5c3f687b8df570bcbada8ba66b9b/src/zeep/helpers.py#L15
|
||||
@property
|
||||
def __class__(self):
|
||||
return zeep.xsd.valueobjects.CompoundValue
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
self.__check(key, value)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self.__check(key, value)
|
||||
return super().__setattr__(key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__getattribute__(key)
|
||||
|
||||
# Not required as SimpleNamespace doesn't implement it by default, but this makes it explicit.
|
||||
def __setitem__(self, key, value):
|
||||
raise NotImplementedError
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.__delattr__(key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__dict__)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
def keys(self):
|
||||
return self.__dict__.keys()
|
||||
|
||||
def values(self):
|
||||
return self.__dict__.values()
|
||||
|
||||
def items(self):
|
||||
return self.__dict__.items()
|
||||
|
||||
@classmethod
|
||||
def __check(cls, key, value):
|
||||
assert not key.startswith('_')
|
||||
assert type(value) in SERIALIZABLE_TYPES + (SerialProxy,)
|
||||
@@ -0,0 +1 @@
|
||||
from zeep.exceptions import *
|
||||
@@ -0,0 +1 @@
|
||||
from zeep.helpers import serialize_object
|
||||
@@ -0,0 +1 @@
|
||||
from zeep.wsa import *
|
||||
@@ -0,0 +1 @@
|
||||
from zeep.wsdl.utils import etree_to_string
|
||||
@@ -0,0 +1 @@
|
||||
from zeep.wsse import *
|
||||
@@ -0,0 +1 @@
|
||||
from zeep.wsse.username import UsernameToken
|
||||
Reference in New Issue
Block a user