meerschaum 2.2.6__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 (80) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/__main__.py +9 -9
  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 +167 -37
  8. meerschaum/_internal/shell/Shell.py +290 -99
  9. meerschaum/_internal/shell/updates.py +175 -0
  10. meerschaum/actions/__init__.py +29 -17
  11. meerschaum/actions/api.py +12 -12
  12. meerschaum/actions/attach.py +113 -0
  13. meerschaum/actions/copy.py +68 -41
  14. meerschaum/actions/delete.py +112 -50
  15. meerschaum/actions/edit.py +3 -3
  16. meerschaum/actions/install.py +40 -32
  17. meerschaum/actions/pause.py +44 -27
  18. meerschaum/actions/register.py +19 -5
  19. meerschaum/actions/restart.py +107 -0
  20. meerschaum/actions/show.py +130 -159
  21. meerschaum/actions/start.py +161 -100
  22. meerschaum/actions/stop.py +78 -42
  23. meerschaum/actions/sync.py +3 -3
  24. meerschaum/actions/upgrade.py +28 -36
  25. meerschaum/api/_events.py +25 -1
  26. meerschaum/api/_oauth2.py +2 -0
  27. meerschaum/api/_websockets.py +2 -2
  28. meerschaum/api/dash/callbacks/jobs.py +36 -44
  29. meerschaum/api/dash/jobs.py +89 -78
  30. meerschaum/api/routes/__init__.py +1 -0
  31. meerschaum/api/routes/_actions.py +148 -17
  32. meerschaum/api/routes/_jobs.py +407 -0
  33. meerschaum/api/routes/_pipes.py +25 -25
  34. meerschaum/config/_default.py +1 -0
  35. meerschaum/config/_formatting.py +1 -0
  36. meerschaum/config/_jobs.py +1 -1
  37. meerschaum/config/_paths.py +11 -0
  38. meerschaum/config/_shell.py +84 -67
  39. meerschaum/config/_version.py +1 -1
  40. meerschaum/config/static/__init__.py +18 -0
  41. meerschaum/connectors/Connector.py +13 -7
  42. meerschaum/connectors/__init__.py +28 -15
  43. meerschaum/connectors/api/APIConnector.py +27 -1
  44. meerschaum/connectors/api/_actions.py +71 -6
  45. meerschaum/connectors/api/_jobs.py +368 -0
  46. meerschaum/connectors/api/_misc.py +1 -1
  47. meerschaum/connectors/api/_pipes.py +85 -84
  48. meerschaum/connectors/api/_request.py +13 -9
  49. meerschaum/connectors/parse.py +27 -15
  50. meerschaum/core/Pipe/_bootstrap.py +16 -8
  51. meerschaum/core/Pipe/_sync.py +3 -0
  52. meerschaum/jobs/_Executor.py +69 -0
  53. meerschaum/jobs/_Job.py +899 -0
  54. meerschaum/jobs/__init__.py +396 -0
  55. meerschaum/jobs/systemd.py +694 -0
  56. meerschaum/plugins/__init__.py +97 -12
  57. meerschaum/utils/daemon/Daemon.py +352 -147
  58. meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
  59. meerschaum/utils/daemon/RotatingFile.py +22 -8
  60. meerschaum/utils/daemon/StdinFile.py +121 -0
  61. meerschaum/utils/daemon/__init__.py +42 -27
  62. meerschaum/utils/daemon/_names.py +15 -13
  63. meerschaum/utils/formatting/__init__.py +83 -37
  64. meerschaum/utils/formatting/_jobs.py +146 -55
  65. meerschaum/utils/formatting/_shell.py +6 -0
  66. meerschaum/utils/misc.py +41 -22
  67. meerschaum/utils/packages/__init__.py +21 -15
  68. meerschaum/utils/packages/_packages.py +9 -6
  69. meerschaum/utils/process.py +9 -9
  70. meerschaum/utils/prompt.py +20 -7
  71. meerschaum/utils/schedule.py +21 -15
  72. meerschaum/utils/venv/__init__.py +2 -2
  73. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
  74. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/RECORD +80 -70
  75. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
  76. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
  77. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
  78. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
  79. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
  80. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
