meerschaum 2.2.5.dev3__py3-none-any.whl → 2.2.7__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.
Files changed (65) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/__main__.py +10 -5
  3. meerschaum/_internal/arguments/_parser.py +13 -2
  4. meerschaum/_internal/docs/index.py +523 -26
  5. meerschaum/_internal/entry.py +13 -13
  6. meerschaum/_internal/shell/Shell.py +26 -22
  7. meerschaum/_internal/shell/updates.py +175 -0
  8. meerschaum/_internal/term/__init__.py +2 -2
  9. meerschaum/actions/bootstrap.py +13 -14
  10. meerschaum/actions/python.py +11 -8
  11. meerschaum/actions/register.py +149 -37
  12. meerschaum/actions/show.py +79 -71
  13. meerschaum/actions/stop.py +11 -11
  14. meerschaum/actions/sync.py +3 -3
  15. meerschaum/actions/upgrade.py +28 -36
  16. meerschaum/api/dash/callbacks/login.py +21 -13
  17. meerschaum/api/dash/pages/login.py +2 -2
  18. meerschaum/api/routes/_login.py +5 -5
  19. meerschaum/api/routes/_pipes.py +20 -20
  20. meerschaum/config/__init__.py +8 -1
  21. meerschaum/config/_formatting.py +1 -0
  22. meerschaum/config/_paths.py +24 -2
  23. meerschaum/config/_shell.py +78 -66
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/config/paths.py +21 -2
  26. meerschaum/config/static/__init__.py +2 -0
  27. meerschaum/connectors/Connector.py +7 -2
  28. meerschaum/connectors/__init__.py +7 -5
  29. meerschaum/connectors/api/APIConnector.py +7 -2
  30. meerschaum/connectors/api/_actions.py +23 -31
  31. meerschaum/connectors/api/_misc.py +1 -1
  32. meerschaum/connectors/api/_request.py +13 -9
  33. meerschaum/connectors/api/_uri.py +5 -5
  34. meerschaum/core/Pipe/__init__.py +7 -3
  35. meerschaum/core/Pipe/_data.py +23 -15
  36. meerschaum/core/Pipe/_deduplicate.py +1 -1
  37. meerschaum/core/Pipe/_dtypes.py +5 -0
  38. meerschaum/core/Pipe/_fetch.py +18 -16
  39. meerschaum/core/Pipe/_sync.py +23 -15
  40. meerschaum/plugins/_Plugin.py +6 -6
  41. meerschaum/plugins/__init__.py +1 -1
  42. meerschaum/utils/daemon/Daemon.py +88 -129
  43. meerschaum/utils/daemon/FileDescriptorInterceptor.py +14 -5
  44. meerschaum/utils/daemon/RotatingFile.py +23 -17
  45. meerschaum/utils/daemon/__init__.py +28 -21
  46. meerschaum/utils/dataframe.py +12 -4
  47. meerschaum/utils/debug.py +9 -15
  48. meerschaum/utils/formatting/__init__.py +92 -46
  49. meerschaum/utils/formatting/_jobs.py +47 -9
  50. meerschaum/utils/misc.py +117 -11
  51. meerschaum/utils/packages/__init__.py +28 -16
  52. meerschaum/utils/prompt.py +5 -0
  53. meerschaum/utils/schedule.py +21 -15
  54. meerschaum/utils/typing.py +1 -0
  55. meerschaum/utils/venv/__init__.py +5 -1
  56. meerschaum/utils/warnings.py +8 -1
  57. meerschaum/utils/yaml.py +2 -2
  58. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/METADATA +1 -1
  59. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/RECORD +65 -64
  60. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/WHEEL +1 -1
  61. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/LICENSE +0 -0
  62. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/NOTICE +0 -0
  63. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/entry_points.txt +0 -0
  64. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/top_level.txt +0 -0
  65. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/zip-safe +0 -0
@@ -32,14 +32,14 @@ class RotatingFile(io.IOBase):
32
32
  SEEK_BACK_ATTEMPTS: int = 5
33
33
 
