meerschaum 2.2.7__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/__init__.py +1 -1
  4. meerschaum/_internal/arguments/_parse_arguments.py +72 -6
  5. meerschaum/_internal/arguments/_parser.py +45 -15
  6. meerschaum/_internal/docs/index.py +265 -8
  7. meerschaum/_internal/entry.py +154 -24
  8. meerschaum/_internal/shell/Shell.py +264 -77
  9. meerschaum/actions/__init__.py +29 -17
  10. meerschaum/actions/api.py +12 -12
  11. meerschaum/actions/attach.py +113 -0
  12. meerschaum/actions/copy.py +68 -41
  13. meerschaum/actions/delete.py +112 -50
  14. meerschaum/actions/edit.py +3 -3
  15. meerschaum/actions/install.py +40 -32
  16. meerschaum/actions/pause.py +44 -27
  17. meerschaum/actions/restart.py +107 -0
  18. meerschaum/actions/show.py +130 -159
  19. meerschaum/actions/start.py +161 -100
  20. meerschaum/actions/stop.py +78 -42
  21. meerschaum/api/_events.py +25 -1
  22. meerschaum/api/_oauth2.py +2 -0
  23. meerschaum/api/_websockets.py +2 -2
  24. meerschaum/api/dash/callbacks/jobs.py +36 -44
  25. meerschaum/api/dash/jobs.py +89 -78
  26. meerschaum/api/routes/__init__.py +1 -0
  27. meerschaum/api/routes/_actions.py +148 -17
  28. meerschaum/api/routes/_jobs.py +407 -0
  29. meerschaum/api/routes/_pipes.py +5 -5
  30. meerschaum/config/_default.py +1 -0
  31. meerschaum/config/_jobs.py +1 -1
  32. meerschaum/config/_paths.py +7 -0
  33. meerschaum/config/_shell.py +8 -3
  34. meerschaum/config/_version.py +1 -1
  35. meerschaum/config/static/__init__.py +17 -0
  36. meerschaum/connectors/Connector.py +13 -7
  37. meerschaum/connectors/__init__.py +28 -15
  38. meerschaum/connectors/api/APIConnector.py +27 -1
  39. meerschaum/connectors/api/_actions.py +71 -6
  40. meerschaum/connectors/api/_jobs.py +368 -0
  41. meerschaum/connectors/api/_pipes.py +85 -84
  42. meerschaum/connectors/parse.py +27 -15
  43. meerschaum/core/Pipe/_bootstrap.py +16 -8
  44. meerschaum/jobs/_Executor.py +69 -0
  45. meerschaum/jobs/_Job.py +899 -0
  46. meerschaum/jobs/__init__.py +396 -0
  47. meerschaum/jobs/systemd.py +694 -0
  48. meerschaum/plugins/__init__.py +97 -12
  49. meerschaum/utils/daemon/Daemon.py +276 -30
  50. meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
  51. meerschaum/utils/daemon/RotatingFile.py +14 -7
  52. meerschaum/utils/daemon/StdinFile.py +121 -0
  53. meerschaum/utils/daemon/__init__.py +15 -7
  54. meerschaum/utils/daemon/_names.py +15 -13
  55. meerschaum/utils/formatting/__init__.py +2 -1
  56. meerschaum/utils/formatting/_jobs.py +115 -62
  57. meerschaum/utils/formatting/_shell.py +6 -0
  58. meerschaum/utils/misc.py +41 -22
  59. meerschaum/utils/packages/_packages.py +9 -6
  60. meerschaum/utils/process.py +9 -9
  61. meerschaum/utils/prompt.py +16 -8
  62. meerschaum/utils/venv/__init__.py +2 -2
  63. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
  64. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/RECORD +70 -61
  65. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
  66. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
  67. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
  68. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
  69. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
  70. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
@@ -112,7 +112,7 @@ class FileDescriptorInterceptor:
112
112
  except OSError as e:
113
113
  if e.errno != FD_CLOSED:
114
114
  warn(
115
- f"Error while trying to close the duplicated file descriptor:\n"
115
+ "Error while trying to close the duplicated file descriptor:\n"
116
116
  + f"{traceback.format_exc()}"
117
117
  )
118
118
 
@@ -121,7 +121,7 @@ class FileDescriptorInterceptor:
121
121
  except OSError as e:
