Odooly 2.6.2__tar.gz → 2.6.4__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.4}/CHANGES.rst +18 -0
- {odooly-2.6.2 → odooly-2.6.4/Odooly.egg-info}/PKG-INFO +4 -7
- {odooly-2.6.2/Odooly.egg-info → odooly-2.6.4}/PKG-INFO +4 -7
- {odooly-2.6.2 → odooly-2.6.4}/README.rst +3 -6
- {odooly-2.6.2 → odooly-2.6.4}/odooly.py +35 -37
- odooly-2.6.4/odooly_run.py +165 -0
- odooly-2.6.2/odooly_run.py +0 -131
- {odooly-2.6.2 → odooly-2.6.4}/LICENSE +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/MANIFEST.in +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/Odooly.egg-info/SOURCES.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/Odooly.egg-info/dependency_links.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/Odooly.egg-info/entry_points.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/Odooly.egg-info/top_level.txt +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/Makefile +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/api.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/conf.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/developer.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/index.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/intro.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/make.bat +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/docs/tutorial.rst +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/odooly.ini +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/setup.cfg +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/setup.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/tests/__init__.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/tests/_common.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/tests/test_client.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/tests/test_interact.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/tests/test_model.py +0 -0
- {odooly-2.6.2 → odooly-2.6.4}/tests/test_util.py +0 -0
|
@@ -2,6 +2,24 @@ Changelog
|
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
2.6.4 (2026-03-26)
|
|
6
|
+
~~~~~~~~~~~~~~~~~~
|
|
7
|
+
|
|
8
|
+
* Remove ``self`` from vars available in interactive mode.
|
|
9
|
+
It was giving wrong suggestions in case of typing error.
|
|
10
|
+
|
|
11
|
+
* Inject ``clear`` command in built-ins for Python <= 3.12.
|
|
12
|
+
|
|
13
|
+
* Adjust ``call_kw`` path to ``/web/dataset/call_kw/<model>/<method``
|
|
14
|
+
for adequate HTTP logging, and to be consistent with Odoo Web.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
2.6.3 (2026-03-18)
|
|
18
|
+
~~~~~~~~~~~~~~~~~~
|
|
19
|
+
|
|
20
|
+
* Improve output coloring. Do not color Python keywords and built-ins.
|
|
21
|
+
|
|
22
|
+
|
|
5
23
|
2.6.2 (2026-03-17)
|
|
6
24
|
~~~~~~~~~~~~~~~~~~
|
|
7
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Odooly
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.4
|
|
4
4
|
Summary: Versatile tool for Odoo
|
|
5
5
|
Home-page: http://odooly.readthedocs.org/
|
|
6
6
|
Author: Florent Xicluna
|
|
@@ -46,11 +46,8 @@ Overview
|
|
|
46
46
|
--------
|
|
47
47
|
|
|
48
48
|
Odooly connects to an Odoo instance through HTTP. It makes it easy to browse the application model,
|
|
49
|
-
and to perform actions. It
|
|
50
|
-
|
|
51
|
-
(1) with command line arguments
|
|
52
|
-
(2) as an interactive shell
|
|
53
|
-
(3) as a client library
|
|
49
|
+
and to perform actions on the server. It is both an interactive shell (with pretty-printing and colors)
|
|
50
|
+
and a client library.
|
|
54
51
|
|
|
55
52
|
|
|
56
53
|
Key features:
|
|
@@ -58,7 +55,7 @@ Key features:
|
|
|
58
55
|
- provides an API similar to Odoo Model, through Webclient API
|
|
59
56
|
- supports JSON-2 API with Odoo 19 and more recent
|
|
60
57
|
- supports external API JSON-RPC as alternative
|
|
61
|
-
- compatible with Odoo 9 to
|
|
58
|
+
- compatible with Odoo 9 to 20
|
|
62
59
|
- single file ``odooly.py``, no external dependency
|
|
63
60
|
- helpers for ``search``, for data model introspection, etc...
|
|
64
61
|
- simplified syntax for search ``domain``
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Odooly
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.4
|
|
4
4
|
Summary: Versatile tool for Odoo
|
|
5
5
|
Home-page: http://odooly.readthedocs.org/
|
|
6
6
|
Author: Florent Xicluna
|
|
@@ -46,11 +46,8 @@ Overview
|
|
|
46
46
|
--------
|
|
47
47
|
|
|
48
48
|
Odooly connects to an Odoo instance through HTTP. It makes it easy to browse the application model,
|
|
49
|
-
and to perform actions. It
|
|
50
|
-
|
|
51
|
-
(1) with command line arguments
|
|
52
|
-
(2) as an interactive shell
|
|
53
|
-
(3) as a client library
|
|
49
|
+
and to perform actions on the server. It is both an interactive shell (with pretty-printing and colors)
|
|
50
|
+
and a client library.
|
|
54
51
|
|
|
55
52
|
|
|
56
53
|
Key features:
|
|
@@ -58,7 +55,7 @@ Key features:
|
|
|
58
55
|
- provides an API similar to Odoo Model, through Webclient API
|
|
59
56
|
- supports JSON-2 API with Odoo 19 and more recent
|
|
60
57
|
- supports external API JSON-RPC as alternative
|
|
61
|
-
- compatible with Odoo 9 to
|
|
58
|
+
- compatible with Odoo 9 to 20
|
|
62
59
|
- single file ``odooly.py``, no external dependency
|
|
63
60
|
- helpers for ``search``, for data model introspection, etc...
|
|
64
61
|
- simplified syntax for search ``domain``
|
|
@@ -17,11 +17,8 @@ Overview
|
|
|
17
17
|
--------
|
|
18
18
|
|
|
19
19
|
Odooly connects to an Odoo instance through HTTP. It makes it easy to browse the application model,
|
|
20
|
-
and to perform actions. It
|
|
21
|
-
|
|
22
|
-
(1) with command line arguments
|
|
23
|
-
(2) as an interactive shell
|
|
24
|
-
(3) as a client library
|
|
20
|
+
and to perform actions on the server. It is both an interactive shell (with pretty-printing and colors)
|
|
21
|
+
and a client library.
|
|
25
22
|
|
|
26
23
|
|
|
27
24
|
Key features:
|
|
@@ -29,7 +26,7 @@ Key features:
|
|
|
29
26
|
- provides an API similar to Odoo Model, through Webclient API
|
|
30
27
|
- supports JSON-2 API with Odoo 19 and more recent
|
|
31
28
|
- supports external API JSON-RPC as alternative
|
|
32
|
-
- compatible with Odoo 9 to
|
|
29
|
+
- compatible with Odoo 9 to 20
|
|
33
30
|
- single file ``odooly.py``, no external dependency
|
|
34
31
|
- helpers for ``search``, for data model introspection, etc...
|
|
35
32
|
- simplified syntax for search ``domain``
|
|
@@ -28,7 +28,7 @@ try:
|
|
|
28
28
|
except ImportError:
|
|
29
29
|
requests = None
|
|
30
30
|
|
|
31
|
-
__version__ = '2.6.
|
|
31
|
+
__version__ = '2.6.4'
|
|
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')
|
|
@@ -1278,6 +1276,8 @@ class Client:
|
|
|
1278
1276
|
return self._post_jsonrpc(f"{name}/{method}", params=params)
|
|
1279
1277
|
else:
|
|
1280
1278
|
def dispatch_web(method, params):
|
|
1279
|
+
if method == 'call_kw' and name == 'web/dataset':
|
|
1280
|
+
method = f"{method}/{params['model']}/{params['method']}"
|
|
1281
1281
|
return self._post_jsonrpc(f"{name}/{method}", params=params)
|
|
1282
1282
|
return dispatch_web
|
|
1283
1283
|
|
|
@@ -1306,7 +1306,7 @@ class Client:
|
|
|
1306
1306
|
try:
|
|
1307
1307
|
cls._saved_config[environment] = read_config(environment)
|
|
1308
1308
|
except Exception:
|
|
1309
|
-
envs =
|
|
1309
|
+
envs = [*cls._saved_config]
|
|
1310
1310
|
if cls._config_file.exists():
|
|
1311
1311
|
envs += [env for env in read_config() if env not in cls._saved_config]
|
|
1312
1312
|
raise Error("No such environment.\nAvailable: " + ", ".join(envs))
|
|
@@ -1467,7 +1467,6 @@ class Client:
|
|
|
1467
1467
|
assert not user, f"Use client.login({user!r}) instead"
|
|
1468
1468
|
self._globals['client'] = self.env.client
|
|
1469
1469
|
self._globals['env'] = env = self.env
|
|
1470
|
-
self._globals['self'] = env.user if env.uid else None
|
|
1471
1470
|
self._set_prompt()
|
|
1472
1471
|
# Logged in?
|
|
1473
1472
|
if env.uid:
|
|
@@ -1480,7 +1479,7 @@ class Client:
|
|
|
1480
1479
|
|
|
1481
1480
|
@classmethod
|
|
1482
1481
|
def _set_interactive(cls, global_vars={}):
|
|
1483
|
-
global
|
|
1482
|
+
global color_bold, color_comment, color_py, color_repr
|
|
1484
1483
|
# Don't call multiple times
|
|
1485
1484
|
del Client._set_interactive
|
|
1486
1485
|
assert not cls._is_interactive()
|
|
@@ -1488,11 +1487,11 @@ class Client:
|
|
|
1488
1487
|
global_vars[name] = globals()[name]
|
|
1489
1488
|
try: # Python >= 3.14
|
|
1490
1489
|
from _pyrepl.utils import disp_str, gen_colors, _colorize
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
color_bold =
|
|
1494
|
-
color_comment =
|
|
1495
|
-
global_vars |= {'
|
|
1490
|
+
color_py = color_repr = lambda v: "".join(disp_str(v, colors=[*gen_colors(v)])[0])
|
|
1491
|
+
color_py.__name__ = color_py.__qualname__ = 'color_python'
|
|
1492
|
+
color_bold = color_py('def _').split()[1].replace('_', '{}').format
|
|
1493
|
+
color_comment = color_py('#').replace('#', '{}').format
|
|
1494
|
+
global_vars |= {'color_py': color_py, 'decolor': _colorize.decolor}
|
|
1496
1495
|
except ImportError:
|
|
1497
1496
|
pass
|
|
1498
1497
|
cls._globals = global_vars
|
|
@@ -1512,7 +1511,6 @@ class Client:
|
|
|
1512
1511
|
and no country is set into the database.
|
|
1513
1512
|
Login if successful.
|
|
1514
1513
|
"""
|
|
1515
|
-
extra = (login, country_code) if login != ADMIN_USER or country_code else ()
|
|
1516
1514
|
self.database.create(master_pwd=passwd, name=database, lang=lang,
|
|
1517
1515
|
password=user_password, demo=demo, login=login,
|
|
1518
1516
|
country_code=country_code, phone='')
|
|
@@ -1524,10 +1522,9 @@ class Client:
|
|
|
1524
1522
|
The superadmin `passwd` and `database` are mandatory.
|
|
1525
1523
|
Login if successful.
|
|
1526
1524
|
"""
|
|
1527
|
-
extra =
|
|
1525
|
+
extra = {"neutralize_database": neutralize_database} if neutralize_database else {}
|
|
1528
1526
|
if extra and self.version_info < 16.0:
|
|
1529
1527
|
raise Error("Argument 'neutralize_database' is not supported")
|
|
1530
|
-
extra = {"neutralize_database": extra[0]} if extra else {}
|
|
1531
1528
|
self.database.duplicate(master_pwd=passwd, name=self.env.db_name,
|
|
1532
1529
|
new_name=database, **extra)
|
|
1533
1530
|
# Copy the cache for authentication
|
|
@@ -1634,7 +1631,7 @@ class Model(BaseModel):
|
|
|
1634
1631
|
methods = doc_dict.get('methods') or {}
|
|
1635
1632
|
result = []
|
|
1636
1633
|
for key, vals in methods.items():
|
|
1637
|
-
arg_names =
|
|
1634
|
+
arg_names = [*vals['parameters']]
|
|
1638
1635
|
if 'model' not in vals.get('api', ()):
|
|
1639
1636
|
arg_names.insert(0, 'ids')
|
|
1640
1637
|
result.append((key, arg_names))
|
|
@@ -1913,14 +1910,14 @@ class BaseRecord(BaseModel):
|
|
|
1913
1910
|
|
|
1914
1911
|
def __sub__(self, other):
|
|
1915
1912
|
self._check_model(other, '-')
|
|
1916
|
-
other_ids =
|
|
1913
|
+
other_ids = {*other.ids}
|
|
1917
1914
|
ids = [idn for (id_, idn) in zip(self.ids, self._idnames)
|
|
1918
1915
|
if id_ not in other_ids]
|
|
1919
1916
|
return RecordList(self._model, ids)
|
|
1920
1917
|
|
|
1921
1918
|
def __and__(self, other):
|
|
1922
1919
|
self._check_model(other, '&')
|
|
1923
|
-
other_ids =
|
|
1920
|
+
other_ids = {*other.ids}
|
|
1924
1921
|
self_set = self.union()
|
|
1925
1922
|
ids = [idn for (id_, idn) in zip(self_set.ids, self_set._idnames)
|
|
1926
1923
|
if id_ in other_ids]
|
|
@@ -1938,19 +1935,19 @@ class BaseRecord(BaseModel):
|
|
|
1938
1935
|
|
|
1939
1936
|
def __lt__(self, other):
|
|
1940
1937
|
self._check_model(other, '<')
|
|
1941
|
-
return
|
|
1938
|
+
return {*self.ids} < {*other.ids}
|
|
1942
1939
|
|
|
1943
1940
|
def __le__(self, other):
|
|
1944
1941
|
self._check_model(other, '<=')
|
|
1945
|
-
return
|
|
1942
|
+
return {*self.ids}.issubset(other.ids)
|
|
1946
1943
|
|
|
1947
1944
|
def __gt__(self, other):
|
|
1948
1945
|
self._check_model(other, '>')
|
|
1949
|
-
return
|
|
1946
|
+
return {*self.ids} > {*other.ids}
|
|
1950
1947
|
|
|
1951
1948
|
def __ge__(self, other):
|
|
1952
1949
|
self._check_model(other, '>=')
|
|
1953
|
-
return
|
|
1950
|
+
return {*self.ids}.issuperset(other.ids)
|
|
1954
1951
|
|
|
1955
1952
|
def __int__(self):
|
|
1956
1953
|
return int(self.ensure_one().id)
|
|
@@ -2008,7 +2005,7 @@ class BaseRecord(BaseModel):
|
|
|
2008
2005
|
raise TypeError(f"Mixing apples and oranges: {self} {oper} {other}")
|
|
2009
2006
|
|
|
2010
2007
|
def _concat_ids(self, args):
|
|
2011
|
-
ids =
|
|
2008
|
+
ids = [*self._idnames]
|
|
2012
2009
|
for other in args:
|
|
2013
2010
|
self._check_model(other, '+')
|
|
2014
2011
|
ids.extend(other._idnames)
|
|
@@ -2026,7 +2023,7 @@ class BaseRecord(BaseModel):
|
|
|
2026
2023
|
"""
|
|
2027
2024
|
ids = self._concat_ids(args)
|
|
2028
2025
|
if len(ids) > 1:
|
|
2029
|
-
seen =
|
|
2026
|
+
seen = {*()}
|
|
2030
2027
|
uniqids = []
|
|
2031
2028
|
for idn in ids:
|
|
2032
2029
|
id_, name = idn if isinstance(idn, (list, tuple)) else (idn, None)
|
|
@@ -2269,7 +2266,7 @@ class Record(BaseRecord):
|
|
|
2269
2266
|
else:
|
|
2270
2267
|
idnames = [(arg, name)] = [arg]
|
|
2271
2268
|
Ids, Id1 = self.env._class_ids
|
|
2272
|
-
attrs = {'id': Id1(arg), 'ids': Ids([arg]), '_idnames': idnames, '_cached_keys':
|
|
2269
|
+
attrs = {'id': Id1(arg), 'ids': Ids([arg]), '_idnames': idnames, '_cached_keys': {*()}}
|
|
2273
2270
|
if name is not None:
|
|
2274
2271
|
attrs['_Record__name'] = attrs['display_name'] = name
|
|
2275
2272
|
self.__dict__.update(attrs)
|
|
@@ -2335,7 +2332,7 @@ class Record(BaseRecord):
|
|
|
2335
2332
|
exist, only one of them is returned (randomly).
|
|
2336
2333
|
"""
|
|
2337
2334
|
xml_ids = self._model._get_external_ids(self.ids)
|
|
2338
|
-
return
|
|
2335
|
+
return [*xml_ids][0] if xml_ids else False
|
|
2339
2336
|
|
|
2340
2337
|
def _set_external_id(self, xml_id):
|
|
2341
2338
|
"""Set the External ID of this record."""
|
|
@@ -2390,6 +2387,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
|
|
|
2390
2387
|
from _pyrepl import readline
|
|
2391
2388
|
else:
|
|
2392
2389
|
from code import InteractiveConsole as Console
|
|
2390
|
+
builtins.clear = type('', (), {'__repr__': lambda s: os.system('clear') or ''})()
|
|
2393
2391
|
try:
|
|
2394
2392
|
import readline
|
|
2395
2393
|
import rlcompleter
|
|
@@ -2407,7 +2405,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
|
|
|
2407
2405
|
pass # IOError if file missing, or other error
|
|
2408
2406
|
|
|
2409
2407
|
if use_pprint:
|
|
2410
|
-
pp = lambda obj: print(
|
|
2408
|
+
pp = lambda obj: print(color_repr(pprint.pformat(obj, **PP_FORMAT)))
|
|
2411
2409
|
|
|
2412
2410
|
def displayhook(value, _printer=pp, _builtins=builtins):
|
|
2413
2411
|
# Pretty-format the output
|
|
@@ -2419,7 +2417,7 @@ def _interact(global_vars, use_pprint=True, usage=USAGE):
|
|
|
2419
2417
|
def excepthook(exc_type, exc, tb):
|
|
2420
2418
|
# Print readable errors
|
|
2421
2419
|
msg = ''.join(format_exception(exc_type, exc, tb, chain=False, **exfmt))
|
|
2422
|
-
print(
|
|
2420
|
+
print(color_repr(msg.rstrip()))
|
|
2423
2421
|
sys.excepthook = excepthook
|
|
2424
2422
|
|
|
2425
2423
|
builtins.usage = type('Usage', (), {'__call__': lambda s: print(usage),
|
|
@@ -2484,7 +2482,7 @@ def main(interact=_interact):
|
|
|
2484
2482
|
return
|
|
2485
2483
|
|
|
2486
2484
|
global_vars = Client._set_interactive()
|
|
2487
|
-
print(
|
|
2485
|
+
print(color_repr(USAGE))
|
|
2488
2486
|
|
|
2489
2487
|
connect_client(args)
|
|
2490
2488
|
return interact(global_vars) if interact else global_vars
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
CSI = '\233' # Control Sequence Introducer, same as ESC+[ or \x1b[
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class THEME: # Default theme
|
|
27
|
+
keyword = f'{CSI}1;34m'
|
|
28
|
+
builtin = f'{CSI}36m'
|
|
29
|
+
comment = f'{CSI}31m'
|
|
30
|
+
string = f'{CSI}32m'
|
|
31
|
+
number = f'{CSI}33m'
|
|
32
|
+
op = f'{CSI}m' # No color
|
|
33
|
+
definition = f'{CSI}1m'
|
|
34
|
+
reset = f'{CSI}m'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def gen_colors(value, theme, keywords=KEYWORDS):
|
|
38
|
+
"""Generate color tuples (start, end, color)."""
|
|
39
|
+
sio = io.StringIO(value)
|
|
40
|
+
line_offsets = [..., 0] + [sio.tell() for __ in sio]
|
|
41
|
+
sio.seek(0)
|
|
42
|
+
|
|
43
|
+
bracket_level = is_def_name = 0
|
|
44
|
+
yield None, 0, theme.reset
|
|
45
|
+
try:
|
|
46
|
+
for token in tokenize.generate_tokens(sio.readline):
|
|
47
|
+
if token.start == token.end:
|
|
48
|
+
continue
|
|
49
|
+
color, end_offset = None, 0
|
|
50
|
+
if token.type in T_STRING:
|
|
51
|
+
if token.type in T_MIDDLE and token.string[-1:] in '{}':
|
|
52
|
+
end_offset += 1
|
|
53
|
+
color = theme.string
|
|
54
|
+
elif token.type == T.COMMENT:
|
|
55
|
+
color = theme.comment
|
|
56
|
+
elif token.type == T.NUMBER:
|
|
57
|
+
color = theme.number
|
|
58
|
+
elif token.type == T.OP:
|
|
59
|
+
if token.string in '([{':
|
|
60
|
+
bracket_level += 1
|
|
61
|
+
elif token.string in ')]}':
|
|
62
|
+
bracket_level -= 1
|
|
63
|
+
color = theme.op
|
|
64
|
+
elif token.type == T.NAME:
|
|
65
|
+
if token.string in keywords:
|
|
66
|
+
is_def_name = token.string in DEF_CLASS
|
|
67
|
+
color = theme.keyword
|
|
68
|
+
elif keywords is not KEYWORDS:
|
|
69
|
+
continue
|
|
70
|
+
elif is_def_name:
|
|
71
|
+
is_def_name = False
|
|
72
|
+
color = theme.definition
|
|
73
|
+
elif token.string in SOFTKEYW and not bracket_level:
|
|
74
|
+
color = theme.keyword # soft_keyword
|
|
75
|
+
elif token.string in BUILTINS:
|
|
76
|
+
color = theme.builtin
|
|
77
|
+
if color is not None:
|
|
78
|
+
start = line_offsets[token.start[0]] + token.start[1]
|
|
79
|
+
end = line_offsets[token.end[0]] + token.end[1] + end_offset
|
|
80
|
+
yield start, end, color
|
|
81
|
+
except (SyntaxError, tokenize.TokenError):
|
|
82
|
+
pass
|
|
83
|
+
yield None, None, theme.reset
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _apply_colors(value, theme, keywords):
|
|
87
|
+
output = ''
|
|
88
|
+
if value:
|
|
89
|
+
__, end, c_reset = next(colors := gen_colors(value, theme=theme, keywords=keywords))
|
|
90
|
+
|
|
91
|
+
while (last_pos := end) is not None:
|
|
92
|
+
start, end, color = next(colors)
|
|
93
|
+
if start is None:
|
|
94
|
+
output += _escape(value[last_pos:])
|
|
95
|
+
else:
|
|
96
|
+
output += _escape(value[last_pos:start]) + color + _escape(value[start:end]) + c_reset
|
|
97
|
+
return output
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _escape(value):
|
|
101
|
+
if value.isascii():
|
|
102
|
+
return value
|
|
103
|
+
result = ''
|
|
104
|
+
for char in value:
|
|
105
|
+
if char > '\x7f' and unicodedata.category(char)[:1] == 'C':
|
|
106
|
+
char = rf'\u{ord(char):04x}'
|
|
107
|
+
result += char
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def color_python(value):
|
|
112
|
+
"""Colorize Python syntax."""
|
|
113
|
+
return _apply_colors(value, THEME(), KEYWORDS)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def color_repr(value):
|
|
117
|
+
"""Colorize string representation."""
|
|
118
|
+
return _apply_colors(value, THEME(), CONSTANTS)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def color_json(value):
|
|
122
|
+
"""Colorize JSON representation."""
|
|
123
|
+
return _apply_colors(value, THEME(), J_SPECIAL)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def patch_colors(module):
|
|
127
|
+
"""Set functions to color output."""
|
|
128
|
+
global THEME
|
|
129
|
+
try: # Python >= 3.14
|
|
130
|
+
from _pyrepl.utils import BUILTINS, THEME
|
|
131
|
+
BUILTINS |= {'Client', 'client', 'env', 'clear'}
|
|
132
|
+
except ImportError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
module.color_repr = color_repr
|
|
136
|
+
decolor = odooly.partial(re.compile(r'(\233|\033\[)[;\d]*m').sub, '')
|
|
137
|
+
if module.color_py is not str: # Python >= 3.14
|
|
138
|
+
return {'color_repr': color_repr, 'decolor': decolor}
|
|
139
|
+
theme = THEME()
|
|
140
|
+
module.color_py = color_python
|
|
141
|
+
module.color_bold = f'{theme.definition}{{}}{theme.reset}'.format
|
|
142
|
+
module.color_comment = f'{theme.comment}{{}}{theme.reset}'.format
|
|
143
|
+
return {'color_py': color_python, 'color_repr': color_repr, 'decolor': decolor}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main():
|
|
147
|
+
args = odooly.get_parser().parse_args()
|
|
148
|
+
|
|
149
|
+
if args.config:
|
|
150
|
+
odooly.Client._config_file = odooly.Path.cwd() / args.config
|
|
151
|
+
if args.list_env:
|
|
152
|
+
print('Available settings: ' + ' '.join(odooly.read_config()))
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
global_vars = odooly.Client._set_interactive()
|
|
156
|
+
if odooly.color_py is not str or (os.getenv('FORCE_COLOR') and not os.getenv('NO_COLOR')):
|
|
157
|
+
global_vars.update(patch_colors(odooly))
|
|
158
|
+
|
|
159
|
+
print(odooly.color_repr(odooly.USAGE))
|
|
160
|
+
odooly.connect_client(args)
|
|
161
|
+
odooly._interact(global_vars)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
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
|