34
34
  def __init__(
35
- self,
36
- file_path: pathlib.Path,
37
- num_files_to_keep: Optional[int] = None,
38
- max_file_size: Optional[int] = None,
39
- redirect_streams: bool = False,
40
- write_timestamps: bool = False,
41
- timestamp_format: str = '%Y-%m-%d %H:%M',
42
- ):
35
+ self,
36
+ file_path: pathlib.Path,
37
+ num_files_to_keep: Optional[int] = None,
38
+ max_file_size: Optional[int] = None,
39
+ redirect_streams: bool = False,
40
+ write_timestamps: bool = False,
41
+ timestamp_format: str = '%Y-%m-%d %H:%M',
42
+ ):
43
43
  """
44
44
  Create a file-like object which manages other files.
45
45
 
@@ -79,11 +79,7 @@ class RotatingFile(io.IOBase):
79
79
  self.redirect_streams = redirect_streams
80
80
  self.write_timestamps = write_timestamps
81
81
  self.timestamp_format = timestamp_format
82
- self.subfile_regex_pattern = re.compile(
83
- r'^'
84
- + self.file_path.name
85
- + r'(?:\.\d+)?$'
86
- )
82
+ self.subfile_regex_pattern = re.compile(r'(.*)\.log(?:\.\d+)?$')
87
83
 
88
84
  ### When subfiles are opened, map from their index to the file objects.
89
85
  self.subfile_objects = {}
@@ -173,7 +169,7 @@ class RotatingFile(io.IOBase):
173
169
  latest_index = (
174
170
  self.get_index_from_subfile_name(existing_subfile_paths[-1].name)
175
171
  if existing_subfile_paths
176
- else -1
172
+ else 0
177
173
  )
178
174
  return latest_index
179
175
 
@@ -222,9 +218,12 @@ class RotatingFile(io.IOBase):
222
218
  [
223
219
  (file_name, self.get_index_from_subfile_name(file_name))
224
220
  for file_name in os.listdir(self.file_path.parent)
225
- if re.match(self.subfile_regex_pattern, file_name)
221
+ if (
222
+ file_name.startswith(self.file_path.name)
223
+ and re.match(self.subfile_regex_pattern, file_name)
224
+ )
226
225
  ],
227
- key = lambda x: x[1],
226
+ key=lambda x: x[1],
228
227
  )
229
228
  return [
230
229
  (self.file_path.parent / file_name)
@@ -372,6 +371,9 @@ class RotatingFile(io.IOBase):
372
371
  self._current_file_obj.write(data)
373
372
  if suffix_str:
374
373
  self._current_file_obj.write(suffix_str)
374
+ except BrokenPipeError:
375
+ warn("BrokenPipeError encountered. The daemon may have been terminated.")
376
+ return
375
377
  except Exception as e:
376
378
  warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
377
379
  self.flush()
@@ -582,10 +584,14 @@ class RotatingFile(io.IOBase):
582
584
  if self.redirect_streams:
583
585
  try:
584
586
  sys.stdout.flush()
587
+ except BrokenPipeError:
588
+ pass
585
589
  except Exception as e:
586
590
  warn(f"Failed to flush STDOUT:\n{traceback.format_exc()}")
587
591
  try:
588
592
  sys.stderr.flush()
593
+ except BrokenPipeError:
594
+ pass
589
595
  except Exception as e:
590
596
  warn(f"Failed to flush STDERR:\n{traceback.format_exc()}")
591
597
 
@@ -597,7 +603,6 @@ class RotatingFile(io.IOBase):
597
603
  if not self.write_timestamps:
598
604
  return
599
605
 
600
- threads = self.__dict__.get('_interceptor_threads', [])
601
606
  self._stdout_interceptor = FileDescriptorInterceptor(
602
607
  sys.stdout.fileno(),
603
608
  self.get_timestamp_prefix_str,
@@ -640,6 +645,7 @@ class RotatingFile(io.IOBase):
640
645
  """
641
646
  if not self.write_timestamps:
642
647
  return
648
+
643
649
  interceptors = self.__dict__.get('_interceptors', [])
644
650
  interceptor_threads = self.__dict__.get('_interceptor_threads', [])
645
651
 
@@ -67,19 +67,17 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
67
67
  if daemon.status == 'running':
68
68
  return True, f"Daemon '{daemon}' is already running."
