meerschaum 2.0.0rc7__py3-none-any.whl → 2.0.0rc8__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.
- meerschaum/actions/__init__.py +97 -48
- meerschaum/actions/bootstrap.py +1 -1
- meerschaum/actions/clear.py +1 -1
- meerschaum/actions/deduplicate.py +1 -1
- meerschaum/actions/delete.py +8 -7
- meerschaum/actions/drop.py +1 -10
- meerschaum/actions/edit.py +1 -1
- meerschaum/actions/install.py +1 -1
- meerschaum/actions/pause.py +1 -1
- meerschaum/actions/register.py +1 -1
- meerschaum/actions/setup.py +1 -1
- meerschaum/actions/show.py +1 -1
- meerschaum/actions/start.py +18 -7
- meerschaum/actions/stop.py +5 -4
- meerschaum/actions/sync.py +3 -1
- meerschaum/actions/uninstall.py +1 -1
- meerschaum/actions/upgrade.py +1 -1
- meerschaum/actions/verify.py +54 -3
- meerschaum/config/_formatting.py +26 -0
- meerschaum/config/_jobs.py +28 -5
- meerschaum/config/_paths.py +21 -5
- meerschaum/config/_version.py +1 -1
- meerschaum/connectors/api/_fetch.py +1 -1
- meerschaum/connectors/api/_pipes.py +6 -11
- meerschaum/connectors/sql/_fetch.py +29 -11
- meerschaum/core/Pipe/_deduplicate.py +39 -23
- meerschaum/core/Pipe/_dtypes.py +2 -1
- meerschaum/core/Pipe/_verify.py +59 -24
- meerschaum/plugins/__init__.py +3 -0
- meerschaum/utils/daemon/Daemon.py +108 -27
- meerschaum/utils/daemon/__init__.py +35 -1
- meerschaum/utils/formatting/__init__.py +144 -1
- meerschaum/utils/formatting/_pipes.py +28 -5
- meerschaum/utils/misc.py +183 -187
- meerschaum/utils/packages/__init__.py +1 -1
- meerschaum/utils/packages/_packages.py +1 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/METADATA +4 -1
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/RECORD +44 -44
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/LICENSE +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/NOTICE +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/WHEEL +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/top_level.txt +0 -0
- {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/zip-safe +0 -0
@@ -168,7 +168,7 @@ class Daemon:
|
|
168
168
|
finally:
|
169
169
|
self._log_refresh_timer.cancel()
|
170
170
|
self.rotating_log.close()
|
171
|
-
if self.pid_path.exists():
|
171
|
+
if self.pid is None and self.pid_path.exists():
|
172
172
|
self.pid_path.unlink()
|
173
173
|
|
174
174
|
if keep_daemon_output:
|
@@ -254,7 +254,7 @@ class Daemon:
|
|
254
254
|
return _launch_success_bool, msg
|
255
255
|
|
256
256
|
|
257
|
-
def kill(self, timeout: Optional[int] =
|
257
|
+
def kill(self, timeout: Optional[int] = 8) -> SuccessTuple:
|
258
258
|
"""Forcibly terminate a running daemon.
|
259
259
|
Sends a SIGTERM signal to the process.
|
260
260
|
|
@@ -293,7 +293,7 @@ class Daemon:
|
|
293
293
|
return True, "Success"
|
294
294
|
|
295
295
|
|
296
|
-
def quit(self, timeout:
|
296
|
+
def quit(self, timeout: Union[int, float, None] = None) -> SuccessTuple:
|
297
297
|
"""Gracefully quit a running daemon."""
|
298
298
|
if self.status == 'paused':
|
299
299
|
return self.kill(timeout)
|
@@ -305,10 +305,24 @@ class Daemon:
|
|
305
305
|
|
306
306
|
def pause(
|
307
307
|
self,
|
308
|
-
timeout:
|
309
|
-
check_timeout_interval: float =
|
308
|
+
timeout: Union[int, float, None] = None,
|
309
|
+
check_timeout_interval: Union[float, int, None] = None,
|
310
310
|
) -> SuccessTuple:
|
311
|
-
"""
|
311
|
+
"""
|
312
|
+
Pause the daemon if it is running.
|
313
|
+
|
314
|
+
Parameters
|
315
|
+
----------
|
316
|
+
timeout: Union[float, int, None], default None
|
317
|
+
The maximum number of seconds to wait for a process to suspend.
|
318
|
+
|
319
|
+
check_timeout_interval: Union[float, int, None], default None
|
320
|
+
The number of seconds to wait between checking if the process is still running.
|
321
|
+
|
322
|
+
Returns
|
323
|
+
-------
|
324
|
+
A `SuccessTuple` indicating whether the `Daemon` process was successfully suspended.
|
325
|
+
"""
|
312
326
|
if self.process is None:
|
313
327
|
return False, f"Daemon '{self.daemon_id}' is not running and cannot be paused."
|
314
328
|
|
@@ -320,8 +334,18 @@ class Daemon:
|
|
320
334
|
except Exception as e:
|
321
335
|
return False, f"Failed to pause daemon '{self.daemon_id}':\n{e}"
|
322
336
|
|
323
|
-
|
324
|
-
|
337
|
+
timeout = self.get_timeout_seconds(timeout)
|
338
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds(
|
339
|
+
check_timeout_interval
|
340
|
+
)
|
341
|
+
|
342
|
+
psutil = attempt_import('psutil')
|
343
|
+
|
344
|
+
if not timeout:
|
345
|
+
try:
|
346
|
+
success = self.process.status() == 'stopped'
|
347
|
+
except psutil.NoSuchProcess as e:
|
348
|
+
success = True
|
325
349
|
msg = "Success" if success else f"Failed to suspend daemon '{self.daemon_id}'."
|
326
350
|
if success:
|
327
351
|
self._capture_process_timestamp('paused')
|
@@ -329,9 +353,12 @@ class Daemon:
|
|
329
353
|
|
330
354
|
begin = time.perf_counter()
|
331
355
|
while (time.perf_counter() - begin) < timeout:
|
332
|
-
|
333
|
-
self.
|
334
|
-
|
356
|
+
try:
|
357
|
+
if self.process.status() == 'stopped':
|
358
|
+
self._capture_process_timestamp('paused')
|
359
|
+
return True, "Success"
|
360
|
+
except psutil.NoSuchProcess as e:
|
361
|
+
return False, f"Process exited unexpectedly. Was it killed?\n{e}"
|
335
362
|
time.sleep(check_timeout_interval)
|
336
363
|
|
337
364
|
return False, (
|
@@ -342,10 +369,24 @@ class Daemon:
|
|
342
369
|
|
343
370
|
def resume(
|
344
371
|
self,
|
345
|
-
timeout:
|
346
|
-
check_timeout_interval: float =
|
372
|
+
timeout: Union[int, float, None] = None,
|
373
|
+
check_timeout_interval: Union[float, int, None] = None,
|
347
374
|
) -> SuccessTuple:
|
348
|
-
"""
|
375
|
+
"""
|
376
|
+
Resume the daemon if it is paused.
|
377
|
+
|
378
|
+
Parameters
|
379
|
+
----------
|
380
|
+
timeout: Union[float, int, None], default None
|
381
|
+
The maximum number of seconds to wait for a process to resume.
|
382
|
+
|
383
|
+
check_timeout_interval: Union[float, int, None], default None
|
384
|
+
The number of seconds to wait between checking if the process is still stopped.
|
385
|
+
|
386
|
+
Returns
|
387
|
+
-------
|
388
|
+
A `SuccessTuple` indicating whether the `Daemon` process was successfully resumed.
|
389
|
+
"""
|
349
390
|
if self.status == 'running':
|
350
391
|
return True, f"Daemon '{self.daemon_id}' is already running."
|
351
392
|
|
@@ -357,7 +398,12 @@ class Daemon:
|
|
357
398
|
except Exception as e:
|
358
399
|
return False, f"Failed to resume daemon '{self.daemon_id}':\n{e}"
|
359
400
|
|
360
|
-
|
401
|
+
timeout = self.get_timeout_seconds(timeout)
|
402
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds(
|
403
|
+
check_timeout_interval
|
404
|
+
)
|
405
|
+
|
406
|
+
if not timeout:
|
361
407
|
success = self.status == 'running'
|
362
408
|
msg = "Success" if success else f"Failed to resume daemon '{self.daemon_id}'."
|
363
409
|
if success:
|
@@ -417,8 +463,8 @@ class Daemon:
|
|
417
463
|
def _send_signal(
|
418
464
|
self,
|
419
465
|
signal_to_send,
|
420
|
-
timeout:
|
421
|
-
check_timeout_interval: float =
|
466
|
+
timeout: Union[float, int, None] = None,
|
467
|
+
check_timeout_interval: Union[float, int, None] = None,
|
422
468
|
) -> SuccessTuple:
|
423
469
|
"""Send a signal to the daemon process.
|
424
470
|
|
@@ -427,13 +473,11 @@ class Daemon:
|
|
427
473
|
signal_to_send:
|
428
474
|
The signal the send to the daemon, e.g. `signals.SIGINT`.
|
429
475
|
|
430
|
-
timeout:
|
476
|
+
timeout: Union[float, int, None], default None
|
431
477
|
The maximum number of seconds to wait for a process to terminate.
|
432
|
-
Defaults to 3.
|
433
478
|
|
434
|
-
check_timeout_interval: float, default
|
479
|
+
check_timeout_interval: Union[float, int, None], default None
|
435
480
|
The number of seconds to wait between checking if the process is still running.
|
436
|
-
Defaults to 0.1.
|
437
481
|
|
438
482
|
Returns
|
439
483
|
-------
|
@@ -444,11 +488,17 @@ class Daemon:
|
|
444
488
|
except Exception as e:
|
445
489
|
return False, f"Failed to send signal {signal_to_send}:\n{traceback.format_exc()}"
|
446
490
|
|
447
|
-
|
491
|
+
timeout = self.get_timeout_seconds(timeout)
|
492
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds(
|
493
|
+
check_timeout_interval
|
494
|
+
)
|
495
|
+
|
496
|
+
if not timeout:
|
448
497
|
return True, f"Successfully sent '{signal}' to daemon '{self.daemon_id}'."
|
498
|
+
|
449
499
|
begin = time.perf_counter()
|
450
500
|
while (time.perf_counter() - begin) < timeout:
|
451
|
-
if not self.
|
501
|
+
if not self.status == 'running':
|
452
502
|
return True, "Success"
|
453
503
|
time.sleep(check_timeout_interval)
|
454
504
|
|
@@ -464,8 +514,8 @@ class Daemon:
|
|
464
514
|
raise a `FileExistsError`.
|
465
515
|
"""
|
466
516
|
try:
|
467
|
-
self.path.mkdir(parents=True, exist_ok=
|
468
|
-
_already_exists =
|
517
|
+
self.path.mkdir(parents=True, exist_ok=True)
|
518
|
+
_already_exists = any(os.scandir(self.path))
|
469
519
|
except FileExistsError:
|
470
520
|
_already_exists = True
|
471
521
|
|
@@ -506,8 +556,17 @@ class Daemon:
|
|
506
556
|
if self.process is None:
|
507
557
|
return 'stopped'
|
508
558
|
|
509
|
-
|
510
|
-
|
559
|
+
psutil = attempt_import('psutil')
|
560
|
+
try:
|
561
|
+
if self.process.status() == 'stopped':
|
562
|
+
return 'paused'
|
563
|
+
except psutil.NoSuchProcess:
|
564
|
+
if self.pid_path.exists():
|
565
|
+
try:
|
566
|
+
self.pid_path.unlink()
|
567
|
+
except Exception as e:
|
568
|
+
pass
|
569
|
+
return 'stopped'
|
511
570
|
|
512
571
|
return 'running'
|
513
572
|
|
@@ -804,6 +863,28 @@ class Daemon:
|
|
804
863
|
self.rotating_log.delete()
|
805
864
|
|
806
865
|
|
866
|
+
def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]:
|
867
|
+
"""
|
868
|
+
Return the timeout value to use. Use `--timeout-seconds` if provided,
|
869
|
+
else the configured default (8).
|
870
|
+
"""
|
871
|
+
if isinstance(timeout, (int, float)):
|
872
|
+
return timeout
|
873
|
+
return get_config('jobs', 'timeout_seconds')
|
874
|
+
|
875
|
+
|
876
|
+
def get_check_timeout_interval_seconds(
|
877
|
+
self,
|
878
|
+
check_timeout_interval: Union[int, float, None] = None,
|
879
|
+
) -> Union[int, float]:
|
880
|
+
"""
|
881
|
+
Return the interval value to check the status of timeouts.
|
882
|
+
"""
|
883
|
+
if isinstance(check_timeout_interval, (int, float)):
|
884
|
+
return check_timeout_interval
|
885
|
+
return get_config('jobs', 'check_timeout_interval_seconds')
|
886
|
+
|
887
|
+
|
807
888
|
def __getstate__(self):
|
808
889
|
"""
|
809
890
|
Pickle this Daemon.
|
@@ -14,6 +14,7 @@ from meerschaum.utils.daemon.Daemon import Daemon
|
|
14
14
|
from meerschaum.utils.daemon.Log import Log
|
15
15
|
from meerschaum.utils.daemon.RotatingFile import RotatingFile
|
16
16
|
|
17
|
+
|
17
18
|
def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
18
19
|
"""Parse sysargs and execute a Meerschaum action as a daemon.
|
19
20
|
|
@@ -27,7 +28,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
27
28
|
A SuccessTuple.
|
28
29
|
"""
|
29
30
|
from meerschaum._internal.entry import entry
|
30
|
-
_args =
|
31
|
+
_args = {}
|
31
32
|
if '--name' in sysargs or '--job-name' in sysargs:
|
32
33
|
from meerschaum._internal.arguments._parse_arguments import parse_arguments
|
33
34
|
_args = parse_arguments(sysargs)
|
@@ -36,6 +37,38 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
36
37
|
label = shlex.join(filtered_sysargs) if sysargs else None
|
37
38
|
except Exception as e:
|
38
39
|
label = ' '.join(filtered_sysargs) if sysargs else None
|
40
|
+
|
41
|
+
name = _args.get('name', None)
|
42
|
+
daemon = None
|
43
|
+
if name:
|
44
|
+
try:
|
45
|
+
daemon = Daemon(daemon_id=name)
|
46
|
+
except Exception as e:
|
47
|
+
daemon = None
|
48
|
+
|
49
|
+
if daemon is not None:
|
50
|
+
existing_sysargs = daemon.properties['target']['args'][0]
|
51
|
+
existing_kwargs = parse_arguments(existing_sysargs)
|
52
|
+
|
53
|
+
### Remove sysargs because flags are aliased.
|
54
|
+
_ = _args.pop('daemon', None)
|
55
|
+
_ = _args.pop('sysargs', None)
|
56
|
+
_ = _args.pop('filtered_sysargs', None)
|
57
|
+
debug = _args.pop('debug', None)
|
58
|
+
_args['sub_args'] = sorted(_args.get('sub_args', []))
|
59
|
+
_ = existing_kwargs.pop('daemon', None)
|
60
|
+
_ = existing_kwargs.pop('sysargs', None)
|
61
|
+
_ = existing_kwargs.pop('filtered_sysargs', None)
|
62
|
+
_ = existing_kwargs.pop('debug', None)
|
63
|
+
existing_kwargs['sub_args'] = sorted(existing_kwargs.get('sub_args', []))
|
64
|
+
|
65
|
+
### Only run if the kwargs equal or no actions are provided.
|
66
|
+
if existing_kwargs == _args or not _args.get('action', []):
|
67
|
+
return daemon.run(
|
68
|
+
debug = debug,
|
69
|
+
allow_dirty_run = True,
|
70
|
+
)
|
71
|
+
|
39
72
|
success_tuple = run_daemon(
|
40
73
|
entry,
|
41
74
|
filtered_sysargs,
|
@@ -47,6 +80,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
47
80
|
success_tuple = False, str(success_tuple)
|
48
81
|
return success_tuple
|
49
82
|
|
83
|
+
|
50
84
|
def daemon_action(**kw) -> SuccessTuple:
|
51
85
|
"""Execute a Meerschaum action as a daemon."""
|
52
86
|
from meerschaum.utils.packages import run_python_package
|
@@ -33,6 +33,7 @@ __all__ = sorted([
|
|
33
33
|
'translate_rich_to_termcolor',
|
34
34
|
'get_console',
|
35
35
|
'print_tuple',
|
36
|
+
'print_options',
|
36
37
|
'fill_ansi',
|
37
38
|
'pprint',
|
38
39
|
'highlight_pipes',
|
@@ -222,9 +223,29 @@ def print_tuple(
|
|
222
223
|
common_only: bool = False,
|
223
224
|
upper_padding: int = 0,
|
224
225
|
lower_padding: int = 0,
|
226
|
+
calm: bool = False,
|
225
227
|
_progress: Optional['rich.progress.Progress'] = None,
|
226
228
|
) -> None:
|
227
|
-
"""
|
229
|
+
"""
|
230
|
+
Print `meerschaum.utils.typing.SuccessTuple`.
|
231
|
+
|
232
|
+
Parameters
|
233
|
+
----------
|
234
|
+
skip_common: bool, default True
|
235
|
+
If `True`, do not print common success tuples (i.e. `(True, "Success")`).
|
236
|
+
|
237
|
+
common_only: bool, default False
|
238
|
+
If `True`, only print if the success tuple is common.
|
239
|
+
|
240
|
+
upper_padding: int, default 0
|
241
|
+
How many newlines to prepend to the message.
|
242
|
+
|
243
|
+
lower_padding: int, default 0
|
244
|
+
How many newlines to append to the message.
|
245
|
+
|
246
|
+
calm: bool, default False
|
247
|
+
If `True`, use the default emoji and color scheme.
|
248
|
+
"""
|
228
249
|
from meerschaum.config.static import STATIC_CONFIG
|
229
250
|
_init()
|
230
251
|
try:
|
@@ -233,6 +254,9 @@ def print_tuple(
|
|
233
254
|
status = 'failure'
|
234
255
|
tup = None, None
|
235
256
|
|
257
|
+
if calm:
|
258
|
+
status += '_calm'
|
259
|
+
|
236
260
|
omit_messages = STATIC_CONFIG['system']['success']['ignore']
|
237
261
|
|
238
262
|
do_print = True
|
@@ -262,6 +286,125 @@ def print_tuple(
|
|
262
286
|
print(msg)
|
263
287
|
|
264
288
|
|
289
|
+
def print_options(
|
290
|
+
options: Optional[Dict[str, Any]] = None,
|
291
|
+
nopretty: bool = False,
|
292
|
+
no_rich: bool = False,
|
293
|
+
name: str = 'options',
|
294
|
+
header: Optional[str] = None,
|
295
|
+
num_cols: Optional[int] = None,
|
296
|
+
adjust_cols: bool = True,
|
297
|
+
**kw
|
298
|
+
) -> None:
|
299
|
+
"""
|
300
|
+
Print items in an iterable as a fancy table.
|
301
|
+
|
302
|
+
Parameters
|
303
|
+
----------
|
304
|
+
options: Optional[Dict[str, Any]], default None
|
305
|
+
The iterable to be printed.
|
306
|
+
|
307
|
+
nopretty: bool, default False
|
308
|
+
If `True`, don't use fancy formatting.
|
309
|
+
|
310
|
+
no_rich: bool, default False
|
311
|
+
If `True`, don't use `rich` to format the output.
|
312
|
+
|
313
|
+
name: str, default 'options'
|
314
|
+
The text in the default header after `'Available'`.
|
315
|
+
|
316
|
+
header: Optional[str], default None
|
317
|
+
If provided, override `name` and use this as the header text.
|
318
|
+
|
319
|
+
num_cols: Optional[int], default None
|
320
|
+
How many columns in the table. Depends on the terminal size. If `None`, use 8.
|
321
|
+
|
322
|
+
adjust_cols: bool, default True
|
323
|
+
If `True`, adjust the number of columns depending on the terminal size.
|
324
|
+
|
325
|
+
"""
|
326
|
+
import os
|
327
|
+
from meerschaum.utils.packages import import_rich
|
328
|
+
from meerschaum.utils.formatting import make_header, highlight_pipes
|
329
|
+
from meerschaum.actions import actions as _actions
|
330
|
+
from meerschaum.utils.misc import get_cols_lines, string_width, iterate_chunks
|
331
|
+
|
332
|
+
|
333
|
+
if options is None:
|
334
|
+
options = {}
|
335
|
+
_options = []
|
336
|
+
for o in options:
|
337
|
+
_options.append(str(o))
|
338
|
+
_header = f"Available {name}" if header is None else header
|
339
|
+
|
340
|
+
if num_cols is None:
|
341
|
+
num_cols = 8
|
342
|
+
|
343
|
+
def _print_options_no_rich():
|
344
|
+
if not nopretty:
|
345
|
+
print()
|
346
|
+
print(make_header(_header))
|
347
|
+
### print actions
|
348
|
+
for option in sorted(_options):
|
349
|
+
if not nopretty:
|
350
|
+
print(" - ", end="")
|
351
|
+
print(option)
|
352
|
+
if not nopretty:
|
353
|
+
print()
|
354
|
+
|
355
|
+
rich = import_rich()
|
356
|
+
if rich is None or nopretty or no_rich:
|
357
|
+
_print_options_no_rich()
|
358
|
+
return None
|
359
|
+
|
360
|
+
### Prevent too many options from being truncated on small terminals.
|
361
|
+
if adjust_cols and _options:
|
362
|
+
_cols, _lines = get_cols_lines()
|
363
|
+
while num_cols > 1:
|
364
|
+
cell_len = int(((_cols - 4) - (3 * (num_cols - 1))) / num_cols)
|
365
|
+
num_too_big = sum([(1 if string_width(o) > cell_len else 0) for o in _options])
|
366
|
+
if num_too_big > int(len(_options) / 3):
|
367
|
+
num_cols -= 1
|
368
|
+
continue
|
369
|
+
break
|
370
|
+
|
371
|
+
from meerschaum.utils.formatting import pprint, get_console
|
372
|
+
from meerschaum.utils.packages import attempt_import
|
373
|
+
rich_columns = attempt_import('rich.columns')
|
374
|
+
rich_panel = attempt_import('rich.panel')
|
375
|
+
rich_table = attempt_import('rich.table')
|
376
|
+
Text = attempt_import('rich.text').Text
|
377
|
+
box = attempt_import('rich.box')
|
378
|
+
Panel = rich_panel.Panel
|
379
|
+
Columns = rich_columns.Columns
|
380
|
+
Table = rich_table.Table
|
381
|
+
|
382
|
+
if _header is not None:
|
383
|
+
table = Table(
|
384
|
+
title = '\n' + _header,
|
385
|
+
box = box.SIMPLE,
|
386
|
+
show_header = False,
|
387
|
+
show_footer = False,
|
388
|
+
title_style = '',
|
389
|
+
expand = True,
|
390
|
+
)
|
391
|
+
else:
|
392
|
+
table = Table.grid(padding=(0, 2))
|
393
|
+
for i in range(num_cols):
|
394
|
+
table.add_column()
|
395
|
+
|
396
|
+
chunks = iterate_chunks(
|
397
|
+
[Text.from_ansi(highlight_pipes(o)) for o in sorted(_options)],
|
398
|
+
num_cols,
|
399
|
+
fillvalue=''
|
400
|
+
)
|
401
|
+
for c in chunks:
|
402
|
+
table.add_row(*c)
|
403
|
+
|
404
|
+
get_console().print(table)
|
405
|
+
return None
|
406
|
+
|
407
|
+
|
265
408
|
def fill_ansi(string: str, style: str = '') -> str:
|
266
409
|
"""
|
267
410
|
Fill in non-formatted segments of ANSI text.
|
@@ -309,19 +309,42 @@ def highlight_pipes(message: str) -> str:
|
|
309
309
|
"""
|
310
310
|
Add syntax highlighting to an info message containing stringified `meerschaum.Pipe` objects.
|
311
311
|
"""
|
312
|
-
if 'Pipe(' not in message
|
312
|
+
if 'Pipe(' not in message and ')' not in message:
|
313
313
|
return message
|
314
|
+
|
314
315
|
from meerschaum import Pipe
|
315
316
|
segments = message.split('Pipe(')
|
316
317
|
msg = ''
|
317
318
|
_d = {}
|
318
319
|
for i, segment in enumerate(segments):
|
319
|
-
|
320
|
-
|
321
|
-
|
320
|
+
comma_index = segment.find(',')
|
321
|
+
paren_index = segment.find(')')
|
322
|
+
single_quote_index = segment.find("'")
|
323
|
+
double_quote_index = segment.find('"')
|
324
|
+
|
325
|
+
has_comma = comma_index != -1
|
326
|
+
has_paren = paren_index != -1
|
327
|
+
has_single_quote = single_quote_index != -1
|
328
|
+
has_double_quote = double_quote_index != -1
|
329
|
+
quote_index = double_quote_index if has_double_quote else single_quote_index
|
330
|
+
|
331
|
+
has_pipe = (
|
332
|
+
has_comma
|
333
|
+
and
|
334
|
+
has_paren
|
335
|
+
and
|
336
|
+
(has_single_quote or has_double_quote)
|
337
|
+
and not
|
338
|
+
(has_double_quote and has_double_quote)
|
339
|
+
and not
|
340
|
+
(comma_index > paren_index or quote_index > paren_index)
|
341
|
+
)
|
342
|
+
|
343
|
+
if has_pipe:
|
344
|
+
code = "_d['pipe'] = Pipe(" + segment[:paren_index + 1]
|
322
345
|
try:
|
323
346
|
exec(code)
|
324
|
-
_to_add = pipe_repr(_d['pipe']) + segment[paren_index:]
|
347
|
+
_to_add = pipe_repr(_d['pipe']) + segment[paren_index + 1:]
|
325
348
|
except Exception as e:
|
326
349
|
_to_add = 'Pipe(' + segment
|
327
350
|
msg += _to_add
|