122
122
  if e.errno != FD_CLOSED:
123
123
  warn(
124
- f"Error while trying to close the write-pipe "
124
+ "Error while trying to close the write-pipe "
125
125
  + "to the intercepted file descriptor:\n"
126
126
  + f"{traceback.format_exc()}"
127
127
  )
@@ -130,7 +130,7 @@ class FileDescriptorInterceptor:
130
130
  except OSError as e:
131
131
  if e.errno != FD_CLOSED:
132
132
  warn(
133
- f"Error while trying to close the read-pipe "
133
+ "Error while trying to close the read-pipe "
134
134
  + "to the intercepted file descriptor:\n"
135
135
  + f"{traceback.format_exc()}"
136
136
  )
@@ -140,7 +140,7 @@ class FileDescriptorInterceptor:
140
140
  except OSError as e:
141
141
  if e.errno != FD_CLOSED:
142
142
  warn(
143
- f"Error while trying to close the signal-read-pipe "
143
+ "Error while trying to close the signal-read-pipe "
144
144
  + "to the intercepted file descriptor:\n"
145
145
  + f"{traceback.format_exc()}"
146
146
  )
@@ -150,7 +150,7 @@ class FileDescriptorInterceptor:
150
150
  except OSError as e:
151
151
  if e.errno != FD_CLOSED:
152
152
  warn(
153
- f"Error while trying to close the signal-write-pipe "
153
+ "Error while trying to close the signal-write-pipe "
154
154
  + "to the intercepted file descriptor:\n"
155
155
  + f"{traceback.format_exc()}"
156
156
  )
@@ -38,7 +38,7 @@ class RotatingFile(io.IOBase):
38
38
  max_file_size: Optional[int] = None,
39
39
  redirect_streams: bool = False,
40
40
  write_timestamps: bool = False,
41
- timestamp_format: str = '%Y-%m-%d %H:%M',
41
+ timestamp_format: Optional[str] = None,
42
42
  ):
43
43
  """
44
44
  Create a file-like object which manages other files.
@@ -63,12 +63,18 @@ class RotatingFile(io.IOBase):
63
63
 
64
64
  write_timestamps: bool, default False
65
65
  If `True`, prepend the current UTC timestamp to each line of the file.
66
+
67
+ timestamp_format: str, default None
68
+ If `write_timestamps` is `True`, use this format for the timestamps.
69
+ Defaults to `'%Y-%m-%d %H:%M'`.
66
70
  """
67
71
  self.file_path = pathlib.Path(file_path)
68
72
  if num_files_to_keep is None:
69
73
  num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
70
74
  if max_file_size is None:
71
75
  max_file_size = get_config('jobs', 'logs', 'max_file_size')
76
+ if timestamp_format is None:
77
+ timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
72
78
  if num_files_to_keep < 2:
73
79
  raise ValueError("At least 2 files must be kept.")
74
80
  if max_file_size < 1:
@@ -232,10 +238,10 @@ class RotatingFile(io.IOBase):
232
238
 
233
239
 
234
240
  def refresh_files(
235
- self,
236
- potential_new_len: int = 0,
237
- start_interception: bool = False,
238
- ) -> '_io.TextUIWrapper':
241
+ self,
242
+ potential_new_len: int = 0,
243
+ start_interception: bool = False,
244
+ ) -> '_io.TextUIWrapper':
239
245
  """
240
246
  Check the state of the subfiles.
241
247
  If the latest subfile is too large, create a new file and delete old ones.
@@ -339,7 +345,7 @@ class RotatingFile(io.IOBase):
339
345
 
340
346
  def get_timestamp_prefix_str(self) -> str:
341
347
  """