69
69
  return daemon.run(
70
- debug = debug,
71
- allow_dirty_run = True,
70
+ debug=debug,
71
+ allow_dirty_run=True,
72
72
  )
73
73
 
74
74
  success_tuple = run_daemon(
75
75
  entry,
76
76
  filtered_sysargs,
77
- daemon_id = _args.get('name', None) if _args else None,
78
- label = label,
79
- keep_daemon_output = ('--rm' not in sysargs)
77
+ daemon_id=_args.get('name', None) if _args else None,
78
+ label=label,
79
+ keep_daemon_output=('--rm' not in sysargs),
80
80
  )
81
- if not isinstance(success_tuple, tuple):
82
- success_tuple = False, str(success_tuple)
83
81
  return success_tuple
84
82
 
85
83
 
@@ -109,25 +107,25 @@ def daemon_action(**kw) -> SuccessTuple:
109
107
 
110
108
 
111
109
  def run_daemon(
112
- func: Callable[[Any], Any],
113
- *args,
114
- daemon_id: Optional[str] = None,
115
- keep_daemon_output: bool = False,
116
- allow_dirty_run: bool = False,
117
- label: Optional[str] = None,
118
- **kw
119
- ) -> Any:
110
+ func: Callable[[Any], Any],
111
+ *args,
112
+ daemon_id: Optional[str] = None,
113
+ keep_daemon_output: bool = True,
114
+ allow_dirty_run: bool = False,
115
+ label: Optional[str] = None,
116
+ **kw
117
+ ) -> Any:
120
118
  """Execute a function as a daemon."""
121
119
  daemon = Daemon(
122
120
  func,
123
- daemon_id = daemon_id,
124
- target_args = [arg for arg in args],
125
- target_kw = kw,
126
- label = label,
121
+ daemon_id=daemon_id,
122
+ target_args=[arg for arg in args],
123
+ target_kw=kw,
124
+ label=label,
127
125
  )
128
126
  return daemon.run(
129
- keep_daemon_output = keep_daemon_output,
130
- allow_dirty_run = allow_dirty_run,
127
+ keep_daemon_output=keep_daemon_output,
128
+ allow_dirty_run=allow_dirty_run,
131
129
  )
132
130
 
133
131
 
