meerschaum 2.2.7__py3-none-any.whl → 2.3.0.dev3__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 (52) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/_internal/arguments/_parse_arguments.py +10 -3
  3. meerschaum/_internal/arguments/_parser.py +44 -15
  4. meerschaum/_internal/entry.py +22 -1
  5. meerschaum/_internal/shell/Shell.py +129 -31
  6. meerschaum/actions/__init__.py +8 -6
  7. meerschaum/actions/api.py +12 -12
  8. meerschaum/actions/attach.py +108 -0
  9. meerschaum/actions/delete.py +35 -26
  10. meerschaum/actions/show.py +119 -148
  11. meerschaum/actions/start.py +85 -75
  12. meerschaum/actions/stop.py +68 -39
  13. meerschaum/api/_events.py +18 -1
  14. meerschaum/api/_oauth2.py +2 -0
  15. meerschaum/api/_websockets.py +2 -2
  16. meerschaum/api/dash/jobs.py +5 -2
  17. meerschaum/api/routes/__init__.py +1 -0
  18. meerschaum/api/routes/_actions.py +122 -44
  19. meerschaum/api/routes/_jobs.py +371 -0
  20. meerschaum/api/routes/_pipes.py +5 -5
  21. meerschaum/config/_default.py +1 -0
  22. meerschaum/config/_paths.py +1 -0
  23. meerschaum/config/_shell.py +8 -3
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/config/static/__init__.py +10 -0
  26. meerschaum/connectors/__init__.py +9 -11
  27. meerschaum/connectors/api/APIConnector.py +18 -1
  28. meerschaum/connectors/api/_actions.py +60 -71
  29. meerschaum/connectors/api/_jobs.py +330 -0
  30. meerschaum/connectors/parse.py +23 -7
  31. meerschaum/plugins/__init__.py +89 -5
  32. meerschaum/utils/daemon/Daemon.py +255 -30
  33. meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
  34. meerschaum/utils/daemon/RotatingFile.py +10 -6
  35. meerschaum/utils/daemon/StdinFile.py +110 -0
  36. meerschaum/utils/daemon/__init__.py +13 -7
  37. meerschaum/utils/formatting/__init__.py +2 -1
  38. meerschaum/utils/formatting/_jobs.py +83 -54
  39. meerschaum/utils/formatting/_shell.py +6 -0
  40. meerschaum/utils/jobs/_Job.py +710 -0
  41. meerschaum/utils/jobs/__init__.py +245 -0
  42. meerschaum/utils/misc.py +18 -17
  43. meerschaum/utils/packages/_packages.py +2 -2
  44. meerschaum/utils/prompt.py +16 -8
  45. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/METADATA +9 -9
  46. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/RECORD +52 -46
  47. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/WHEEL +1 -1
  48. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/LICENSE +0 -0
  49. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/NOTICE +0 -0
  50. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/entry_points.txt +0 -0
  51. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/top_level.txt +0 -0
  52. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/zip-safe +0 -0
@@ -63,6 +63,9 @@ 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 '%Y-%m-%d %H:%M'
68
+ If `write_timestamps` is `True`, use this format for the timestamps.
66
69
  """
67
70
  self.file_path = pathlib.Path(file_path)
68
71
  if num_files_to_keep is None:
@@ -232,10 +235,10 @@ class RotatingFile(io.IOBase):
232
235
 
233
236
 
234
237
  def refresh_files(
235
- self,
236
- potential_new_len: int = 0,
237
- start_interception: bool = False,
238
- ) -> '_io.TextUIWrapper':
238
+ self,
239
+ potential_new_len: int = 0,
240
+ start_interception: bool = False,
241
+ ) -> '_io.TextUIWrapper':
239
242
  """
240
243
  Check the state of the subfiles.
241
244
  If the latest subfile is too large, create a new file and delete old ones.
@@ -339,7 +342,7 @@ class RotatingFile(io.IOBase):
339
342
 
340
343
  def get_timestamp_prefix_str(self) -> str:
341
344
  """
342
- Return the current minute prefixm string.
345
+ Return the current minute prefix string.
343
346
  """