342
- Return the current minute prefixm string.
348
+ Return the current minute prefix string.
343
349
  """
344
350
  return datetime.now(timezone.utc).strftime(self.timestamp_format) + ' | '
345
351
 
@@ -568,7 +574,8 @@ class RotatingFile(io.IOBase):
568
574
  return
569
575
 
570
576
  self._cursor = (max_ix, position)
571
- self._current_file_obj.seek(position)
577
+ if self._current_file_obj is not None:
578
+ self._current_file_obj.seek(position)
572
579
 
573
580
 
574
581
  def flush(self) -> None:
@@ -0,0 +1,121 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Create a file manager to pass STDIN to the Daemon.
7
+ """
8
+
9
+ import io
10
+ import pathlib
11
+ import time
12
+ import os
13
+ import selectors
14
+ import traceback
15
+
16
+ from meerschaum.utils.typing import Optional, Union
17
+ from meerschaum.utils.warnings import warn
18
+
19
+
20
+ class StdinFile(io.TextIOBase):
21
+ """
22
+ Redirect user input into a Daemon's context.
23
+ """
24
+ def __init__(
25
+ self,
26
+ file_path: Union[pathlib.Path, str],
27
+ lock_file_path: Optional[pathlib.Path] = None,
28
+ ):
29
+ if isinstance(file_path, str):
30
+ file_path = pathlib.Path(file_path)
31
+
32
+ self.file_path = file_path
33
+ self.blocking_file_path = (
34
+ lock_file_path
35
+ if lock_file_path is not None
36
+ else (file_path.parent / (file_path.name + '.block'))
37
+ )
38
+ self._file_handler = None
39
+ self._fd = None
40
+ self.sel = selectors.DefaultSelector()
41
+
42
+ @property
43
+ def file_handler(self):
44
+ """
45
+ Return the read file handler to the provided file path.
46
+ """
47
+ if self._file_handler is not None:
48
+ return self._file_handler
49
+
50
+ if self.file_path.exists():
51
+ self.file_path.unlink()
52
+
53
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
54
+ os.mkfifo(self.file_path.as_posix(), mode=0o600)
55
+
56
+ self._fd = os.open(self.file_path, os.O_RDONLY | os.O_NONBLOCK)
57
+ self._file_handler = os.fdopen(self._fd, 'rb', buffering=0)
58
+ self.sel.register(self._file_handler, selectors.EVENT_READ)
59
+ return self._file_handler
60
+
61
+ def write(self, data):
62
+ if isinstance(data, str):
63
+ data = data.encode('utf-8')
64
+
65
+ with open(self.file_path, 'wb') as f:
66
+ f.write(data)
67
+
68
+ def fileno(self):
69
+ fileno = self.file_handler.fileno()
70
+ return fileno
71
+
72
+ def read(self, size=-1):
73
+ """
74
+ Read from the FIFO pipe, blocking on EOFError.
75
+ """
76
+ _ = self.file_handler
77
+ while True:
78
+ try:
79
+ data = self._file_handler.read(size)
80
+ if data:
81
+ try:
82
+ if self.blocking_file_path.exists():
83
+ self.blocking_file_path.unlink()
84
+ except Exception:
85
+ warn(traceback.format_exc())
86
+ return data.decode('utf-8')
87
+ except (OSError, EOFError):
88
+ pass
89
+
90
+ self.blocking_file_path.touch()
91
+ time.sleep(0.1)
92
+
93
+ def readline(self, size=-1):
94
+ line = ''
95
+ while True:
96
+ data = self.read(1)
97
+ if not data or data == '\n':
98
+ break
99
+ line += data
100
+
101
+ return line
102
+
103
+ def close(self):
104
+ if self._file_handler is not None:
105
+ self.sel.unregister(self._file_handler)
106
+ self._file_handler.close()
107
+ os.close(self._fd)
108
+ self._file_handler = None
109
+ self._fd = None
110
+
111
+ super().close()
112
+
113
+ def is_open(self):
114
+ return self._file_handler is not None
115
+
116
+
117
+ def __str__(self) -> str:
118
+ return f"StdinFile('{self.file_path}')"
119
+
120
+ def __repr__(self) -> str:
121
+ return str(self)
@@ -10,9 +10,11 @@ from __future__ import annotations
10
10
  import os, pathlib, shutil, json, datetime, threading, shlex
11
11
  from meerschaum.utils.typing import SuccessTuple, List, Optional, Callable, Any, Dict
12
12
  from meerschaum.config._paths import DAEMON_RESOURCES_PATH
13
+ from meerschaum.utils.daemon.StdinFile import StdinFile
13
14
  from meerschaum.utils.daemon.Daemon import Daemon
14
15
  from meerschaum.utils.daemon.RotatingFile import RotatingFile
15
16
  from meerschaum.utils.daemon.FileDescriptorInterceptor import FileDescriptorInterceptor
17
+ from meerschaum.utils.daemon._names import get_new_daemon_name
16
18
 
17
19
 
18
20
  def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