@@ -268,3 +266,12 @@ def get_filtered_daemons(
268
266
  pass
269
267
  daemons.append(d)
270
268
  return daemons
269
+
270
+
271
+ def running_in_daemon() -> bool:
272
+ """
273
+ Return whether the current thread is running in a Daemon context.
274
+ """
275
+ from meerschaum.config.static import STATIC_CONFIG
276
+ daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
277
+ return daemon_env_var in os.environ
@@ -8,13 +8,21 @@ Utility functions for working with DataFrames.
8
8
 
9
9
  from __future__ import annotations
10
10
  from datetime import datetime
11
+
12
+ import meerschaum as mrsm
11
13
  from meerschaum.utils.typing import (
12
14
  Optional, Dict, Any, List, Hashable, Generator,
13
- Iterator, Iterable, Union, Tuple,
15
+ Iterator, Iterable, Union, TYPE_CHECKING,
14
16
  )
15
17
 
18
+ if TYPE_CHECKING:
19
+ pd, dask = mrsm.attempt_import('pandas', 'dask')
20
+
16
21
 
17
- def add_missing_cols_to_df(df: 'pd.DataFrame', dtypes: Dict[str, Any]) -> pd.DataFrame:
22
+ def add_missing_cols_to_df(
23
+ df: 'pd.DataFrame',
24
+ dtypes: Dict[str, Any],
25
+ ) -> 'pd.DataFrame':
18
26
  """
19
27
  Add columns from the dtypes dictionary as null columns to a new DataFrame.
20
28
 
@@ -723,7 +731,7 @@ def get_datetime_bound_from_df(
723
731
  df: Union['pd.DataFrame', dict, list],
724
732
  datetime_column: str,
725
733
  minimum: bool = True,
726
- ) -> Union[int, 'datetime.datetime', None]:
734
+ ) -> Union[int, datetime, None]:
727
735
  """
728
736
  Return the minimum or maximum datetime (or integer) from a DataFrame.
729
737
 
@@ -818,7 +826,7 @@ def chunksize_to_npartitions(chunksize: Optional[int]) -> int:
818
826
 
819
827
 
820
828
  def df_from_literal(
821
- pipe: Optional['meerschaum.Pipe'] = None,
829
+ pipe: Optional[mrsm.Pipe] = None,
822
830
  literal: str = None,
823
831
  debug: bool = False
824
832
  ) -> 'pd.DataFrame':
meerschaum/utils/debug.py CHANGED
@@ -93,22 +93,16 @@ def _checkpoint(
93
93
  ) -> None:
94
94
  """If the `_progress` and `_task` objects are provided, increment the task by one step.
95
95
  If `_total` is provided, update the total instead.
96
-
97
- Parameters
98
- ----------
99
- _progress: Optional['rich.progress.Progress'] :
100
- (Default value = None)
101
- _task: Optional[int] :
102
- (Default value = None)
103
- _total: Optional[int] :
104
- (Default value = None)
105
- **kw :
106
-
107
-
108
- Returns
109
- -------
110
-
111
96
  """
112
97
  if _progress is not None and _task is not None:
113
98
  _kw = {'total': _total} if _total is not None else {'advance': 1}
114
99
  _progress.update(_task, **_kw)
100
+
101
+
102
+ def trace(browser: bool = True):
103
+ """
104
+ Open a web-based debugger to trace the execution of the program.
105
+ """
106
+ from meerschaum.utils.packages import attempt_import
107
+ heartrate = attempt_import('heartrate')
108
+ heartrate.trace(files=heartrate.files.all, browser=browser)
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import platform
11
11
  import os
12
12
  import sys
13
+ import meerschaum as mrsm
13
14
  from meerschaum.utils.typing import Optional, Union, Any, Dict
14
15
  from meerschaum.utils.formatting._shell import make_header
15
16
  from meerschaum.utils.formatting._pprint import pprint
@@ -33,6 +34,7 @@ __all__ = sorted([
33
34
  'colored',
34
35
  'translate_rich_to_termcolor',
35
36
  'get_console',
37
+ 'format_success_tuple',
36
38
  'print_tuple',
37
39
  'print_options',
38
40
  'fill_ansi',
@@ -222,16 +224,17 @@ def get_console():
222
224
 
223
225
 
224
226
  def print_tuple(
225
- tup: tuple,
226
- skip_common: bool = True,
227
- common_only: bool = False,
228
- upper_padding: int = 0,
229
- lower_padding: int = 0,
230
- calm: bool = False,
231
- _progress: Optional['rich.progress.Progress'] = None,
232
- ) -> None:
227
+ tup: mrsm.SuccessTuple,
228
+ skip_common: bool = True,
229
+ common_only: bool = False,
230
+ upper_padding: int = 0,
231
+ lower_padding: int = 0,
232
+ left_padding: int = 1,
233
+ calm: bool = False,
234
+ _progress: Optional['rich.progress.Progress'] = None,
235
+ ) -> None:
233
236
  """
234
- Print `meerschaum.utils.typing.SuccessTuple`.
237
+ Format `meerschaum.utils.typing.SuccessTuple`.
235
238
 
236
239
  Parameters
237
240
  ----------
@@ -247,6 +250,59 @@ def print_tuple(
247
250
  lower_padding: int, default 0
248
251
  How many newlines to append to the message.
249
252
 
253
+ left_padding: int, default 1
254
+ How mant spaces to preprend to the message.
255
+
256
+ calm: bool, default False
257
+ If `True`, use the default emoji and color scheme.
258
+
259
+ """
260
+ from meerschaum.config.static import STATIC_CONFIG
261
+ do_print = True
262
+
263
+ omit_messages = STATIC_CONFIG['system']['success']['ignore']
264
+
265
+ if common_only:
266
+ skip_common = False
267
+ do_print = tup[1] in omit_messages
268
+
269
+ if skip_common:
270
+ do_print = tup[1] not in omit_messages
271
+
272
+ if not do_print:
273
+ return
274
+
275
+ print(format_success_tuple(
276
+ tup,
277
+ upper_padding=upper_padding,
278
+ lower_padding=lower_padding,
279
+ calm=calm,
280
+ _progress=_progress,
281
+ ))
282
+
283
+
284
+ def format_success_tuple(
285
+ tup: mrsm.SuccessTuple,
286
+ upper_padding: int = 0,
287
+ lower_padding: int = 0,
288
+ left_padding: int = 1,
289
+ calm: bool = False,
290
+ _progress: Optional['rich.progress.Progress'] = None,
291
+ ) -> str:
292
+ """
293
+ Format `meerschaum.utils.typing.SuccessTuple`.
294
+
295
+ Parameters
296
+ ----------
297
+ upper_padding: int, default 0
298
+ How many newlines to prepend to the message.
299
+
300
+ lower_padding: int, default 0
301
+ How many newlines to append to the message.
302
+
303
+ left_padding: int, default 1
304
+ How mant spaces to preprend to the message.
305
+
250
306
  calm: bool, default False
251
307
  If `True`, use the default emoji and color scheme.
252
308
  """
@@ -261,46 +317,35 @@ def print_tuple(
261
317
  if calm:
262
318
  status += '_calm'
263
319
 
264
- omit_messages = STATIC_CONFIG['system']['success']['ignore']
320
+ ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
321
+ from meerschaum.config import get_config
322
+ status_config = get_config('formatting', status, patch=True)
265
323
 
266
- do_print = True
324
+ msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
325
+ lines = msg.split('\n')
326
+ lines = [lines[0]] + [
327
+ ((' ' + line if not line.startswith(' ') else line))
328
+ for line in lines[1:]
329
+ ]
330
+ if ANSI:
331
+ lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
267
332
 
268
- if common_only:
269
- skip_common = False
270
- do_print = tup[1] in omit_messages
271
-
272
- if skip_common:
273
- do_print = tup[1] not in omit_messages
274
-
275
- if do_print:
276
- ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
277
- from meerschaum.config import get_config
278
- status_config = get_config('formatting', status, patch=True)
279
-
280
- msg = ' ' + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
281
- lines = msg.split('\n')
282
- lines = [lines[0]] + [
283
- ((' ' + line if not line.startswith(' ') else line))
284
- for line in lines[1:]
285
- ]
286
- if ANSI:
287
- lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
288
- msg = '\n'.join(lines)
289
- msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
290
- print(msg)
333
+ msg = '\n'.join(lines)
334
+ msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
335
+ return msg
291
336
 
292
337
 
293
338
  def print_options(
294
- options: Optional[Dict[str, Any]] = None,
295
- nopretty: bool = False,
296
- no_rich: bool = False,
297
- name: str = 'options',
298
- header: Optional[str] = None,
299
- num_cols: Optional[int] = None,
300
- adjust_cols: bool = True,
301
- sort_options: bool = False,
302
- **kw
303
- ) -> None:
339
+ options: Optional[Dict[str, Any]] = None,
340
+ nopretty: bool = False,
341
+ no_rich: bool = False,
342
+ name: str = 'options',
343
+ header: Optional[str] = None,
344
+ num_cols: Optional[int] = None,
345
+ adjust_cols: bool = True,
346
+ sort_options: bool = False,
347
+ **kw
348
+ ) -> None:
304
349
  """
305
350
  Print items in an iterable as a fancy table.
306
351
 
@@ -342,7 +387,7 @@ def print_options(
342
387
  _options.append(str(o))
343
388
  if sort_options:
344
389
  _options = sorted(_options)
345
- _header = f"Available {name}" if header is None else header
390
+ _header = f"\nAvailable {name}" if header is None else header
346
391
 
347
392
  if num_cols is None:
348
393
  num_cols = 8
@@ -388,7 +433,7 @@ def print_options(
388
433
 
389
434
  if _header is not None:
390
435
  table = Table(
391
- title = ('\n' + _header) if header else header,
436
+ title = _header,
392
437
  box = box.SIMPLE,
393
438
  show_header = False,
394
439
  show_footer = False,
@@ -467,6 +512,7 @@ def fill_ansi(string: str, style: str = '') -> str:
467
512
 
468
513
  return rich_text_to_str(msg)
469
514
 
515
+
470
516
  def __getattr__(name: str) -> str:
471
517
  """
472
518
  Lazily load module-level variables.
@@ -7,7 +7,8 @@ Print jobs information.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import List, Optional, Any
10
+ import meerschaum as mrsm
11
+ from meerschaum.utils.typing import List, Optional, Any, is_success_tuple
11
12
  from meerschaum.utils.daemon import (
12
13
  Daemon,
13
14
  get_daemons,
@@ -17,9 +18,9 @@ from meerschaum.utils.daemon import (
17
18
  )
18
19
 
19
20
  def pprint_jobs(
20
- daemons: List[Daemon],
21
- nopretty: bool = False,
22
- ):
21
+ daemons: List[Daemon],
22
+ nopretty: bool = False,
23
+ ):
23
24
  """Pretty-print a list of Daemons."""
24
25
  from meerschaum.utils.formatting import make_header
25
26
 
@@ -48,10 +49,12 @@ def pprint_jobs(
48
49
  pprint_job(d, nopretty=nopretty)
49
50
 
50
51
  def _pretty_print():
51
- from meerschaum.utils.formatting import get_console, UNICODE, ANSI
52
+ from meerschaum.utils.formatting import get_console, UNICODE, ANSI, format_success_tuple
52
53
  from meerschaum.utils.packages import import_rich, attempt_import
53
54
  rich = import_rich()
54
- rich_table, rich_text, rich_box = attempt_import('rich.table', 'rich.text', 'rich.box')
55
+ rich_table, rich_text, rich_box, rich_json, rich_panel, rich_console = attempt_import(
56
+ 'rich.table', 'rich.text', 'rich.box', 'rich.json', 'rich.panel', 'rich.console',
57
+ )
55
58
  table = rich_table.Table(
56
59
  title = rich_text.Text('Jobs'),
57
60
  box = (rich_box.ROUNDED if UNICODE else rich_box.ASCII),
@@ -62,13 +65,42 @@ def pprint_jobs(
62
65
  table.add_column("Command")
63
66
  table.add_column("Status")
64
67
 
68
+ def get_success_text(d):
69
+ success_tuple = d.properties.get('result', None)
70
+ if isinstance(success_tuple, list):
71
+ success_tuple = tuple(success_tuple)
72
+ if not is_success_tuple(success_tuple):
73
+ return rich_text.Text('')
74
+
75
+ success = success_tuple[0]
76
+ msg = success_tuple[1]
77
+ lines = msg.split('\n')
78
+ msg = '\n'.join(line.lstrip().rstrip() for line in lines)
79
+ success_tuple = success, msg
80
+ success_tuple_str = (
81
+ format_success_tuple(success_tuple, left_padding=1)
82
+ if success_tuple is not None
83
+ else None
84
+ )
85
+ success_tuple_text = (
86
+ rich_text.Text.from_ansi(success_tuple_str)
87
+ ) if success_tuple_str is not None else None
88
+
89
+ if success_tuple_text is None:
90
+ return rich_text.Text('')
91
+
92
+ return rich_text.Text('\n') + success_tuple_text
93
+
94
+
65
95
  for d in running_daemons:
66
96
  if d.hidden:
67
97
  continue
68
98
  table.add_row(
69
99
  d.daemon_id,
70
100
  d.label,
71
- rich_text.Text(d.status, style=('green' if ANSI else ''))
101
+ rich_console.Group(
102
+ rich_text.Text(d.status, style=('green' if ANSI else '')),
103
+ ),
72
104
  )
73
105
 
74
106
  for d in paused_daemons:
@@ -77,16 +109,22 @@ def pprint_jobs(
77
109
  table.add_row(
78
110
  d.daemon_id,
79
111
  d.label,
80
- rich_text.Text(d.status, style=('yellow' if ANSI else ''))
112
+ rich_console.Group(
113
+ rich_text.Text(d.status, style=('yellow' if ANSI else '')),
114
+ ),
81
115
  )
82
116
 
83
117
  for d in stopped_daemons:
84
118
  if d.hidden:
85
119
  continue
120
+
86
121
  table.add_row(
87
122
  d.daemon_id,
88
123
  d.label,
89
- rich_text.Text(d.status, style=('red' if ANSI else ''))
124
+ rich_console.Group(
125
+ rich_text.Text(d.status, style=('red' if ANSI else '')),
126
+ get_success_text(d)
127
+ ),
90
128
  )
91
129
  get_console().print(table)
92
130