Odooly 2.6.2__tar.gz → 2.6.3__tar.gz
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.
- {odooly-2.6.2 → odooly-2.6.3}/CHANGES.rst +6 -0
- {odooly-2.6.2 → odooly-2.6.3/Odooly.egg-info}/PKG-INFO +1 -1
- {odooly-2.6.2/Odooly.egg-info → odooly-2.6.3}/PKG-INFO +1 -1
- {odooly-2.6.2 → odooly-2.6.3}/odooly.py +32 -36
- odooly-2.6.3/odooly_run.py +164 -0
- odooly-2.6.2/odooly_run.py +0 -131
- {odooly-2.6.2 → odooly-2.6.3}/LICENSE +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/MANIFEST.in +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/SOURCES.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/dependency_links.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/entry_points.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/Odooly.egg-info/top_level.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/README.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/Makefile +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/api.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/conf.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/developer.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/index.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/intro.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/make.bat +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/docs/tutorial.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/odooly.ini +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/setup.cfg +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/setup.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/tests/__init__.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/tests/_common.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/tests/test_client.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/tests/test_interact.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/tests/test_model.py +0 -0
- {odooly-2.6.2 → odooly-2.6.3}/tests/test_util.py +0 -0
|
@@ -28,7 +28,7 @@ try:
|
|
|
28
28
|
except ImportError:
|
|
29
29
|
requests = None
|
|
30
30
|
|
|
31
|
-
__version__ = '2.6.
|
|
31
|
+
__version__ = '2.6.3'
|
|
32
32
|
__all__ = ['Client', 'Env', 'HTTPSession', 'WebAPI', 'Service', 'Json2',
|
|
33
33
|
'Printer', 'Error', 'ServerError',
|
|
34
34
|
'BaseModel', 'Model', 'BaseRecord', 'Record', 'RecordList',
|
|
@@ -132,7 +132,7 @@ for query in sql_queries:
|
|
|
132
132
|
log(str({'queries': sql_queries, 'result': result}))
|
|
133
133
|
result[:] = []
|
|
134
134
|
"""
|
|
135
|
-
|
|
135
|
+
color_bold = color_comment = color_py = color_repr = str
|
|
136
136
|
http_context = None
|
|
137
137
|
|
|
138
138
|
if os.getenv('ODOOLY_SSL_UNVERIFIED'):
|
|
@@ -225,12 +225,12 @@ def _memoize(inst, attr, value, doc_values=None):
|
|
|
225
225
|
|
|
226
226
|
# Simplified ast.literal_eval which does not parse operators
|
|
227
227
|
def _convert(node):
|
|
228
|
-
if isinstance(node, _ast.Constant):
|
|
228
|
+
if isinstance(node, _ast.Constant) and node.value.__class__ is not complex:
|
|
229
229
|
return node.value
|
|
230
230
|
if isinstance(node, _ast.Tuple):
|
|
231
231
|
return tuple(map(_convert, node.elts))
|
|
232
232
|
if isinstance(node, _ast.List):
|
|
233
|
-
return
|
|
233
|
+
return [*map(_convert, node.elts)]
|
|
234
234
|
if isinstance(node, _ast.Dict):
|
|
235
235
|
return {_convert(k): _convert(v)
|
|
236
236
|
for (k, v) in zip(node.keys, node.values)}
|
|
@@ -242,10 +242,8 @@ def _convert(node):
|
|
|
242
242
|
raise ValueError('malformed or disallowed expression')
|
|
243
243
|
|
|
244
244
|
|
|
245
|
-
def literal_eval(expression
|
|
245
|
+
def literal_eval(expression):
|
|
246
246
|
node = compile(expression, '<unknown>', 'eval', _ast.PyCF_ONLY_AST)
|
|
247
|
-
if expression[:1] == '0' and expression[1:2] in _octal_digits:
|
|
248
|
-
raise SyntaxError('unsupported octal notation')
|
|
249
247
|
return _convert(node.body)
|
|
250
248
|
|
|
251
249
|
|
|
@@ -419,7 +417,7 @@ class Printer:
|
|
|
419
417
|
xch = message[:self.cols - len(suffix)] + suffix
|
|
420
418
|
print(color_comment(f"{_prefix} {xch}"))
|
|
421
419
|
|
|
422
|
-
print_sent = functools.partialmethod(_print_, _prefix='
|
|
420
|
+
print_sent = functools.partialmethod(_print_, _prefix=' >')
|
|
423
421
|
print_recv = functools.partialmethod(_print_, _prefix='<--')
|
|
424
422
|
|
|
425
423
|
def __bool__(self):
|
|
@@ -452,7 +450,7 @@ class WebAPI:
|
|
|
452
450
|
self._dispatch = client._proxy_web(endpoint)
|
|
453
451
|
self._server = urljoin(client._server, '/')
|
|
454
452
|
self._endpoint = f'/{endpoint}'
|
|
455
|
-
self._methods = methods
|
|
453
|
+
self._methods = [*methods]
|
|
456
454
|
self._printer = client._printer
|
|
457
455
|
|
|
458
456
|
def __repr__(self):
|
|
@@ -802,7 +800,7 @@ class Env:
|
|
|
802
800
|
|
|
803
801
|
def refresh(self):
|
|
804
802
|
db_key, preserve = (self.db_name, self.client._server), ('auth', Env)
|
|
805
|
-
for key in
|
|
803
|
+
for key in [*self._cache]:
|
|
806
804
|
if key[1:] == db_key and key[0] not in preserve and self._cache[key] != self:
|
|
807
805
|
del self._cache[key]
|
|
808
806
|
self._access_models = None
|
|
@@ -884,7 +882,7 @@ class Env:
|
|
|
884
882
|
method, [ids] = 'search_read', searchargs(params[:1])
|
|
885
883
|
else:
|
|
886
884
|
order_ids = kwargs.pop('order', False) and params[0]
|
|
887
|
-
ids = sorted(
|
|
885
|
+
ids = sorted({*params[0]} - {False})
|
|
888
886
|
if not ids and order_ids:
|
|
889
887
|
return [False] * len(order_ids)
|
|
890
888
|
if not ids:
|
|
@@ -1091,7 +1089,7 @@ class Env:
|
|
|
1091
1089
|
if not self.db_name and (not self.uid or login == self.user.login) and self.session_info.get('db'):
|
|
1092
1090
|
empty_db_key = (self.db_name, self.client._server)
|
|
1093
1091
|
self.db_name = self.session_info['db']
|
|
1094
|
-
for key in
|
|
1092
|
+
for key in [*self._cache]:
|
|
1095
1093
|
if key[1:] == empty_db_key:
|
|
1096
1094
|
self._cache_set(key[0], self._cache[key])
|
|
1097
1095
|
print(f'Session authenticated for {login!r}' if self.session_info['uid'] else 'Failed')
|
|
@@ -1306,7 +1304,7 @@ class Client:
|
|
|
1306
1304
|
try:
|
|
1307
1305
|
cls._saved_config[environment] = read_config(environment)
|
|
1308
1306
|
except Exception:
|
|
1309
|
-
envs =
|
|
1307
|
+
envs = [*cls._saved_config]
|
|
1310
1308
|
if cls._config_file.exists():
|
|
1311
1309
|
envs += [env for env in read_config() if env not in cls._saved_config]
|
|
1312
1310
|
raise Error("No such environment.\nAvailable: " + ", ".join(envs))
|
|
@@ -1480,7 +1478,7 @@ class Client:
|
|
|
1480
1478
|
|
|
1481
1479
|
@classmethod
|
|
1482
1480
|
def _set_interactive(cls, global_vars={}):
|
|
1483
|
-
global
|
|
1481
|
+
global color_bold, color_comment, color_py, color_repr
|
|
1484
1482
|
# Don't call multiple times
|
|
1485
1483
|
del Client._set_interactive
|
|
1486
1484
|
assert not cls._is_interactive()
|
|
@@ -1488,11 +1486,11 @@ class Client:
|
|
|
1488
1486
|
global_vars[name] = globals()[name]
|
|
1489
1487
|
try: # Python >= 3.14
|
|
1490
1488
|
from _pyrepl.utils import disp_str, gen_colors, _colorize
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
color_bold =
|
|
1494
|
-
color_comment =
|
|
1495
|
-
global_vars |= {'
|
|
1489
|
+
color_py = color_repr = lambda v: "".join(disp_str(v, colors=[*gen_colors(v)])[0])
|
|
1490
|
+
color_py.__name__ = color_py.__qualname__ = 'color_python'
|
|
1491
|
+
color_bold = color_py('def _').split()[1].replace('_', '{}').format
|
|
1492
|
+
color_comment = color_py('#').replace('#', '{}').format
|
|
1493
|
+
global_vars |= {'color_py': color_py, 'decolor': _colorize.decolor}
|
|
1496
1494
|
except ImportError:
|
|
1497
1495
|
pass
|
|
1498
1496
|
cls._globals = global_vars
|
|
@@ -1512,7 +1510,6 @@ class Client:
|
|
|
1512
1510
|
and no country is set into the database.
|
|
1513
1511
|
Login if successful.
|
|
1514
1512
|
"""
|
|
1515
|
-
extra = (login, country_code) if login != ADMIN_USER or country_code else ()
|
|
1516
1513
|
self.database.create(master_pwd=passwd, name=database, lang=lang,
|
|
1517
1514
|
password=user_password, demo=demo, login=login,
|
|
1518
1515
|
country_code=country_code, phone='')
|
|
@@ -1524,10 +1521,9 @@ class Client:
|
|
|
1524
1521
|
The superadmin `passwd` and `database` are mandatory.
|
|
1525
1522
|
Login if successful.
|
|
1526
1523
|
"""
|
|
1527
|
-
extra =
|
|
1524
|
+
extra = {"neutralize_database": neutralize_database} if neutralize_database else {}
|
|
1528
1525
|
if extra and self.version_info < 16.0:
|
|
1529
1526
|
raise Error("Argument 'neutralize_database' is not supported")
|
|
1530
|
-
extra = {"neutralize_database": extra[0]} if extra else {}
|
|
1531
1527
|
self.database.duplicate(master_pwd=passwd, name=self.env.db_name,
|
|
1532
1528
|
new_name=database, **extra)
|
|
1533
1529
|
# Copy the cache for authentication
|
|
@@ -1634,7 +1630,7 @@ class Model(BaseModel):
|
|
|
1634
1630
|
methods = doc_dict.get('methods') or {}
|
|
1635
1631
|
result = []
|
|
1636
1632
|
for key, vals in methods.items():
|
|
1637
|
-
arg_names =
|
|
1633
|
+
arg_names = [*vals['parameters']]
|
|
1638
1634
|
if 'model' not in vals.get('api', ()):
|
|
1639
1635
|
arg_names.insert(0, 'ids')
|
|
1640
1636
|
result.append((key, arg_names))
|
|
@@ -1913,14 +1909,14 @@ class BaseRecord(BaseModel):
|
|
|
1913
1909
|
|
|
1914
1910
|
def __sub__(self, other):
|
|
1915
1911
|
self._check_model(other, '-')
|
|
1916
|
-
other_ids =
|
|
1912
|
+
other_ids = {*other.ids}
|
|
1917
1913
|
ids = [idn for (id_, idn) in zip(self.ids, self._idnames)
|
|
1918
1914
|
if id_ not in other_ids]
|
|
1919
1915
|
return RecordList(self._model, ids)
|
|
1920
1916
|
|
|
1921
1917
|
def __and__(self, other):
|
|
1922
1918
|
self._check_model(other, '&')
|
|
1923
|
-
other_ids =
|
|
1919
|
+
other_ids = {*other.ids}
|
|
1924
1920
|
self_set = self.union()
|
|
1925
1921
|
ids = [idn for (id_, idn) in zip(self_set.ids, self_set._idnames)
|
|
1926
1922
|
if id_ in other_ids]
|
|
@@ -1938,19 +1934,19 @@ class BaseRecord(BaseModel):
|
|
|
1938
1934
|
|
|
1939
1935
|
def __lt__(self, other):
|
|
1940
1936
|
self._check_model(other, '<')
|
|
1941
|
-
return
|
|
1937
|
+
return {*self.ids} < {*other.ids}
|
|
1942
1938
|
|
|
1943
1939
|
def __le__(self, other):
|
|
1944
1940
|
self._check_model(other, '<=')
|
|
1945
|
-
return
|
|
1941
|
+
return {*self.ids}.issubset(other.ids)
|
|
1946
1942
|
|
|
1947
1943
|
def __gt__(self, other):
|
|
1948
1944
|
self._check_model(other, '>')
|
|
1949
|
-
return
|
|
1945
|
+
return {*self.ids} > {*other.ids}
|
|
1950
1946
|
|
|
1951
1947
|
def __ge__(self, other):
|
|
1952
1948
|
self._check_model(other, '>=')
|
|
1953
|
-
return
|
|
1949
|
+
return {*self.ids}.issuperset(other.ids)
|
|
1954
1950
|
|
|
1955
1951
|
def __int__(self):
|
|
1956
1952
|
return int(self.ensure_one().id)
|
|
@@ -2008,7 +2004,7 @@ class BaseRecord(BaseModel):
|
|
|
2008
2004
|
raise TypeError(f"Mixing apples and oranges: {self} {oper} {other}")
|
|
2009
2005
|
|
|
2010
2006
|
def _concat_ids(self, args):
|
|
2011
|
-
ids =
|
|
2007
|
+
ids = [*self._idnames]
|
|
2012
2008
|
for other in args:
|
|
2013
2009
|
self._check_model(other, '+')
|
|
2014
2010
|
ids.extend(other._idnames)
|
|
@@ -2026,7 +2022,7 @@ class BaseRecord(BaseModel):
|
|
|
2026
2022
|
"""
|
|
2027
2023
|
ids = self._concat_ids(args)
|
|
2028
2024
|
if len(ids) > 1:
|
|
2029
|
-
seen =
|
|
2025
|
+
seen = {*()}
|
|
2030
2026
|
uniqids = []
|
|
2031
2027
|
for idn in ids:
|
|
2032
2028
|
id_, name = idn if isinstance(idn, (list, tuple)) else (idn, None)
|
|
@@ -2269,7 +2265,7 @@ class Record(BaseRecord):
|
|
|
2269
2265
|
else:
|
|
2270
2266
|
idnames = [(arg, name)] = [arg]
|
|
2271
2267
|
Ids, Id1 = self.env._class_ids
|
|
2272
|
-
attrs = {'id': Id1(arg), 'ids': Ids([arg]), '_idnames': idnames, '_cached_keys':
|
|
2268
|
+
attrs = {'id': Id1(arg), 'ids': Ids([arg]), '_idnames': idnames, '_cached_keys': {*()}}
|
|
2273
2269
|
if name is not None:
|
|
2274
2270
|
attrs['_Record__name'] = attrs['display_name'] = name
|
|
2275
2271
|
self.__dict__.update(attrs)
|
|
@@ -2335,7 +2331,7 @@ class Record(BaseRecord):
|
|
|
2335
2331
|
exist, only one of them is returned (randomly).
|
|
2336
2332
|
"""
|
|
2337
2333
|
xml_ids = self._model._get_external_ids(self.ids)
|
|
2338
|
-
return
|
|
2334
|
+
return [*xml_ids][0] if xml_ids else False
|
|
2339
2335
|
|
|
2340
2336
|
def _set_external_id(self, xml_id):
|
|
2341
2337
|
"""Set the External ID of this record."""
|
|
@@ -2407,7 +2403,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
|
|
|
2407
2403
|
pass # IOError if file missing, or other error
|
|
2408
2404
|
|
|
2409
2405
|
if use_pprint:
|
|
2410
|
-
pp = lambda obj: print(
|
|
2406
|
+
pp = lambda obj: print(color_repr(pprint.pformat(obj, **PP_FORMAT)))
|
|
2411
2407
|
|
|
2412
2408
|
def displayhook(value, _printer=pp, _builtins=builtins):
|
|
2413
2409
|
# Pretty-format the output
|
|
@@ -2419,7 +2415,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
|
|
|
2419
2415
|
def excepthook(exc_type, exc, tb):
|
|
2420
2416
|
# Print readable errors
|
|
2421
2417
|
msg = ''.join(format_exception(exc_type, exc, tb, chain=False, **exfmt))
|
|
2422
|
-
print(
|
|
2418
|
+
print(color_repr(msg.rstrip()))
|
|
2423
2419
|
sys.excepthook = excepthook
|
|
2424
2420
|
|
|
2425
2421
|
builtins.usage = type('Usage', (), {'__call__': lambda s: print(usage),
|
|
@@ -2484,7 +2480,7 @@ def main(interact=_interact):
|
|
|
2484
2480
|
return
|
|
2485
2481
|
|
|
2486
2482
|
global_vars = Client._set_interactive()
|
|
2487
|
-
print(
|
|
2483
|
+
print(color_repr(USAGE))
|
|
2488
2484
|
|
|
2489
2485
|
connect_client(args)
|
|
2490
2486
|
return interact(global_vars) if interact else global_vars
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""Connect to an Odoo server."""
|
|
3
|
+
# Colorize command-line output
|
|
4
|
+
import builtins
|
|
5
|
+
import keyword
|
|
6
|
+
import io
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import token as T
|
|
10
|
+
import tokenize
|
|
11
|
+
import unicodedata
|
|
12
|
+
|
|
13
|
+
import odooly
|
|
14
|
+
|
|
15
|
+
BUILTINS = {name for name in dir(builtins) if name[:1] != '_'}
|
|
16
|
+
T_STRING = {getattr(T, tk) for tk in dir(T) if 'STRING' in tk}
|
|
17
|
+
T_MIDDLE = {getattr(T, tk) for tk in dir(T) if 'STRING_MIDDLE' in tk}
|
|
18
|
+
KEYWORDS = {*keyword.kwlist}
|
|
19
|
+
SOFTKEYW = {*getattr(keyword, 'softkwlist', ())}
|
|
20
|
+
J_SPECIAL = {'false', 'null', 'true'}
|
|
21
|
+
CONSTANTS = {'False', 'None', 'True'}
|
|
22
|
+
DEF_CLASS = {'def', 'class'}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class THEME: # Default theme
|
|
26
|
+
keyword = '\x1b[1;34m'
|
|
27
|
+
builtin = '\x1b[36m'
|
|
28
|
+
comment = '\x1b[31m'
|
|
29
|
+
string = '\x1b[32m'
|
|
30
|
+
number = '\x1b[33m'
|
|
31
|
+
op = '\x1b[0m' # No color
|
|
32
|
+
definition = '\x1b[1m'
|
|
33
|
+
reset = '\x1b[0m'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def gen_colors(value, theme, keywords=KEYWORDS):
|
|
37
|
+
"""Generate color tuples (start, end, color)."""
|
|
38
|
+
sio = io.StringIO(value)
|
|
39
|
+
line_offsets = [..., 0] + [sio.tell() for __ in sio]
|
|
40
|
+
sio.seek(0)
|
|
41
|
+
|
|
42
|
+
bracket_level = is_def_name = 0
|
|
43
|
+
yield None, 0, theme.reset
|
|
44
|
+
try:
|
|
45
|
+
for token in tokenize.generate_tokens(sio.readline):
|
|
46
|
+
if token.start == token.end:
|
|
47
|
+
continue
|
|
48
|
+
color, end_offset = None, 0
|
|
49
|
+
if token.type in T_STRING:
|
|
50
|
+
if token.type in T_MIDDLE and token.string[-1:] in '{}':
|
|
51
|
+
end_offset += 1
|
|
52
|
+
color = theme.string
|
|
53
|
+
elif token.type == T.COMMENT:
|
|
54
|
+
color = theme.comment
|
|
55
|
+
elif token.type == T.NUMBER:
|
|
56
|
+
color = theme.number
|
|
57
|
+
elif token.type == T.OP:
|
|
58
|
+
if token.string in '([{':
|
|
59
|
+
bracket_level += 1
|
|
60
|
+
elif token.string in ')]}':
|
|
61
|
+
bracket_level -= 1
|
|
62
|
+
color = theme.op
|
|
63
|
+
elif token.type == T.NAME:
|
|
64
|
+
if token.string in keywords:
|
|
65
|
+
is_def_name = token.string in DEF_CLASS
|
|
66
|
+
color = theme.keyword
|
|
67
|
+
elif keywords is not KEYWORDS:
|
|
68
|
+
continue
|
|
69
|
+
elif is_def_name:
|
|
70
|
+
is_def_name = False
|
|
71
|
+
color = theme.definition
|
|
72
|
+
elif token.string in SOFTKEYW and not bracket_level:
|
|
73
|
+
color = theme.keyword # soft_keyword
|
|
74
|
+
elif token.string in BUILTINS:
|
|
75
|
+
color = theme.builtin
|
|
76
|
+
if color is not None:
|
|
77
|
+
start = line_offsets[token.start[0]] + token.start[1]
|
|
78
|
+
end = line_offsets[token.end[0]] + token.end[1] + end_offset
|
|
79
|
+
yield start, end, color
|
|
80
|
+
except (SyntaxError, tokenize.TokenError):
|
|
81
|
+
pass
|
|
82
|
+
yield None, None, theme.reset
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _apply_colors(value, theme, keywords):
|
|
86
|
+
output = ''
|
|
87
|
+
if value:
|
|
88
|
+
__, end, c_reset = next(colors := gen_colors(value, theme=theme, keywords=keywords))
|
|
89
|
+
|
|
90
|
+
while (last_pos := end) is not None:
|
|
91
|
+
start, end, color = next(colors)
|
|
92
|
+
if start is None:
|
|
93
|
+
output += _escape(value[last_pos:])
|
|
94
|
+
else:
|
|
95
|
+
output += _escape(value[last_pos:start]) + color + _escape(value[start:end]) + c_reset
|
|
96
|
+
return output
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _escape(value):
|
|
100
|
+
if value.isascii():
|
|
101
|
+
return value
|
|
102
|
+
result = ''
|
|
103
|
+
for char in value:
|
|
104
|
+
if char > '\x7f' and unicodedata.category(char)[:1] == 'C':
|
|
105
|
+
char = rf'\u{ord(char):04x}'
|
|
106
|
+
result += char
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def color_python(value):
|
|
111
|
+
"""Colorize Python syntax."""
|
|
112
|
+
return _apply_colors(value, THEME(), KEYWORDS)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def color_repr(value):
|
|
116
|
+
"""Colorize string representation."""
|
|
117
|
+
return _apply_colors(value, THEME(), CONSTANTS)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def color_json(value):
|
|
121
|
+
"""Colorize JSON representation."""
|
|
122
|
+
return _apply_colors(value, THEME(), J_SPECIAL)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def patch_colors(module):
|
|
126
|
+
"""Set functions to color output."""
|
|
127
|
+
global THEME
|
|
128
|
+
try: # Python >= 3.14
|
|
129
|
+
from _pyrepl.utils import BUILTINS, THEME
|
|
130
|
+
BUILTINS |= {'Client', 'client', 'env', 'self'}
|
|
131
|
+
except ImportError:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
module.color_repr = color_repr
|
|
135
|
+
if module.color_py is not str: # Python >= 3.14
|
|
136
|
+
return {'color_repr': color_repr}
|
|
137
|
+
theme = THEME()
|
|
138
|
+
module.color_py = color_python
|
|
139
|
+
module.color_bold = f'{theme.definition}{{}}{theme.reset}'.format
|
|
140
|
+
module.color_comment = f'{theme.comment}{{}}{theme.reset}'.format
|
|
141
|
+
decolor = odooly.partial(re.compile(r'\x1b\[[;\d]+m').sub, '')
|
|
142
|
+
return {'color_py': color_python, 'color_repr': color_repr, 'decolor': decolor}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
args = odooly.get_parser().parse_args()
|
|
147
|
+
|
|
148
|
+
if args.config:
|
|
149
|
+
odooly.Client._config_file = odooly.Path.cwd() / args.config
|
|
150
|
+
if args.list_env:
|
|
151
|
+
print('Available settings: ' + ' '.join(odooly.read_config()))
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
global_vars = odooly.Client._set_interactive()
|
|
155
|
+
if odooly.color_py is not str or (os.getenv('FORCE_COLOR') and not os.getenv('NO_COLOR')):
|
|
156
|
+
global_vars.update(patch_colors(odooly))
|
|
157
|
+
|
|
158
|
+
print(odooly.color_repr(odooly.USAGE))
|
|
159
|
+
odooly.connect_client(args)
|
|
160
|
+
odooly._interact(global_vars)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == '__main__':
|
|
164
|
+
main()
|
odooly-2.6.2/odooly_run.py
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
"""Connect to an Odoo server."""
|
|
3
|
-
# Colorize command-line output for Python <= 3.13
|
|
4
|
-
import builtins
|
|
5
|
-
import keyword
|
|
6
|
-
import io
|
|
7
|
-
import os
|
|
8
|
-
import pathlib
|
|
9
|
-
import re
|
|
10
|
-
import token as T
|
|
11
|
-
import tokenize
|
|
12
|
-
import unicodedata
|
|
13
|
-
|
|
14
|
-
import odooly
|
|
15
|
-
|
|
16
|
-
BUILTINS = {str(name) for name in dir(builtins) if name[:1] != '_'}
|
|
17
|
-
T_STRING = {getattr(T, tk) for tk in dir(T) if 'STRING' in tk}
|
|
18
|
-
T_MIDDLE = {getattr(T, tk) for tk in dir(T) if 'STRING_MIDDLE' in tk}
|
|
19
|
-
|
|
20
|
-
issoftkeyword = getattr(keyword, 'issoftkeyword', lambda t: False)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class THEME: # Default theme
|
|
24
|
-
keyword = '\x1b[1;34m'
|
|
25
|
-
builtin = '\x1b[36m'
|
|
26
|
-
comment = '\x1b[31m'
|
|
27
|
-
string = '\x1b[32m'
|
|
28
|
-
number = '\x1b[33m'
|
|
29
|
-
op = '\x1b[0m' # No color
|
|
30
|
-
definition = '\x1b[1m'
|
|
31
|
-
reset = '\x1b[0m'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def gen_colors(value, theme=THEME()):
|
|
35
|
-
"""Generate color tuples (start, end, color, reset_color)."""
|
|
36
|
-
sio = io.StringIO(value)
|
|
37
|
-
line_lengths = [0] + [len(line) for line in sio.readlines()]
|
|
38
|
-
for i in range(1, len(line_lengths)):
|
|
39
|
-
line_lengths[i] += line_lengths[i-1]
|
|
40
|
-
|
|
41
|
-
sio.seek(0)
|
|
42
|
-
bracket_level = is_def_name = 0
|
|
43
|
-
try:
|
|
44
|
-
for token in tokenize.generate_tokens(sio.readline):
|
|
45
|
-
if token.start == token.end:
|
|
46
|
-
continue
|
|
47
|
-
color, end_offset = None, -1
|
|
48
|
-
if token.type in T_STRING:
|
|
49
|
-
if token.type in T_MIDDLE and token.string.endswith(('{', '}')):
|
|
50
|
-
end_offset += 1
|
|
51
|
-
color = theme.string
|
|
52
|
-
elif token.type == T.COMMENT:
|
|
53
|
-
color = theme.comment
|
|
54
|
-
elif token.type == T.NUMBER:
|
|
55
|
-
color = theme.number
|
|
56
|
-
elif token.type == T.OP:
|
|
57
|
-
if token.string in '([{':
|
|
58
|
-
bracket_level += 1
|
|
59
|
-
elif token.string in ')]}':
|
|
60
|
-
bracket_level -= 1
|
|
61
|
-
color = theme.op
|
|
62
|
-
elif token.type == T.NAME:
|
|
63
|
-
if is_def_name:
|
|
64
|
-
is_def_name = False
|
|
65
|
-
color = theme.definition
|
|
66
|
-
elif keyword.iskeyword(token.string):
|
|
67
|
-
if token.string in ('def', 'class'):
|
|
68
|
-
is_def_name = True
|
|
69
|
-
color = theme.keyword
|
|
70
|
-
elif issoftkeyword(token.string) and not bracket_level:
|
|
71
|
-
color = theme.keyword # soft_keyword
|
|
72
|
-
elif token.string in BUILTINS:
|
|
73
|
-
color = theme.builtin
|
|
74
|
-
if color is not None:
|
|
75
|
-
start = line_lengths[token.start[0] - 1] + token.start[1]
|
|
76
|
-
end = line_lengths[token.end[0] - 1] + token.end[1] + end_offset
|
|
77
|
-
yield start, end, color, theme.reset
|
|
78
|
-
except (SyntaxError, tokenize.TokenError):
|
|
79
|
-
pass
|
|
80
|
-
yield None, None, None, None
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def colorize(value):
|
|
84
|
-
"""Colorize Python syntax."""
|
|
85
|
-
output = ''
|
|
86
|
-
if value:
|
|
87
|
-
start, end, color, c_reset = next(colors := gen_colors(value))
|
|
88
|
-
|
|
89
|
-
for idx, char in enumerate(value):
|
|
90
|
-
if start == idx:
|
|
91
|
-
output += color
|
|
92
|
-
|
|
93
|
-
if char > '\x7f' and unicodedata.category(char)[:1] == 'C':
|
|
94
|
-
char = rf'\u{ord(char):04x}'
|
|
95
|
-
output += char
|
|
96
|
-
|
|
97
|
-
if end == idx:
|
|
98
|
-
output += c_reset
|
|
99
|
-
start, end, color, c_reset = next(colors)
|
|
100
|
-
return output
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def patch_colors(module):
|
|
104
|
-
"""Set functions to color output."""
|
|
105
|
-
module.colorize = colorize
|
|
106
|
-
module.color_bold = f'{THEME.definition}{{}}{THEME.reset}'.format
|
|
107
|
-
module.color_comment = f'{THEME.comment}{{}}{THEME.reset}'.format
|
|
108
|
-
decolor = odooly.partial(re.compile(r'\x1b\[[;\d]+m').sub, '')
|
|
109
|
-
return {'colorize': colorize, 'decolor': decolor}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def main():
|
|
113
|
-
args = odooly.get_parser().parse_args()
|
|
114
|
-
|
|
115
|
-
if args.config:
|
|
116
|
-
odooly.Client._config_file = odooly.Path.cwd() / args.config
|
|
117
|
-
if args.list_env:
|
|
118
|
-
print('Available settings: ' + ' '.join(odooly.read_config()))
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
global_vars = odooly.Client._set_interactive()
|
|
122
|
-
if os.getenv('FORCE_COLOR') and not os.getenv('NO_COLOR') and odooly.colorize is str:
|
|
123
|
-
global_vars.update(patch_colors(odooly)) # Python <= 3.13
|
|
124
|
-
|
|
125
|
-
print(odooly.colorize(odooly.USAGE))
|
|
126
|
-
odooly.connect_client(args)
|
|
127
|
-
odooly._interact(global_vars)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if __name__ == '__main__':
|
|
131
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|