@@ -76,7 +78,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
76
78
  filtered_sysargs,
77
79
  daemon_id=_args.get('name', None) if _args else None,
78
80
  label=label,
79
- keep_daemon_output=('--rm' not in sysargs),
81
+ keep_daemon_output=('--rm' not in (sysargs or [])),
80
82
  )
81
83
  return success_tuple
82
84
 
@@ -181,7 +183,11 @@ def get_daemon_ids() -> List[str]:
181
183
  """
182
184
  Return the IDs of all daemons on disk.
183
185
  """
184
- return sorted(os.listdir(DAEMON_RESOURCES_PATH))
186
+ return [
187
+ daemon_dir
188
+ for daemon_dir in sorted(os.listdir(DAEMON_RESOURCES_PATH))
189
+ if (DAEMON_RESOURCES_PATH / daemon_dir / 'properties.json').exists()
190
+ ]
185
191
 
186
192
 
187
193
  def get_running_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
@@ -225,10 +231,11 @@ def get_stopped_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
225
231
 
226
232
 
227
233
  def get_filtered_daemons(
228
- filter_list: Optional[List[str]] = None,
229
- warn: bool = False,
230
- ) -> List[Daemon]:
231
- """Return a list of `Daemons` filtered by a list of `daemon_ids`.
234
+ filter_list: Optional[List[str]] = None,
235
+ warn: bool = False,
236
+ ) -> List[Daemon]:
237
+ """
238
+ Return a list of `Daemons` filtered by a list of `daemon_ids`.
232
239
  Only `Daemons` that exist are returned.
233
240
 
234
241
  If `filter_list` is `None` or empty, return all `Daemons` (from `get_daemons()`).