@@ -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
@@ -27,12 +28,14 @@ _attrs = {
27
28
  'ANSI': None,
28
29
  'UNICODE': None,
29
30
  'CHARSET': None,
31
+ 'RESET': '\033[0m',
30
32
  }
31
33
  __all__ = sorted([
32
- 'ANSI', 'CHARSET', 'UNICODE',
34
+ 'ANSI', 'CHARSET', 'UNICODE', 'RESET',
33
35
  'colored',
34
36
  'translate_rich_to_termcolor',
35
37
  'get_console',
38
+ 'format_success_tuple',
36
39
  'print_tuple',
37
40
  'print_options',
38
41
  'fill_ansi',
@@ -222,16 +225,17 @@ def get_console():
222
225
 
223
226
 
224
227
  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:
228
+ tup: mrsm.SuccessTuple,
229
+ skip_common: bool = True,
230
+ common_only: bool = False,
231
+ upper_padding: int = 0,
232
+ lower_padding: int = 0,
233
+ left_padding: int = 1,
234
+ calm: bool = False,
235
+ _progress: Optional['rich.progress.Progress'] = None,
236
+ ) -> None:
233
237
  """
234
- Print `meerschaum.utils.typing.SuccessTuple`.
238
+ Format `meerschaum.utils.typing.SuccessTuple`.
235
239
 
236
240
  Parameters
237
241
  ----------
@@ -247,24 +251,18 @@ def print_tuple(
247
251
  lower_padding: int, default 0
248
252
  How many newlines to append to the message.
249
253
 
254
+ left_padding: int, default 1
255
+ How mant spaces to preprend to the message.
256
+
250
257
  calm: bool, default False
251
258
  If `True`, use the default emoji and color scheme.
259
+
252
260
  """
253
261
  from meerschaum.config.static import STATIC_CONFIG
254
- _init()
255
- try:
256
- status = 'success' if tup[0] else 'failure'
257
- except TypeError:
258
- status = 'failure'
259
- tup = None, None
260
-
261
- if calm:
262
- status += '_calm'
262
+ do_print = True
263
263
 
264
264
  omit_messages = STATIC_CONFIG['system']['success']['ignore']
265
265
 
266
- do_print = True
267
-
268
266
  if common_only:
269
267
  skip_common = False
270
268
  do_print = tup[1] in omit_messages
@@ -272,22 +270,70 @@ def print_tuple(
272
270
  if skip_common:
273
271
  do_print = tup[1] not in omit_messages
274
272
 
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)
273
+ if not do_print:
274
+ return
275
+
276
+ print(format_success_tuple(
277
+ tup,
278
+ upper_padding=upper_padding,
279
+ lower_padding=lower_padding,
280
+ calm=calm,
281
+ _progress=_progress,
282
+ ))
283
+
284
+
285
+ def format_success_tuple(
286
+ tup: mrsm.SuccessTuple,
287
+ upper_padding: int = 0,
288
+ lower_padding: int = 0,
289
+ left_padding: int = 1,
290
+ calm: bool = False,
291
+ _progress: Optional['rich.progress.Progress'] = None,
292
+ ) -> str:
293
+ """
294
+ Format `meerschaum.utils.typing.SuccessTuple`.
295
+
296
+ Parameters
297
+ ----------
298
+ upper_padding: int, default 0
299
+ How many newlines to prepend to the message.
300
+
301
+ lower_padding: int, default 0
302
+ How many newlines to append to the message.
303
+
304
+ left_padding: int, default 1
305
+ How mant spaces to preprend to the message.
306
+
307
+ calm: bool, default False
308
+ If `True`, use the default emoji and color scheme.
309
+ """
310
+ from meerschaum.config.static import STATIC_CONFIG
311
+ _init()
312
+ try:
313
+ status = 'success' if tup[0] else 'failure'
314
+ except TypeError:
315
+ status = 'failure'
316
+ tup = None, None
317
+
318
+ if calm:
319
+ status += '_calm'
320
+
321
+ ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
322
+ from meerschaum.config import get_config
323
+ status_config = get_config('formatting', status, patch=True)
324
+
325
+ msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
326
+ lines = msg.split('\n')
327
+ lines = [lines[0]] + [
328
+ ((' ' + line if not line.startswith(' ') else line))
329
+ for line in lines[1:]
330
+ ]
331
+ if ANSI:
332
+ lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
333
+
334
+ msg = '\n'.join(lines)
335
+ msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
336
+ return msg
291
337
 
292
338
 
293
339
  def print_options(
@@ -7,86 +7,160 @@ Print jobs information.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import List, Optional, Any
11
- from meerschaum.utils.daemon import (
12
- Daemon,
13
- get_daemons,
14
- get_running_daemons,
15
- get_stopped_daemons,
16
- get_paused_daemons,
10
+
11
+ from datetime import datetime, timezone
12
+
13
+ import meerschaum as mrsm
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,
17
22
  )
23
+ from meerschaum.config import get_config
24
+
18
25
 
19
26
  def pprint_jobs(
20
- daemons: List[Daemon],
21
- nopretty: bool = False,
22
- ):
27
+ jobs: Dict[str, Job],
28
+ nopretty: bool = False,
29
+ ):
23
30
  """Pretty-print a list of Daemons."""
24
31
  from meerschaum.utils.formatting import make_header
32
+ from meerschaum.utils.misc import items_str
25
33
 
26
- running_daemons = get_running_daemons(daemons)
27
- paused_daemons = get_paused_daemons(daemons)
28
- 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()]
29
43
 