344
347
  return datetime.now(timezone.utc).strftime(self.timestamp_format) + ' | '
345
348
 
@@ -568,7 +571,8 @@ class RotatingFile(io.IOBase):
568
571
  return
569
572
 
570
573
  self._cursor = (max_ix, position)
571
- self._current_file_obj.seek(position)
574
+ if self._current_file_obj is not None:
575
+ self._current_file_obj.seek(position)
572
576
 
573
577
 
574
578
  def flush(self) -> None:
@@ -0,0 +1,110 @@
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
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: pathlib.Path,
27
+ lock_file_path: Optional[pathlib.Path] = None,
28
+ ):
29
+ self.file_path = file_path
30
+ self.blocking_file_path = (
31
+ lock_file_path
32
+ if lock_file_path is not None
33
+ else (file_path.parent / (file_path.name + '.block'))
34
+ )
35
+ self._file_handler = None
36
+ self._fd = None
37
+ self.sel = selectors.DefaultSelector()
38
+
39
+ @property
40
+ def file_handler(self):
41
+ """
42
+ Return the read file handler to the provided file path.
43
+ """
44
+ if self._file_handler is not None:
45
+ return self._file_handler
46
+
47
+ if self.file_path.exists():
48
+ self.file_path.unlink()
49
+
50
+ os.mkfifo(self.file_path.as_posix(), mode=0o600)
51
+
52
+ self._fd = os.open(self.file_path, os.O_RDONLY | os.O_NONBLOCK)
53
+ self._file_handler = os.fdopen(self._fd, 'rb', buffering=0)
54
+ self.sel.register(self._file_handler, selectors.EVENT_READ)
55
+ return self._file_handler
56
+
57
+ def write(self, data):
58
+ if isinstance(data, str):
59
+ data = data.encode('utf-8')
60
+
61
+ with open(self.file_path, 'wb') as f:
62
+ f.write(data)
63
+
64
+ def fileno(self):
65
+ fileno = self.file_handler.fileno()
66
+ return fileno
67
+
68
+ def read(self, size=-1):
69
+ """
70
+ Read from the FIFO pipe, blocking on EOFError.
71
+ """
72
+ _ = self.file_handler
73
+ while True:
74
+ try:
75
+ data = self._file_handler.read(size)
76
+ if data:
77
+ try:
78
+ if self.blocking_file_path.exists():
79
+ self.blocking_file_path.unlink()
80
+ except Exception:
81
+ warn(traceback.format_exc())
82
+ return data.decode('utf-8')
83
+ except (OSError, EOFError):
84
+ pass
85
+
86
+ self.blocking_file_path.touch()
87
+ time.sleep(0.1)
88
+
89
+ def readline(self, size=-1):
90
+ line = ''
91
+ while True:
92
+ data = self.read(1)
93
+ if not data or data == '\n':
94
+ break
95
+ line += data
96
+
97
+ return line
98
+
99
+ def close(self):
100
+ if self._file_handler is not None:
101
+ self.sel.unregister(self._file_handler)
102
+ self._file_handler.close()
103
+ os.close(self._fd)
104
+ self._file_handler = None
105
+ self._fd = None
106
+
107
+ super().close()
108
+
109
+ def is_open(self):
110
+ return self._file_handler is not None
@@ -76,7 +76,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
76
76
  filtered_sysargs,
77
77
  daemon_id=_args.get('name', None) if _args else None,
78
78
  label=label,
79
- keep_daemon_output=('--rm' not in sysargs),
79
+ keep_daemon_output=('--rm' not in (sysargs or [])),
80
80
  )
81
81
  return success_tuple
82
82
 
@@ -181,7 +181,11 @@ def get_daemon_ids() -> List[str]:
181
181
  """
182
182
  Return the IDs of all daemons on disk.
183
183
  """
184
- return sorted(os.listdir(DAEMON_RESOURCES_PATH))
184
+ return [
185
+ daemon_dir
186
+ for daemon_dir in sorted(os.listdir(DAEMON_RESOURCES_PATH))
187
+ if (DAEMON_RESOURCES_PATH / daemon_dir / 'properties.json').exists()
188
+ ]
185
189
 