@@ -250,13 +257,14 @@ def get_filtered_daemons(
250
257
  if not filter_list:
251
258
  daemons = get_daemons()
252
259
  return [d for d in daemons if not d.hidden]
260
+
253
261
  from meerschaum.utils.warnings import warn as _warn
254
262
  daemons = []
255
263
  for d_id in filter_list:
256
264
  try:
257
265
  d = Daemon(daemon_id=d_id)
258
266
  _exists = d.path.exists()
259
- except Exception as e:
267
+ except Exception:
260
268
  _exists = False
261
269
  if not _exists:
262
270
  if warn:
@@ -18,7 +18,8 @@ _bank: Dict[str, Dict[str, List[str]]] = {
18
18
  'bright', 'dark', 'neon',
19
19
  ],
20
20
  'sizes': [
21
- 'big', 'small', 'large', 'huge', 'tiny', 'long', 'short', 'grand', 'mini', 'micro'
21
+ 'big', 'small', 'large', 'huge', 'tiny', 'long', 'short', 'average', 'mini', 'micro',
22
+ 'maximum', 'minimum', 'median',
22
23
  ],
23
24
  'personalities': [
24
25
  'groovy', 'cool', 'awesome', 'nice', 'fantastic', 'sweet', 'great', 'amazing',
@@ -26,29 +27,36 @@ _bank: Dict[str, Dict[str, List[str]]] = {
26
27
  ],
27
28
  'emotions': [
28
29
  'angry', 'happy', 'excited', 'suspicious', 'sad', 'thankful', 'grateful', 'satisfied',
30
+ 'peaceful', 'ferocious', 'content',
29
31
  ],
30
32
  'sensations': [
31
33
  'sleepy', 'awake', 'alert', 'thirsty', 'comfy', 'warm', 'cold', 'chilly', 'soft',
32
- 'smooth', 'chunky',
34
+ 'smooth', 'chunky', 'hungry',
33
35
  ],
34
36
  'materials': [
35
37
  'golden', 'silver', 'metal', 'plastic', 'wool', 'wooden', 'nylon', 'fuzzy', 'silky',
38
+ 'suede', 'vinyl',
36
39
  ],
37
40
  'qualities': [
38
41
  'expensive', 'cheap', 'premier', 'best', 'favorite', 'better', 'good', 'affordable',
42
+ 'organic', 'electric',
39
43
  ],
40
44
  },
41
45
  'nouns' : {
42
46
  'animals': [
43
- 'mouse', 'fox', 'horse', 'dragon', 'pig', 'hippo', 'elephant' , 'tiger', 'deer',
47
+ 'mouse', 'fox', 'horse', 'pig', 'hippo', 'elephant' , 'tiger', 'deer', 'salmon',
44
48
  'gerbil', 'snake', 'turtle', 'rhino', 'dog', 'cat', 'giraffe', 'rabbit', 'squirrel',
45
49
  'unicorn', 'lizard', 'lion', 'bear', 'gazelle', 'whale', 'dolphin', 'fish', 'butterfly',
46
50
  'ladybug', 'fly', 'shrimp', 'flamingo', 'parrot', 'tuna', 'panda', 'lemur', 'duck',
47
51
  'seal', 'walrus', 'seagull', 'iguana', 'salamander', 'kitten', 'puppy', 'octopus',
48
52
  ],
53
+ 'weather': [
54
+ 'rain', 'sun', 'snow', 'wind', 'tornado', 'hurricane', 'blizzard', 'monsoon', 'storm',
55
+ 'shower', 'hail',
56
+ ],
49
57
  'plants': [
50
58
  'tree', 'flower', 'vine', 'fern', 'palm', 'palmetto', 'oak', 'pine', 'rose', 'lily',
51
- 'ivy',
59
+ 'ivy', 'leaf', 'shrubbery', 'acorn', 'fruit',
52
60
  ],
53
61
  'foods': [
54
62
  'pizza', 'sushi', 'apple', 'banana', 'sandwich', 'burger', 'taco', 'bratwurst',
@@ -74,8 +82,6 @@ _bank: Dict[str, Dict[str, List[str]]] = {
74
82
  },
75
83
  }
76
84
 
77
- _disallow_combinations: List[Tuple[str, str]] = []
78
-
79
85
  _adjectives: List[str]= []
80
86
  for category, items in _bank['adjectives'].items():
81
87
  _adjectives += items
@@ -84,7 +90,7 @@ _nouns: List[str] = []
84
90
  for category, items in _bank['nouns'].items():
85
91
  _nouns += items
86
92
 
87
- def generate_random_name(separator: str = '_'):
93
+ def generate_random_name(separator: str = '-'):
88
94
  """
89
95
  Return a random adjective and noun combination.
90
96
 
@@ -96,12 +102,8 @@ def generate_random_name(separator: str = '_'):
96
102
  -------
97
103
  A string containing an random adjective and random noun.
98
104
  """
99
- while True:
100
- adjective_category = random.choice(list(_bank['adjectives'].keys()))
101
- noun_category = random.choice(list(_bank['nouns'].keys()))
102
- if (adjective_category, noun_category) in _disallow_combinations:
103
- continue
104
- break
105
+ adjective_category = random.choice(list(_bank['adjectives'].keys()))
106
+ noun_category = random.choice(list(_bank['nouns'].keys()))
105
107
  return (
106
108
  random.choice(_bank['adjectives'][adjective_category])
107
109
  + separator
@@ -28,9 +28,10 @@ _attrs = {
28
28
  'ANSI': None,
29
29
  'UNICODE': None,
30
30
  'CHARSET': None,
31
+ 'RESET': '\033[0m',
31
32
  }
32
33
  __all__ = sorted([
33
- 'ANSI', 'CHARSET', 'UNICODE',
34
+ 'ANSI', 'CHARSET', 'UNICODE', 'RESET',
34
35
  'colored',
35
36
  'translate_rich_to_termcolor',
36
37
  'get_console',
@@ -7,46 +7,59 @@ Print jobs information.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
11
+ from datetime import datetime, timezone
12
+
10
13
  import meerschaum as mrsm
11
- from meerschaum.utils.typing import List, Optional, Any, is_success_tuple
12
- from meerschaum.utils.daemon import (
13
- Daemon,
14
- get_daemons,
15
- get_running_daemons,
16
- get_stopped_daemons,
17
- get_paused_daemons,
14
+ from meerschaum.utils.typing import List, Optional, Any, is_success_tuple, Dict
15
+ from meerschaum.jobs import (
16
+ Job,
17
+ get_jobs,
18
+ get_running_jobs,
19
+ get_stopped_jobs,
20
+ get_paused_jobs,
21
+ get_executor_keys_from_context,
18
22
  )
23
+ from meerschaum.config import get_config
24
+
19
25
 
20
26
  def pprint_jobs(
21
- daemons: List[Daemon],
27
+ jobs: Dict[str, Job],
22
28
  nopretty: bool = False,
23
29
  ):
24
30
  """Pretty-print a list of Daemons."""
25
31
  from meerschaum.utils.formatting import make_header
32
+ from meerschaum.utils.misc import items_str
26
33
 
27
- running_daemons = get_running_daemons(daemons)
28
- paused_daemons = get_paused_daemons(daemons)
29
- stopped_daemons = get_stopped_daemons(daemons)
34
+ running_jobs = get_running_jobs(jobs=jobs)
35
+ paused_jobs = get_paused_jobs(jobs=jobs)
36
+ stopped_jobs = get_stopped_jobs(jobs=jobs)
37
+ executor_keys_list = list(set(
38
+ [
39
+ job.executor_keys.replace('systemd:main', 'systemd')
40
+ for job in jobs.values()
41
+ ]
42
+ )) if jobs else [get_executor_keys_from_context()]
30
43
 
31
44
  def _nopretty_print():
32
45
  from meerschaum.utils.misc import print_options
33
- if running_daemons:
46
+ if running_jobs:
34
47
  if not nopretty:
35
48
  print('\n' + make_header('Running jobs'))
36
- for d in running_daemons:
37
- pprint_job(d, nopretty=nopretty)
49
+ for name, job in running_jobs.items():
50
+ pprint_job(job, nopretty=nopretty)
38
51
 
39
- if paused_daemons:
52
+ if paused_jobs:
40
53
  if not nopretty:
41
54
  print('\n' + make_header('Paused jobs'))
42
- for d in paused_daemons:
43
- pprint_job(d, nopretty=nopretty)
55
+ for name, job in paused_jobs.items():
56
+ pprint_job(job, nopretty=nopretty)
44
57
 
45
- if stopped_daemons:
58
+ if stopped_jobs:
46
59
  if not nopretty:
47
60
  print('\n' + make_header('Stopped jobs'))
48
- for d in stopped_daemons:
49
- pprint_job(d, nopretty=nopretty)
61
+ for name, job in stopped_jobs.items():
62
+ pprint_job(job, nopretty=nopretty)
50
63
 
51
64
  def _pretty_print():
52
65
  from meerschaum.utils.formatting import get_console, UNICODE, ANSI, format_success_tuple
@@ -56,19 +69,25 @@ def pprint_jobs(
56
69
  'rich.table', 'rich.text', 'rich.box', 'rich.json', 'rich.panel', 'rich.console',
57
70
  )
58
71
  table = rich_table.Table(
59
- title = rich_text.Text('Jobs'),
60
- box = (rich_box.ROUNDED if UNICODE else rich_box.ASCII),
61
- show_lines = True,
62
- show_header = ANSI,
72
+ title=rich_text.Text(
73
+ f"\nJobs on Executor"
74
+ + ('s' if len(executor_keys_list) != 1 else '')
75
+ + f" {items_str(executor_keys_list)}"
76
+ ),
77
+ box=(rich_box.ROUNDED if UNICODE else rich_box.ASCII),
78
+ show_lines=True,
79
+ show_header=ANSI,
63
80
  )
64
81
  table.add_column("Name", justify='right', style=('magenta' if ANSI else ''))
82
+ table.add_column(
83
+ "Executor",
84
+ style=(get_config('shell', 'ansi', 'executor', 'rich', 'style') if ANSI else ''),
85
+ )
65
86
  table.add_column("Command")
66
87
  table.add_column("Status")
67
88
 
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)
89
+ def get_success_text(job):
90
+ success_tuple = job.result
72
91
  if not is_success_tuple(success_tuple):
73
92
  return rich_text.Text('')
74
93
 
@@ -92,39 +111,56 @@ def pprint_jobs(
92
111
  return rich_text.Text('\n') + success_tuple_text
93
112
 
94
113
 
95
- for d in running_daemons:
96
- if d.hidden:
97
- continue
98
- table.add_row(
99
- d.daemon_id,
100
- d.label,
101
- rich_console.Group(
102
- rich_text.Text(d.status, style=('green' if ANSI else '')),
114
+ for name, job in running_jobs.items():
115
+ status_group = [
116
+ (
117
+ rich_text.Text(job.status, style=('green' if ANSI else ''))
118
+ if not job.is_blocking_on_stdin()
119
+ else rich_text.Text('waiting for input', style=('yellow' if ANSI else ''))
103
120
  ),
121
+ ] + ([rich_text.Text(f"PID: {pid}")] if (pid := job.pid) else [])
122
+
123
+ if job.restart:
124
+ status_group.append(rich_text.Text('(restarts)'))
125
+
126
+ table.add_row(
127
+ job.name,
128
+ job.executor_keys.replace('systemd:main', 'systemd'),
129
+ job.label,
130
+ rich_console.Group(*status_group),
104
131
  )
105
132
 
106
- for d in paused_daemons:
107
- if d.hidden:
108
- continue
133
+ for name, job in paused_jobs.items():
134
+ status_group = [
135
+ rich_text.Text(job.status, style=('yellow' if ANSI else '')),
136
+ ]
137
+ if job.restart:
138
+ status_group.append(rich_text.Text('(restarts)'))
139
+
109
140
  table.add_row(
110
- d.daemon_id,
111
- d.label,
112
- rich_console.Group(
113
- rich_text.Text(d.status, style=('yellow' if ANSI else '')),
114
- ),
141
+ job.name,
142
+ job.executor_keys.replace('systemd:main', 'systemd'),
143
+ job.label,
144
+ rich_console.Group(*status_group),
115
145
  )
116
146
 
117
- for d in stopped_daemons:
118
- if d.hidden:
119
- continue
147
+ for name, job in stopped_jobs.items():
148
+ status_group = [
149
+ rich_text.Text(job.status, style=('red' if ANSI else '')),
150
+ ]
151
+ if job.restart:
152
+ if job.stop_time is None:
153
+ status_group.append(rich_text.Text('(restarts)'))
154
+ else:
155
+ status_group.append(rich_text.Text('(start to resume restarts)'))
156
+
157
+ status_group.append(get_success_text(job))
120
158
 
121
159
  table.add_row(
122
- d.daemon_id,
123
- d.label,
124
- rich_console.Group(
125
- rich_text.Text(d.status, style=('red' if ANSI else '')),
126
- get_success_text(d)
127
- ),
160
+ job.name,
161
+ job.executor_keys.replace('systemd:main', 'systemd'),
162
+ job.label,
163
+ rich_console.Group(*status_group),
128
164
  )
129
165
  get_console().print(table)
130
166
 
@@ -133,15 +169,32 @@ def pprint_jobs(
133
169
 
134
170
 
135
171
  def pprint_job(
136
- daemon: Daemon,
137
- nopretty: bool = False,
138
- ):
139
- """Pretty-print a single Daemon."""
140
- if daemon.hidden:
141
- return
172
+ job: Job,
173
+ nopretty: bool = False,
174
+ ):
175
+ """Pretty-print a single `Job`."""
142
176
  from meerschaum.utils.warnings import info
143
177
  if not nopretty:
144
- info(f"Command for job '{daemon.daemon_id}':")
145
- print('\n' + daemon.label + '\n')
178
+ info(f"Command for job '{job.name}':")
179
+ print('\n' + job.label + '\n')
146
180
  else:
147
- print(daemon.daemon_id)
181
+ print(job.name)
182
+
183
+
184
+ def strip_timestamp_from_line(line: str) -> str:
185
+ """
186
+ Remove the leading timestamp from a job's line (if present).
187
+ """
188
+ now = datetime.now(timezone.utc)
189
+ timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
190
+ now_str = now.strftime(timestamp_format)
191
+
192
+ date_prefix_str = line[:len(now_str)]
193
+ try:
194
+ line_timestamp = datetime.strptime(date_prefix_str, timestamp_format)
195
+ except Exception:
196
+ line_timestamp = None
197
+ if line_timestamp:
198
+ line = line[(len(now_str) + 3):]
199
+
200
+ return line
@@ -47,9 +47,15 @@ def clear_screen(debug: bool = False) -> bool:
47
47
  from meerschaum.utils.formatting import ANSI, get_console
48
48
  from meerschaum.utils.debug import dprint
49
49
  from meerschaum.config import get_config
50
+ from meerschaum.utils.daemon import running_in_daemon
50
51
  global _tried_clear_command
52
+
53
+ if running_in_daemon():
54
+ return True
55
+
51
56
  if not get_config('shell', 'clear_screen'):
52
57
  return True
58
+
53
59
  print("", end="", flush=True)
54
60
  if debug:
55
61
  dprint("Skipping screen clear.")