30
44
  def _nopretty_print():
31
45
  from meerschaum.utils.misc import print_options
32
- if running_daemons:
46
+ if running_jobs:
33
47
  if not nopretty:
34
48
  print('\n' + make_header('Running jobs'))
35
- for d in running_daemons:
36
- pprint_job(d, nopretty=nopretty)
49
+ for name, job in running_jobs.items():
50
+ pprint_job(job, nopretty=nopretty)
37
51
 
38
- if paused_daemons:
52
+ if paused_jobs:
39
53
  if not nopretty:
40
54
  print('\n' + make_header('Paused jobs'))
41
- for d in paused_daemons:
42
- pprint_job(d, nopretty=nopretty)
55
+ for name, job in paused_jobs.items():
56
+ pprint_job(job, nopretty=nopretty)
43
57
 
44
- if stopped_daemons:
58
+ if stopped_jobs:
45
59
  if not nopretty:
46
60
  print('\n' + make_header('Stopped jobs'))
47
- for d in stopped_daemons:
48
- pprint_job(d, nopretty=nopretty)
61
+ for name, job in stopped_jobs.items():
62
+ pprint_job(job, nopretty=nopretty)
49
63
 
50
64
  def _pretty_print():
51
- from meerschaum.utils.formatting import get_console, UNICODE, ANSI
65
+ from meerschaum.utils.formatting import get_console, UNICODE, ANSI, format_success_tuple
52
66
  from meerschaum.utils.packages import import_rich, attempt_import
53
67
  rich = import_rich()
54
- rich_table, rich_text, rich_box = attempt_import('rich.table', 'rich.text', 'rich.box')
68
+ rich_table, rich_text, rich_box, rich_json, rich_panel, rich_console = attempt_import(
69
+ 'rich.table', 'rich.text', 'rich.box', 'rich.json', 'rich.panel', 'rich.console',
70
+ )
55
71
  table = rich_table.Table(
56
- title = rich_text.Text('Jobs'),
57
- box = (rich_box.ROUNDED if UNICODE else rich_box.ASCII),
58
- show_lines = True,
59
- 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,
60
80
  )
61
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
+ )
62
86
  table.add_column("Command")
63
87
  table.add_column("Status")
64
88
 
