diff --git a/addons/account_edi_ubl_cii/models/account_edi_common.py b/addons/account_edi_ubl_cii/models/account_edi_common.py index b1314f5ff3e..034c60c73e3 100644 --- a/addons/account_edi_ubl_cii/models/account_edi_common.py +++ b/addons/account_edi_ubl_cii/models/account_edi_common.py @@ -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 diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py index fabfcf45b28..3074c73811c 100644 --- a/addons/base_vat/models/res_partner.py +++ b/addons/base_vat/models/res_partner.py @@ -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 diff --git a/addons/l10n_es_edi_sii/models/account_edi_format.py b/addons/l10n_es_edi_sii/models/account_edi_format.py index a0503d344c6..4789537f302 100644 --- a/addons/l10n_es_edi_sii/models/account_edi_format.py +++ b/addons/l10n_es_edi_sii/models/account_edi_format.py @@ -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' diff --git a/odoo/tools/zeep/__init__.py b/odoo/tools/zeep/__init__.py new file mode 100644 index 00000000000..cb3a74b1249 --- /dev/null +++ b/odoo/tools/zeep/__init__.py @@ -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 diff --git a/odoo/tools/zeep/client.py b/odoo/tools/zeep/client.py new file mode 100644 index 00000000000..b335aa23b9d --- /dev/null +++ b/odoo/tools/zeep/client.py @@ -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,) diff --git a/odoo/tools/zeep/exceptions.py b/odoo/tools/zeep/exceptions.py new file mode 100644 index 00000000000..9bf843d2123 --- /dev/null +++ b/odoo/tools/zeep/exceptions.py @@ -0,0 +1 @@ +from zeep.exceptions import * diff --git a/odoo/tools/zeep/helpers.py b/odoo/tools/zeep/helpers.py new file mode 100644 index 00000000000..5db1724ed67 --- /dev/null +++ b/odoo/tools/zeep/helpers.py @@ -0,0 +1 @@ +from zeep.helpers import serialize_object diff --git a/odoo/tools/zeep/wsa.py b/odoo/tools/zeep/wsa.py new file mode 100644 index 00000000000..fa10aff9a5a --- /dev/null +++ b/odoo/tools/zeep/wsa.py @@ -0,0 +1 @@ +from zeep.wsa import * diff --git a/odoo/tools/zeep/wsdl/utils.py b/odoo/tools/zeep/wsdl/utils.py new file mode 100644 index 00000000000..e9b8cc3c653 --- /dev/null +++ b/odoo/tools/zeep/wsdl/utils.py @@ -0,0 +1 @@ +from zeep.wsdl.utils import etree_to_string diff --git a/odoo/tools/zeep/wsse/__init__.py b/odoo/tools/zeep/wsse/__init__.py new file mode 100644 index 00000000000..0b3ad8c67ce --- /dev/null +++ b/odoo/tools/zeep/wsse/__init__.py @@ -0,0 +1 @@ +from zeep.wsse import * diff --git a/odoo/tools/zeep/wsse/username/__init__.py b/odoo/tools/zeep/wsse/username/__init__.py new file mode 100644 index 00000000000..8b5481e46a5 --- /dev/null +++ b/odoo/tools/zeep/wsse/username/__init__.py @@ -0,0 +1 @@ +from zeep.wsse.username import UsernameToken