[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:
Denis Ledoux
2024-03-29 15:35:03 +00:00
parent 67dc32b61f
commit 8e5dc5352c
11 changed files with 186 additions and 7 deletions
@@ -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 -1
View File
@@ -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'
+7
View File
@@ -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
+169
View File
@@ -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,)
+1
View File
@@ -0,0 +1 @@
from zeep.exceptions import *
+1
View File
@@ -0,0 +1 @@
from zeep.helpers import serialize_object
+1
View File
@@ -0,0 +1 @@
from zeep.wsa import *
+1
View File
@@ -0,0 +1 @@
from zeep.wsdl.utils import etree_to_string
+1
View File
@@ -0,0 +1 @@
from zeep.wsse import *
@@ -0,0 +1 @@
from zeep.wsse.username import UsernameToken