65
- for d in running_daemons:
66
- if d.hidden:
67
- continue
89
+ def get_success_text(job):
90
+ success_tuple = job.result
91
+ if not is_success_tuple(success_tuple):
92
+ return rich_text.Text('')
93
+
94
+ success = success_tuple[0]
95
+ msg = success_tuple[1]
96
+ lines = msg.split('\n')
97
+ msg = '\n'.join(line.lstrip().rstrip() for line in lines)
98
+ success_tuple = success, msg
99
+ success_tuple_str = (
100
+ format_success_tuple(success_tuple, left_padding=1)
101
+ if success_tuple is not None
102
+ else None
103
+ )
104
+ success_tuple_text = (
105
+ rich_text.Text.from_ansi(success_tuple_str)
106
+ ) if success_tuple_str is not None else None
107
+
108
+ if success_tuple_text is None:
109
+ return rich_text.Text('')
110
+
111
+ return rich_text.Text('\n') + success_tuple_text
112
+
113
+
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 ''))
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
+
68
126
  table.add_row(
69
- d.daemon_id,
70
- d.label,
71
- rich_text.Text(d.status, style=('green' if ANSI else ''))
127
+ job.name,
128
+ job.executor_keys.replace('systemd:main', 'systemd'),
129
+ job.label,
130
+ rich_console.Group(*status_group),
72
131
  )
73
132
 
74
- for d in paused_daemons:
75
- if d.hidden:
76
- 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
+
77
140
  table.add_row(
78
- d.daemon_id,
79
- d.label,
80
- rich_text.Text(d.status, style=('yellow' if ANSI else ''))
141
+ job.name,
142
+ job.executor_keys.replace('systemd:main', 'systemd'),
143
+ job.label,
144
+ rich_console.Group(*status_group),
81
145
  )
82
146
 
83
- for d in stopped_daemons:
84
- if d.hidden:
85
- 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))
158
+
86
159
  table.add_row(
87
- d.daemon_id,
88
- d.label,
89
- rich_text.Text(d.status, style=('red' if ANSI else ''))
160
+ job.name,
161
+ job.executor_keys.replace('systemd:main', 'systemd'),
162
+ job.label,
163
+ rich_console.Group(*status_group),
90
164
  )
91
165
  get_console().print(table)
92
166
 
@@ -95,15 +169,32 @@ def pprint_jobs(
95
169
 
96
170
 
97
171
  def pprint_job(
98
- daemon: Daemon,
99
- nopretty: bool = False,
100
- ):
101
- """Pretty-print a single Daemon."""
102
- if daemon.hidden:
103
- return
172
+ job: Job,
173
+ nopretty: bool = False,
174
+ ):
175
+ """Pretty-print a single `Job`."""
104
176
  from meerschaum.utils.warnings import info
105
177
  if not nopretty:
106
- info(f"Command for job '{daemon.daemon_id}':")
107
- print('\n' + daemon.label + '\n')
178
+ info(f"Command for job '{job.name}':")
179
+ print('\n' + job.label + '\n')
108
180
  else:
109
- 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.")
meerschaum/utils/misc.py CHANGED
@@ -875,7 +875,7 @@ def dict_from_od(od: collections.OrderedDict) -> Dict[Any, Any]:
875
875
  _d[k] = dict_from_od(v)
876
876
  return _d
877
877
 
878
- def remove_ansi(s : str) -> str:
878
+ def remove_ansi(s: str) -> str:
879
879
  """
880
880
  Remove ANSI escape characters from a string.
881
881
 
@@ -899,10 +899,11 @@ def remove_ansi(s : str) -> str:
899
899
 
900
900
 
901
901
  def get_connector_labels(
902
- *types: str,
903
- search_term: str = '',
904
- ignore_exact_match = True,
905
- ) -> List[str]:
902
+ *types: str,
903
+ search_term: str = '',
904
+ ignore_exact_match = True,
905
+ _additional_options: Optional[List[str]] = None,
906
+ ) -> List[str]:
906
907
  """
907
908
  Read connector labels from the configuration dictionary.
908
909
 
