Coverage for adhoc-cicd-odoo-odoo / odoo / tools / zeep / client.py: 37%
97 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:15 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-09 18:15 +0000
1import zeep
3from decimal import Decimal
4from datetime import date, datetime, timedelta
5from requests import Response
6from types import SimpleNamespace, FunctionType
9TIMEOUT = 30
10SERIALIZABLE_TYPES = (
11 type(None), bool, int, float, str, bytes, tuple, list, dict, Decimal, date, datetime, timedelta, Response
12)
15class Client:
16 """A wrapper for Zeep.Client
18 * providing a simpler API to pass timeouts and session,
19 * restricting its attributes to a few, most-commonly used accross Odoo's modules,
20 * serializing the returned values of its methods.
21 """
22 def __init__(self, *args, **kwargs):
23 transport = kwargs.setdefault('transport', zeep.Transport())
24 # The timeout for loading wsdl and xsd documents.
25 transport.load_timeout = kwargs.pop('timeout', None) or transport.load_timeout or TIMEOUT
26 # The timeout for operations (POST/GET)
27 transport.operation_timeout = kwargs.pop('operation_timeout', None) or transport.operation_timeout or TIMEOUT
28 # The `requests.session` used for HTTP requests
29 transport.session = kwargs.pop('session', None) or transport.session
31 client = zeep.Client(*args, **kwargs)
33 self.__obj = client
34 self.__service = None
36 @classmethod
37 def __serialize_object(cls, obj):
38 if isinstance(obj, list):
39 return [cls.__serialize_object(sub) for sub in obj]
40 if isinstance(obj, (dict, zeep.xsd.valueobjects.CompoundValue)):
41 result = SerialProxy(**{key: cls.__serialize_object(obj[key]) for key in obj})
42 return result
43 if type(obj) in SERIALIZABLE_TYPES:
44 return obj
45 raise ValueError(f'{obj} is not serializable')
47 @classmethod
48 def __serialize_object_wrapper(cls, method):
49 def wrapper(*args, **kwargs):
50 return cls.__serialize_object(method(*args, **kwargs))
51 return wrapper
53 @property
54 def service(self):
55 if not self.__service:
56 self.__service = ReadOnlyMethodNamespace(**{
57 key: self.__serialize_object_wrapper(operation)
58 for key, operation in self.__obj.service._operations.items()
59 })
60 return self.__service
62 def type_factory(self, namespace):
63 types = self.__obj.wsdl.types
64 namespace = namespace if namespace in types.namespaces else types.get_ns_prefix(namespace)
65 documents = types.documents.get_by_namespace(namespace, fail_silently=True)
66 types = {
67 key[len(f'{{{namespace}}}'):]: type_
68 for document in documents
69 for key, type_ in document._types.items()
70 }
71 return ReadOnlyMethodNamespace(**{key: self.__serialize_object_wrapper(type_) for key, type_ in types.items()})
73 def get_type(self, name):
74 return self.__serialize_object_wrapper(self.__obj.wsdl.types.get_type(name))
76 def create_service(self, binding_name, address):
77 service = self.__obj.create_service(binding_name, address)
78 return ReadOnlyMethodNamespace(**{
79 key: self.__serialize_object_wrapper(operation)
80 for key, operation in service._operations.items()
81 })
83 def bind(self, service_name, port_name):
84 service = self.__obj.bind(service_name, port_name)
85 operations = {
86 key: self.__serialize_object_wrapper(operation)
87 for key, operation in service._operations.items()
88 }
89 operations['_binding_options'] = service._binding_options
90 return ReadOnlyMethodNamespace(**operations)
93class ReadOnlyMethodNamespace(SimpleNamespace):
94 """A read-only attribute-based namespace not prefixed by `_` and restricted to functions.
96 By default, `types.SympleNamespace` doesn't implement `__setitem__` and `__delitem__`,
97 no need to implement them to ensure the read-only property of this class.
98 """
99 def __init__(self, **kwargs):
100 assert all(
101 (not key.startswith('_') and isinstance(value, FunctionType))
102 or
103 (key == '_binding_options' and isinstance(value, dict))
104 for key, value in kwargs.items()
105 )
106 super().__init__(**kwargs)
108 def __getitem__(self, key):
109 return self.__dict__[key]
111 def __setattr__(self, key, value):
112 raise NotImplementedError
114 def __delattr__(self, key):
115 raise NotImplementedError
118class SerialProxy(SimpleNamespace):
119 """An attribute-based namespace not prefixed by `_` and restricted to few types.
121 It pretends to be a zeep `CompoundValue` so zeep.helpers.serialize_object threats it as such.
123 `__getitem__` and `__delitem__` are supported, but `__setitem__` is prevented,
124 e.g.
125 ```py
126 proxy = SerialProxy(foo='foo')
127 proxy.foo # Allowed
128 proxy['foo'] # Allowed
129 proxy.foo = 'bar' # Allowed
130 proxy['foo'] = 'bar' # Prevented
131 del proxy.foo # Allowed
132 del proxy['foo'] # Allowed
133 ```
134 """
136 # Pretend to be a CompoundValue so zeep can serialize this when sending a request with this object in the payload
137 # https://stackoverflow.com/a/42958013
138 # https://github.com/mvantellingen/python-zeep/blob/a65b4363c48b5c3f687b8df570bcbada8ba66b9b/src/zeep/helpers.py#L15
139 @property
140 def __class__(self):
141 return zeep.xsd.valueobjects.CompoundValue
143 def __init__(self, **kwargs):
144 for key, value in kwargs.items():
145 self.__check(key, value)
146 super().__init__(**kwargs)
148 def __setattr__(self, key, value):
149 self.__check(key, value)
150 return super().__setattr__(key, value)
152 def __getitem__(self, key):
153 self.__check(key, None)
154 return self.__getattribute__(key)
156 # Not required as SimpleNamespace doesn't implement it by default, but this makes it explicit.
157 def __setitem__(self, key, value):
158 raise NotImplementedError
160 def __delitem__(self, key):
161 self.__check(key, None)
162 self.__delattr__(key)
164 def __iter__(self):
165 return iter(self.__dict__)
167 def __repr__(self):
168 return repr(self.__dict__)
170 def __str__(self):
171 return str(self.__dict__)
173 def keys(self):
174 return self.__dict__.keys()
176 def values(self):
177 return self.__dict__.values()
179 def items(self):
180 return self.__dict__.items()
182 @classmethod
183 def __check(cls, key, value):
184 assert not key.startswith('_') or key.startswith('_value_')
185 assert type(value) in SERIALIZABLE_TYPES + (SerialProxy,)