proteus 7.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proteus/__init__.py +1421 -0
- proteus/config.py +423 -0
- proteus/pyson.py +771 -0
- proteus/tests/__init__.py +9 -0
- proteus/tests/common.py +17 -0
- proteus/tests/test_action.py +49 -0
- proteus/tests/test_config.py +40 -0
- proteus/tests/test_context.py +16 -0
- proteus/tests/test_model.py +406 -0
- proteus/tests/test_readme.py +23 -0
- proteus/tests/test_report.py +29 -0
- proteus/tests/test_wizard.py +54 -0
- proteus-7.8.0.dist-info/METADATA +223 -0
- proteus-7.8.0.dist-info/RECORD +18 -0
- proteus-7.8.0.dist-info/WHEEL +5 -0
- proteus-7.8.0.dist-info/licenses/LICENSE +841 -0
- proteus-7.8.0.dist-info/top_level.txt +1 -0
- proteus-7.8.0.dist-info/zip-safe +1 -0
proteus/config.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
2
|
+
# this repository contains the full copyright notices and license terms.
|
|
3
|
+
"""
|
|
4
|
+
Configuration functions for the proteus package for Tryton.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import datetime
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import xmlrpc.client
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
|
|
16
|
+
import defusedxml.xmlrpc
|
|
17
|
+
|
|
18
|
+
__all__ = ['set_trytond', 'set_xmlrpc', 'get_config']
|
|
19
|
+
|
|
20
|
+
defusedxml.xmlrpc.monkey_patch()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def dump_decimal(self, value, write):
|
|
24
|
+
write('<value><bigdecimal>')
|
|
25
|
+
write(str(Decimal(value)))
|
|
26
|
+
write('</bigdecimal></value>')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def dump_date(self, value, write):
|
|
30
|
+
value = {'__class__': 'date',
|
|
31
|
+
'year': value.year,
|
|
32
|
+
'month': value.month,
|
|
33
|
+
'day': value.day,
|
|
34
|
+
}
|
|
35
|
+
self.dump_struct(value, write)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dump_time(self, value, write):
|
|
39
|
+
value = {'__class__': 'time',
|
|
40
|
+
'hour': value.hour,
|
|
41
|
+
'minute': value.minute,
|
|
42
|
+
'second': value.second,
|
|
43
|
+
'microsecond': value.microsecond,
|
|
44
|
+
}
|
|
45
|
+
self.dump_struct(value, write)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def dump_timedelta(self, value, write):
|
|
49
|
+
value = {'__class__': 'timedelta',
|
|
50
|
+
'seconds': value.total_seconds(),
|
|
51
|
+
}
|
|
52
|
+
self.dump_struct(value, write)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def dump_long(self, value, write):
|
|
56
|
+
try:
|
|
57
|
+
self.dump_long(value, write)
|
|
58
|
+
except OverflowError:
|
|
59
|
+
write('<value><biginteger>')
|
|
60
|
+
write(str(int(value)))
|
|
61
|
+
write('</biginteger></value>\n')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
xmlrpc.client.Marshaller.dispatch[Decimal] = dump_decimal
|
|
65
|
+
xmlrpc.client.Marshaller.dispatch[datetime.date] = dump_date
|
|
66
|
+
xmlrpc.client.Marshaller.dispatch[datetime.time] = dump_time
|
|
67
|
+
xmlrpc.client.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
|
|
68
|
+
xmlrpc.client.Marshaller.dispatch[int] = dump_long
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def dump_struct(self, value, write, escape=xmlrpc.client.escape):
|
|
72
|
+
converted_value = {}
|
|
73
|
+
for k, v in value.items():
|
|
74
|
+
if isinstance(k, int):
|
|
75
|
+
k = str(k)
|
|
76
|
+
elif isinstance(k, float):
|
|
77
|
+
k = repr(k)
|
|
78
|
+
converted_value[k] = v
|
|
79
|
+
return self.dump_struct(converted_value, write, escape=escape)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
xmlrpc.client.Marshaller.dispatch[dict] = dump_struct
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class XMLRPCDecoder(object):
|
|
86
|
+
|
|
87
|
+
decoders = {}
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def register(cls, klass, decoder):
|
|
91
|
+
assert klass not in cls.decoders
|
|
92
|
+
cls.decoders[klass] = decoder
|
|
93
|
+
|
|
94
|
+
def __call__(self, dct):
|
|
95
|
+
if dct.get('__class__') in self.decoders:
|
|
96
|
+
return self.decoders[dct['__class__']](dct)
|
|
97
|
+
return dct
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
XMLRPCDecoder.register('date',
|
|
101
|
+
lambda dct: datetime.date(dct['year'], dct['month'], dct['day']))
|
|
102
|
+
XMLRPCDecoder.register('time',
|
|
103
|
+
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
|
|
104
|
+
dct['microsecond']))
|
|
105
|
+
XMLRPCDecoder.register('timedelta',
|
|
106
|
+
lambda dct: datetime.timedelta(seconds=dct['seconds']))
|
|
107
|
+
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def end_struct(self, data):
|
|
111
|
+
mark = self._marks.pop()
|
|
112
|
+
# map structs to Python dictionaries
|
|
113
|
+
dct = {}
|
|
114
|
+
items = self._stack[mark:]
|
|
115
|
+
for i in range(0, len(items), 2):
|
|
116
|
+
dct[items[i]] = items[i + 1]
|
|
117
|
+
dct = XMLRPCDecoder()(dct)
|
|
118
|
+
self._stack[mark:] = [dct]
|
|
119
|
+
self._value = 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
xmlrpc.client.Unmarshaller.dispatch['struct'] = end_struct
|
|
123
|
+
|
|
124
|
+
_CONFIG = threading.local()
|
|
125
|
+
_CONFIG.current = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ContextManager(object):
|
|
129
|
+
'Context Manager for the tryton context'
|
|
130
|
+
|
|
131
|
+
def __init__(self, config):
|
|
132
|
+
self.config = config
|
|
133
|
+
self.context = config.context
|
|
134
|
+
|
|
135
|
+
def __enter__(self):
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
139
|
+
self.config._context = self.context
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Config(object):
|
|
143
|
+
'Config interface'
|
|
144
|
+
|
|
145
|
+
def __init__(self):
|
|
146
|
+
super().__init__()
|
|
147
|
+
self._context = {}
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def context(self):
|
|
151
|
+
return self._context.copy()
|
|
152
|
+
|
|
153
|
+
def set_context(self, context=None, **kwargs):
|
|
154
|
+
ctx_manager = ContextManager(self)
|
|
155
|
+
|
|
156
|
+
if context is None:
|
|
157
|
+
context = {}
|
|
158
|
+
self._context = self.context
|
|
159
|
+
self._context.update(context)
|
|
160
|
+
self._context.update(kwargs)
|
|
161
|
+
return ctx_manager
|
|
162
|
+
|
|
163
|
+
def reset_context(self):
|
|
164
|
+
ctx_manager = ContextManager(self)
|
|
165
|
+
self._context = {}
|
|
166
|
+
return ctx_manager
|
|
167
|
+
|
|
168
|
+
def get_proxy(self, name):
|
|
169
|
+
raise NotImplementedError
|
|
170
|
+
|
|
171
|
+
def get_proxy_methods(self, name):
|
|
172
|
+
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class _TrytondMethod(object):
|
|
176
|
+
|
|
177
|
+
def __init__(self, name, model, config):
|
|
178
|
+
super().__init__()
|
|
179
|
+
self._name = name
|
|
180
|
+
self._object = model
|
|
181
|
+
self._config = config
|
|
182
|
+
|
|
183
|
+
def __call__(self, *args, **kwargs):
|
|
184
|
+
from trytond.rpc import RPC, RPCReturnException
|
|
185
|
+
from trytond.tools import is_instance_method
|
|
186
|
+
from trytond.transaction import Transaction, TransactionError
|
|
187
|
+
from trytond.worker import run_task
|
|
188
|
+
|
|
189
|
+
if self._name in self._object.__rpc__:
|
|
190
|
+
rpc = self._object.__rpc__[self._name]
|
|
191
|
+
elif self._name in getattr(self._object, '_buttons', {}):
|
|
192
|
+
rpc = RPC(readonly=False, instantiate=0)
|
|
193
|
+
else:
|
|
194
|
+
raise TypeError('%s is not callable' % self._name)
|
|
195
|
+
|
|
196
|
+
extras = {}
|
|
197
|
+
while True:
|
|
198
|
+
with Transaction().start(self._config.database_name,
|
|
199
|
+
self._config.user, readonly=rpc.readonly,
|
|
200
|
+
**extras) as transaction:
|
|
201
|
+
try:
|
|
202
|
+
(c_args, c_kwargs,
|
|
203
|
+
transaction.context, transaction.timestamp) \
|
|
204
|
+
= rpc.convert(self._object, *args, **kwargs)
|
|
205
|
+
if self._config.skip_warning:
|
|
206
|
+
transaction.context['_skip_warnings'] = True
|
|
207
|
+
meth = getattr(self._object, self._name)
|
|
208
|
+
if (rpc.instantiate is None
|
|
209
|
+
or not is_instance_method(
|
|
210
|
+
self._object, self._name)):
|
|
211
|
+
result = rpc.result(meth(*c_args, **c_kwargs))
|
|
212
|
+
else:
|
|
213
|
+
assert rpc.instantiate == 0
|
|
214
|
+
inst = c_args.pop(0)
|
|
215
|
+
if hasattr(inst, self._name):
|
|
216
|
+
result = rpc.result(
|
|
217
|
+
meth(inst, *c_args, **c_kwargs))
|
|
218
|
+
else:
|
|
219
|
+
result = [rpc.result(meth(i, *c_args, **c_kwargs))
|
|
220
|
+
for i in inst]
|
|
221
|
+
except TransactionError as e:
|
|
222
|
+
transaction.rollback()
|
|
223
|
+
e.fix(extras)
|
|
224
|
+
continue
|
|
225
|
+
except RPCReturnException as e:
|
|
226
|
+
transaction.rollback()
|
|
227
|
+
transaction.tasks.clear()
|
|
228
|
+
result = e.result()
|
|
229
|
+
transaction.commit()
|
|
230
|
+
break
|
|
231
|
+
while transaction.tasks:
|
|
232
|
+
task_id = transaction.tasks.pop()
|
|
233
|
+
run_task(self._config.database_name, task_id)
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class TrytondProxy(object):
|
|
238
|
+
'Proxy for function call for trytond'
|
|
239
|
+
|
|
240
|
+
def __init__(self, name, config, type='model'):
|
|
241
|
+
super().__init__()
|
|
242
|
+
self._config = config
|
|
243
|
+
self._object = config.pool.get(name, type=type)
|
|
244
|
+
__init__.__doc__ = object.__init__.__doc__
|
|
245
|
+
|
|
246
|
+
def __getattr__(self, name):
|
|
247
|
+
'Return attribute value'
|
|
248
|
+
return _TrytondMethod(name, self._object, self._config)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TrytondConfig(Config):
|
|
252
|
+
'Configuration for trytond'
|
|
253
|
+
|
|
254
|
+
def __init__(self, database=None, user='admin', config_file=None):
|
|
255
|
+
super().__init__()
|
|
256
|
+
if not database:
|
|
257
|
+
database = os.environ.get('TRYTOND_DATABASE_URI')
|
|
258
|
+
elif (os.environ.get('TRYTOND_DATABASE_URI')
|
|
259
|
+
and not urllib.parse.urlparse(database).scheme):
|
|
260
|
+
url = urllib.parse.urlparse(os.environ['TRYTOND_DATABASE_URI'])
|
|
261
|
+
os.environ['TRYTOND_DATABASE_URI'] = urllib.parse.urlunparse(
|
|
262
|
+
url._replace(path=database))
|
|
263
|
+
else:
|
|
264
|
+
os.environ['TRYTOND_DATABASE_URI'] = database
|
|
265
|
+
if not config_file:
|
|
266
|
+
config_file = os.environ.get('TRYTOND_CONFIG')
|
|
267
|
+
import trytond.config as config
|
|
268
|
+
config.update_etc(config_file)
|
|
269
|
+
from trytond.pool import Pool
|
|
270
|
+
from trytond.transaction import Transaction
|
|
271
|
+
self.database = database
|
|
272
|
+
database_name = None
|
|
273
|
+
if database:
|
|
274
|
+
uri = urllib.parse.urlparse(database)
|
|
275
|
+
database_name = uri.path.strip('/')
|
|
276
|
+
if not database_name:
|
|
277
|
+
database_name = os.environ['DB_NAME']
|
|
278
|
+
self.database_name = database_name
|
|
279
|
+
self._user = user
|
|
280
|
+
self.config_file = config_file
|
|
281
|
+
self.skip_warning = False
|
|
282
|
+
|
|
283
|
+
Pool.start()
|
|
284
|
+
self.pool = Pool(database_name)
|
|
285
|
+
self.pool.init()
|
|
286
|
+
|
|
287
|
+
with Transaction().start(self.database_name, 0) as transaction:
|
|
288
|
+
User = self.pool.get('res.user')
|
|
289
|
+
transaction.context = self.context
|
|
290
|
+
with transaction.set_context(active_test=False):
|
|
291
|
+
self.user = User.search([
|
|
292
|
+
('login', '=', user),
|
|
293
|
+
], limit=1)[0].id
|
|
294
|
+
with transaction.set_user(self.user):
|
|
295
|
+
self._context = User.get_preferences(context_only=True)
|
|
296
|
+
__init__.__doc__ = object.__init__.__doc__
|
|
297
|
+
|
|
298
|
+
def __repr__(self):
|
|
299
|
+
return ("proteus.config.TrytondConfig"
|
|
300
|
+
"(%s, %s, config_file=%s)"
|
|
301
|
+
% (repr(self.database), repr(self._user), repr(self.config_file)))
|
|
302
|
+
__repr__.__doc__ = object.__repr__.__doc__
|
|
303
|
+
|
|
304
|
+
def __eq__(self, other):
|
|
305
|
+
if not isinstance(other, TrytondConfig):
|
|
306
|
+
raise NotImplementedError
|
|
307
|
+
return (self.database_name == other.database_name
|
|
308
|
+
and self._user == other._user
|
|
309
|
+
and self.database == other.database
|
|
310
|
+
and self.config_file == other.config_file)
|
|
311
|
+
|
|
312
|
+
def __hash__(self):
|
|
313
|
+
return hash((self.database_name, self._user,
|
|
314
|
+
self.database, self.config_file))
|
|
315
|
+
|
|
316
|
+
def get_proxy(self, name, type='model'):
|
|
317
|
+
'Return Proxy class'
|
|
318
|
+
return TrytondProxy(name, self, type=type)
|
|
319
|
+
|
|
320
|
+
def get_proxy_methods(self, name, type='model'):
|
|
321
|
+
'Return list of methods'
|
|
322
|
+
proxy = self.get_proxy(name, type=type)
|
|
323
|
+
methods = [x for x in proxy._object.__rpc__]
|
|
324
|
+
if hasattr(proxy._object, '_buttons'):
|
|
325
|
+
methods += [x for x in proxy._object._buttons]
|
|
326
|
+
return methods
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def set_trytond(database=None, user='admin',
|
|
330
|
+
config_file=None):
|
|
331
|
+
'Set trytond package as backend'
|
|
332
|
+
_CONFIG.current = TrytondConfig(database, user, config_file=config_file)
|
|
333
|
+
return _CONFIG.current
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class XmlrpcProxy(object):
|
|
337
|
+
'Proxy for function call for XML-RPC'
|
|
338
|
+
|
|
339
|
+
def __init__(self, name, config, type='model'):
|
|
340
|
+
super().__init__()
|
|
341
|
+
self._config = config
|
|
342
|
+
self._object = getattr(config.server, '%s.%s' % (type, name))
|
|
343
|
+
__init__.__doc__ = object.__init__.__doc__
|
|
344
|
+
|
|
345
|
+
def __getattr__(self, name):
|
|
346
|
+
'Return attribute value'
|
|
347
|
+
return getattr(self._object, name)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class XmlrpcConfig(Config):
|
|
351
|
+
'Configuration for XML-RPC'
|
|
352
|
+
|
|
353
|
+
def __init__(self, url, **kwargs):
|
|
354
|
+
super().__init__()
|
|
355
|
+
self.url = url
|
|
356
|
+
self.server = xmlrpc.client.ServerProxy(
|
|
357
|
+
url, allow_none=True, use_builtin_types=True, **kwargs)
|
|
358
|
+
# TODO add user
|
|
359
|
+
self.user = None
|
|
360
|
+
self._context = self.server.model.res.user.get_preferences(True, {})
|
|
361
|
+
__init__.__doc__ = object.__init__.__doc__
|
|
362
|
+
|
|
363
|
+
def __repr__(self):
|
|
364
|
+
return "proteus.config.XmlrpcConfig(%s)" % repr(self.url)
|
|
365
|
+
__repr__.__doc__ = object.__repr__.__doc__
|
|
366
|
+
|
|
367
|
+
def __eq__(self, other):
|
|
368
|
+
if not isinstance(other, XmlrpcConfig):
|
|
369
|
+
raise NotImplementedError
|
|
370
|
+
return self.url == other.url
|
|
371
|
+
|
|
372
|
+
def __hash__(self):
|
|
373
|
+
return hash(self.url)
|
|
374
|
+
|
|
375
|
+
def get_proxy(self, name, type='model'):
|
|
376
|
+
'Return Proxy class'
|
|
377
|
+
return XmlrpcProxy(name, self, type=type)
|
|
378
|
+
|
|
379
|
+
def get_proxy_methods(self, name, type='model'):
|
|
380
|
+
'Return list of methods'
|
|
381
|
+
object_ = '%s.%s' % (type, name)
|
|
382
|
+
return [x[len(object_) + 1:]
|
|
383
|
+
for x in self.server.system.listMethods()
|
|
384
|
+
if x.startswith(object_)
|
|
385
|
+
and '.' not in x[len(object_) + 1:]]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def set_xmlrpc(url, **kwargs):
|
|
389
|
+
'''
|
|
390
|
+
Set XML-RPC as backend.
|
|
391
|
+
It pass the keyword arguments received to xmlrpclib.ServerProxy()
|
|
392
|
+
'''
|
|
393
|
+
_CONFIG.current = XmlrpcConfig(url, **kwargs)
|
|
394
|
+
return _CONFIG.current
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
@contextmanager
|
|
398
|
+
def set_xmlrpc_session(
|
|
399
|
+
url, username, password=None, parameters=None, **kwargs):
|
|
400
|
+
"""
|
|
401
|
+
Set XML-RPC as backend using session.
|
|
402
|
+
"""
|
|
403
|
+
if parameters is None:
|
|
404
|
+
parameters = {}
|
|
405
|
+
else:
|
|
406
|
+
parameters = parameters.copy()
|
|
407
|
+
if password:
|
|
408
|
+
parameters['password'] = password
|
|
409
|
+
server = xmlrpc.client.ServerProxy(
|
|
410
|
+
url, allow_none=True, use_builtin_types=True, **kwargs)
|
|
411
|
+
user_id, session, _ = server.common.db.login(username, parameters)
|
|
412
|
+
session = ':'.join(map(str, [username, user_id, session]))
|
|
413
|
+
auth = base64.encodebytes(session.encode('utf-8')).decode('ascii')
|
|
414
|
+
auth = ''.join(auth.split()) # get rid of whitespace
|
|
415
|
+
kwargs.setdefault('headers', []).append(
|
|
416
|
+
('Authorization', 'Session ' + auth))
|
|
417
|
+
config = set_xmlrpc(url, **kwargs)
|
|
418
|
+
yield config
|
|
419
|
+
config.server.common.db.logout()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def get_config():
|
|
423
|
+
return _CONFIG.current
|