@@ -941,12 +942,16 @@ def get_connector_labels(
941
942
  continue
942
943
  conns += [ f'{t}:{label}' for label in connectors.get(t, {}) if label != 'default' ]
943
944
 
945
+ if _additional_options:
946
+ conns += _additional_options
947
+
944
948
  possibilities = [
945
- c for c in conns
946
- if c.startswith(search_term)
947
- and c != (
948
- search_term if ignore_exact_match else ''
949
- )
949
+ c
950
+ for c in conns
951
+ if c.startswith(search_term)
952
+ and c != (
953
+ search_term if ignore_exact_match else ''
954
+ )
950
955
  ]
951
956
  return sorted(possibilities)
952
957
 
@@ -1063,7 +1068,7 @@ def async_wrap(func):
1063
1068
  loop = asyncio.get_event_loop()
1064
1069
  pfunc = partial(func, *args, **kwargs)
1065
1070
  return await loop.run_in_executor(executor, pfunc)
1066
- return run
1071
+ return run
1067
1072
 
1068
1073
 
1069
1074
  def debug_trace(browser: bool = True):
@@ -1077,17 +1082,17 @@ def debug_trace(browser: bool = True):
1077
1082
 
1078
1083
 
1079
1084
  def items_str(
1080
- items: List[Any],
1081
- quotes: bool = True,
1082
- quote_str: str = "'",
1083
- commas: bool = True,
1084
- comma_str: str = ',',
1085
- and_: bool = True,
1086
- and_str: str = 'and',
1087
- oxford_comma: bool = True,
1088
- spaces: bool = True,
1089
- space_str = ' ',
1090
- ) -> str:
1085
+ items: List[Any],
1086
+ quotes: bool = True,
1087
+ quote_str: str = "'",
1088
+ commas: bool = True,
1089
+ comma_str: str = ',',
1090
+ and_: bool = True,
1091
+ and_str: str = 'and',
1092
+ oxford_comma: bool = True,
1093
+ spaces: bool = True,
1094
+ space_str = ' ',
1095
+ ) -> str:
1091
1096
  """
1092
1097
  Return a formatted string if list items separated by commas.
1093
1098
 
@@ -1218,6 +1223,19 @@ def is_bcp_available() -> bool:
1218
1223
  return has_bcp
1219
1224
 
1220
1225
 
1226
+ def is_systemd_available() -> bool:
1227
+ """Check if running on systemd."""
1228
+ import subprocess
1229
+ try:
1230
+ has_systemctl = subprocess.call(
1231
+ ['systemctl', '-h'],
1232
+ stdout=subprocess.DEVNULL,
1233
+ stderr=subprocess.STDOUT,
1234
+ ) == 0
1235
+ except Exception:
1236
+ has_systemctl = False
1237
+ return has_systemctl
1238
+
1221
1239
  def get_last_n_lines(file_name: str, N: int):
1222
1240
  """
1223
1241
  https://thispointer.com/python-get-last-n-lines-of-a-text-file-like-tail-command/
@@ -1522,6 +1540,7 @@ def safely_extract_tar(tarf: 'file', output_dir: Union[str, 'pathlib.Path']) ->
1522
1540
 
1523
1541
  return safe_extract(tarf, output_dir)
1524
1542
 
1543
+
1525
1544
  ##################
1526
1545
  # Legacy imports #
1527
1546
  ##################
