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:05 +0000

1import zeep 

2 

3from decimal import Decimal 

4from datetime import date, datetime, timedelta 

5from requests import Response 

6from types import SimpleNamespace, FunctionType 

7 

8 

9TIMEOUT = 30 

10SERIALIZABLE_TYPES = ( 

11 type(None), bool, int, float, str, bytes, tuple, list, dict, Decimal, date, datetime, timedelta, Response 

12) 

13 

14 

15class Client: 

16 """A wrapper for Zeep.Client 

17 

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 

30 

31 client = zeep.Client(*args, **kwargs) 

32 

33 self.__obj = client 

34 self.__service = None 

35 

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') 

46 

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 

52 

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 

61 

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()}) 

72 

73 def get_type(self, name): 

74 return self.__serialize_object_wrapper(self.__obj.wsdl.types.get_type(name)) 

75 

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 }) 

82 

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) 

91 

92 

93class ReadOnlyMethodNamespace(SimpleNamespace): 

94 """A read-only attribute-based namespace not prefixed by `_` and restricted to functions. 

95 

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) 

107 

108 def __getitem__(self, key): 

109 return self.__dict__[key] 

110 

111 def __setattr__(self, key, value): 

112 raise NotImplementedError 

113 

114 def __delattr__(self, key): 

115 raise NotImplementedError 

116 

117 

118class SerialProxy(SimpleNamespace): 

119 """An attribute-based namespace not prefixed by `_` and restricted to few types. 

120 

121 It pretends to be a zeep `CompoundValue` so zeep.helpers.serialize_object threats it as such. 

122 

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 """ 

135 

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 

142 

143 def __init__(self, **kwargs): 

144 for key, value in kwargs.items(): 

145 self.__check(key, value) 

146 super().__init__(**kwargs) 

147 

148 def __setattr__(self, key, value): 

149 self.__check(key, value) 

150 return super().__setattr__(key, value) 

151 

152 def __getitem__(self, key): 

153 self.__check(key, None) 

154 return self.__getattribute__(key) 

155 

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 

159 

160 def __delitem__(self, key): 

161 self.__check(key, None) 

162 self.__delattr__(key) 

163 

164 def __iter__(self): 

165 return iter(self.__dict__) 

166 

167 def __repr__(self): 

168 return repr(self.__dict__) 

169 

170 def __str__(self): 

171 return str(self.__dict__) 

172 

173 def keys(self): 

174 return self.__dict__.keys() 

175 

176 def values(self): 

177 return self.__dict__.values() 

178 

179 def items(self): 

180 return self.__dict__.items() 

181 

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,)