186
190
 
187
191
  def get_running_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
@@ -225,10 +229,11 @@ def get_stopped_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
225
229
 
226
230
 
227
231
  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`.
232
+ filter_list: Optional[List[str]] = None,
233
+ warn: bool = False,
234
+ ) -> List[Daemon]:
235
+ """
236
+ Return a list of `Daemons` filtered by a list of `daemon_ids`.
232
237
  Only `Daemons` that exist are returned.
233
238
 
234
239
  If `filter_list` is `None` or empty, return all `Daemons` (from `get_daemons()`).
@@ -250,13 +255,14 @@ def get_filtered_daemons(
250
255
  if not filter_list:
251
256
  daemons = get_daemons()
252
257
  return [d for d in daemons if not d.hidden]
258
+
253
259
  from meerschaum.utils.warnings import warn as _warn
254
260
  daemons = []
255
261
  for d_id in filter_list:
256
262
  try:
257
263
  d = Daemon(daemon_id=d_id)
258
264
  _exists = d.path.exists()
259
- except Exception as e:
265
+ except Exception:
260
266
  _exists = False
261
267
  if not _exists:
262
268
  if warn:
@@ -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,52 @@ 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.utils.jobs import (
16
+ Job,
17
+ get_jobs,
18
+ get_running_jobs,
19
+ get_stopped_jobs,
20
+ get_paused_jobs,
18
21
  )
22
+ from meerschaum.config import get_config
23
+
19
24
 
20
25
  def pprint_jobs(
21
- daemons: List[Daemon],
26
+ jobs: Dict[str, Job],
22
27
  nopretty: bool = False,
23
28
  ):
24
29
  """Pretty-print a list of Daemons."""
25
30
  from meerschaum.utils.formatting import make_header
26
31
 
27
- running_daemons = get_running_daemons(daemons)
28
- paused_daemons = get_paused_daemons(daemons)
29
- stopped_daemons = get_stopped_daemons(daemons)
32
+ running_jobs = get_running_jobs(jobs=jobs)
33
+ paused_jobs = get_paused_jobs(jobs=jobs)
34
+ stopped_jobs = get_stopped_jobs(jobs=jobs)
35
+ executor_keys = (list(jobs.values())[0].executor_keys if jobs else None) or 'local'
30
36
 
31
37
  def _nopretty_print():
32
38
  from meerschaum.utils.misc import print_options
33
- if running_daemons:
39
+ if running_jobs:
34
40
  if not nopretty:
35
41
  print('\n' + make_header('Running jobs'))
36
- for d in running_daemons:
37
- pprint_job(d, nopretty=nopretty)
42
+ for name, job in running_jobs.items():
43
+ pprint_job(job, nopretty=nopretty)
38
44
 
39
- if paused_daemons:
45
+ if paused_jobs:
40
46
  if not nopretty:
41
47
  print('\n' + make_header('Paused jobs'))
42
- for d in paused_daemons:
43
- pprint_job(d, nopretty=nopretty)
48
+ for name, job in paused_jobs.items():
49
+ pprint_job(job, nopretty=nopretty)
44
50
 
45
- if stopped_daemons:
51
+ if stopped_jobs:
46
52
  if not nopretty:
47
53
  print('\n' + make_header('Stopped jobs'))
48
- for d in stopped_daemons:
49
- pprint_job(d, nopretty=nopretty)
54
+ for name, job in stopped_jobs.items():
55
+ pprint_job(job, nopretty=nopretty)
50
56
 
51
57
  def _pretty_print():
52
58
  from meerschaum.utils.formatting import get_console, UNICODE, ANSI, format_success_tuple
@@ -56,19 +62,17 @@ def pprint_jobs(
56
62
  'rich.table', 'rich.text', 'rich.box', 'rich.json', 'rich.panel', 'rich.console',
57
63
  )
58
64
  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,
65
+ title=rich_text.Text(f"\nJobs on Executor '{executor_keys}'"),
66
+ box=(rich_box.ROUNDED if UNICODE else rich_box.ASCII),
67
+ show_lines=True,
68
+ show_header=ANSI,
63
69
  )
64
70
  table.add_column("Name", justify='right', style=('magenta' if ANSI else ''))
65
71
  table.add_column("Command")
66
72
  table.add_column("Status")
67
73
 
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)
74
+ def get_success_text(job):
75
+ success_tuple = job.result
72
76
  if not is_success_tuple(success_tuple):
73
77
  return rich_text.Text('')
74
78
 
@@ -92,38 +96,43 @@ def pprint_jobs(
92
96
  return rich_text.Text('\n') + success_tuple_text
93
97
 
94
98
 
95
- for d in running_daemons:
96
- if d.hidden:
99
+ for name, job in running_jobs.items():
100
+ if job.hidden:
97
101
  continue
102
+
103
+ status_text = (
104
+ rich_text.Text(job.status, style=('green' if ANSI else ''))
105
+ if not job.is_blocking_on_stdin()
106
+ else rich_text.Text('waiting for input', style=('yellow' if ANSI else ''))
107
+ )
108
+
98
109
  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 '')),
103
- ),
110
+ job.name,
111
+ job.label,
112
+ rich_console.Group(status_text),
104
113
  )
105
114
 
106
- for d in paused_daemons:
107
- if d.hidden:
115
+ for name, job in paused_jobs.items():
116
+ if job.hidden:
108
117
  continue
109
118
  table.add_row(
110
- d.daemon_id,
111
- d.label,
119
+ job.name,
120
+ job.label,
112
121
  rich_console.Group(
113
- rich_text.Text(d.status, style=('yellow' if ANSI else '')),
122
+ rich_text.Text(job.status, style=('yellow' if ANSI else '')),
114
123
  ),
115
124
  )
116
125
 
117
- for d in stopped_daemons:
118
- if d.hidden:
126
+ for name, job in stopped_jobs.items():
127
+ if job.hidden:
119
128
  continue
120
129
 
121
130
  table.add_row(
122
- d.daemon_id,
123
- d.label,
131
+ job.name,
132
+ job.label,
124
133
  rich_console.Group(
125
- rich_text.Text(d.status, style=('red' if ANSI else '')),
126
- get_success_text(d)
134
+ rich_text.Text(job.status, style=('red' if ANSI else '')),
135
+ get_success_text(job)
127
136
  ),
128
137
  )
129
138
  get_console().print(table)
@@ -133,15 +142,35 @@ def pprint_jobs(
133
142
 
134
143
 
135
144
  def pprint_job(
136
- daemon: Daemon,
137
- nopretty: bool = False,
138
- ):
139
- """Pretty-print a single Daemon."""
140
- if daemon.hidden:
145
+ job: Job,
146
+ nopretty: bool = False,
147
+ ):
148
+ """Pretty-print a single `Job`."""
149
+ if job.hidden:
141
150
  return
151
+
142
152
  from meerschaum.utils.warnings import info
143
153
  if not nopretty:
144
- info(f"Command for job '{daemon.daemon_id}':")
145
- print('\n' + daemon.label + '\n')
154
+ info(f"Command for job '{job.name}':")
155
+ print('\n' + job.label + '\n')
146
156
  else:
147
- print(daemon.daemon_id)
157
+ print(job.name)
158
+
159
+
160
+ def strip_timestamp_from_line(line: str) -> str:
161
+ """
162
+ Remove the leading timestamp from a job's line (if present).
163
+ """
164
+ now = datetime.now(timezone.utc)
165
+ timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
166
+ now_str = now.strftime(timestamp_format)
167
+
168
+ date_prefix_str = line[:len(now_str)]
169
+ try:
170
+ line_timestamp = datetime.strptime(date_prefix_str, timestamp_format)
171
+ except Exception:
172
+ line_timestamp = None
173
+ if line_timestamp:
174
+ line = line[(len(now_str) + 3):]
175
+
176
+ 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.")