@@ -816,6 +816,7 @@ def pip_install(
816
816
  """
817
817
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
818
818
  from meerschaum.config import get_config
819
+ from meerschaum.config.static import STATIC_CONFIG
819
820
  from meerschaum.utils.warnings import warn
820
821
  from meerschaum.utils.misc import is_android
821
822
  if args is None:
@@ -827,6 +828,11 @@ def pip_install(
827
828
  if check_wheel:
828
829
  have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
829
830
 
831
+ daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
832
+ inside_daemon = daemon_env_var in os.environ
833
+ if inside_daemon:
834
+ silent = True
835
+
830
836
  _args = list(args)
831
837
  have_pip = venv_contains_package('pip', venv=None, debug=debug)
832
838
  try:
@@ -844,16 +850,16 @@ def pip_install(
844
850
  if have_pip and not have_uv_pip and _install_uv_pip and not is_android():
845
851
  if not pip_install(
846
852
  'uv',
847
- venv = None,
848
- debug = debug,
849
- _install_uv_pip = False,
850
- check_update = False,
851
- check_pypi = False,
852
- check_wheel = False,
853
- ):
853
+ venv=None,
854
+ debug=debug,
855
+ _install_uv_pip=False,
856
+ check_update=False,
857
+ check_pypi=False,
858
+ check_wheel=False,
859
+ ) and not silent:
854
860
  warn(
855
861
  f"Failed to install `uv` for virtual environment '{venv}'.",
856
- color = False,
862
+ color=False,
857
863
  )
858
864
 
859
865
  use_uv_pip = (
@@ -909,13 +915,13 @@ def pip_install(
909
915
  check_wheel = False,
910
916
  debug = debug,
911
917
  _install_uv_pip = False,
912
- ):
918
+ ) and not silent:
913
919
  warn(
914
920
  (
915
921
  "Failed to install `setuptools`, `wheel`, and `uv` for virtual "
916
922
  + f"environment '{venv}'."
917
923
  ),
918
- color = False,
924
+ color=False,
919
925
  )
920
926
 
921
927
  if requirements_file_path is not None:
@@ -975,7 +981,7 @@ def pip_install(
975
981
  if not completely_uninstall_package(
976
982
  _install_no_version,
977
983
  venv=venv, debug=debug,
978
- ):
984
+ ) and not silent:
979
985
  warn(
980
986
  f"Failed to clean up package '{_install_no_version}'.",
981
987
  )
@@ -989,9 +995,9 @@ def pip_install(
989
995
  rc = run_python_package(
990
996
  ('pip' if not use_uv_pip else 'uv'),
991
997
  _args + _packages,
992
- venv = None,
993
- env = _get_pip_os_env(color=color),
994
- debug = debug,
998
+ venv=None,
999
+ env=_get_pip_os_env(color=color),
1000
+ debug=debug,
995
1001
  )
996
1002
  if debug:
997
1003
  print(f"{rc=}")
@@ -1003,7 +1009,7 @@ def pip_install(
1003
1009
  )
1004
1010
  if not silent:
1005
1011
  print(msg)
1006
- if debug:
1012
+ if debug and not silent:
1007
1013
  print('pip ' + ('un' if _uninstall else '') + 'install returned:', success)
1008
1014
  return success
1009
1015
 
@@ -47,18 +47,20 @@ packages: Dict[str, Dict[str, str]] = {
47
47
  'packaging' : 'packaging>=21.3.0',
48
48
  'prompt_toolkit' : 'prompt-toolkit>=3.0.39',
49
49
  'more_itertools' : 'more-itertools>=8.7.0',
50
- 'daemon' : 'python-daemon>=0.2.3',
51
- 'fasteners' : 'fasteners>=0.18.0',
52
- 'psutil' : 'psutil>=5.8.0',
53
- 'watchfiles' : 'watchfiles>=0.21.0',
54
- 'dill' : 'dill>=0.3.3',
50
+ 'fasteners' : 'fasteners>=0.19.0',
55
51
  'virtualenv' : 'virtualenv>=20.1.0',
56
52
  'apscheduler' : 'APScheduler>=4.0.0a5',
57
53
  'uv' : 'uv>=0.2.11',
58
54
  },
55
+ 'jobs': {
56
+ 'dill' : 'dill>=0.3.3',
57
+ 'daemon' : 'python-daemon>=0.2.3',
58
+ 'watchfiles' : 'watchfiles>=0.21.0',
59
+ 'psutil' : 'psutil>=5.8.0',
60
+ },
59
61
  'drivers': {
60
62
  'cryptography' : 'cryptography>=38.0.1',
61
- 'psycopg' : 'psycopg[binary]>=3.1.18',
63
+ 'psycopg' : 'psycopg[binary]>=3.2.1',
62
64
  'pymysql' : 'PyMySQL>=0.9.0',
63
65
  'aiomysql' : 'aiomysql>=0.0.21',
64
66
  'sqlalchemy_cockroachdb' : 'sqlalchemy-cockroachdb>=2.0.0',
@@ -156,6 +158,7 @@ packages['api'] = {
156
158
  packages['api'].update(packages['sql'])
157
159
  packages['api'].update(packages['formatting'])
158
160
  packages['api'].update(packages['dash'])
161
+ packages['api'].update(packages['jobs'])
159
162
 
160
163
  all_packages = {}
161
164
  for group, import_names in packages.items():