meerschaum 2.3.0.dev3__py3-none-any.whl → 2.3.0rc2__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 (55) hide show
  1. meerschaum/__init__.py +3 -2
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/__init__.py +1 -1
  4. meerschaum/_internal/arguments/_parse_arguments.py +56 -2
  5. meerschaum/_internal/arguments/_parser.py +6 -2
  6. meerschaum/_internal/entry.py +94 -23
  7. meerschaum/_internal/shell/Shell.py +112 -35
  8. meerschaum/actions/attach.py +12 -7
  9. meerschaum/actions/copy.py +68 -41
  10. meerschaum/actions/delete.py +75 -26
  11. meerschaum/actions/edit.py +3 -3
  12. meerschaum/actions/install.py +40 -32
  13. meerschaum/actions/pause.py +44 -27
  14. meerschaum/actions/restart.py +107 -0
  15. meerschaum/actions/show.py +8 -8
  16. meerschaum/actions/start.py +27 -46
  17. meerschaum/actions/stop.py +11 -4
  18. meerschaum/api/_events.py +10 -3
  19. meerschaum/api/dash/jobs.py +69 -70
  20. meerschaum/api/routes/_actions.py +8 -3
  21. meerschaum/api/routes/_jobs.py +37 -19
  22. meerschaum/config/_default.py +1 -1
  23. meerschaum/config/_paths.py +5 -0
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/config/static/__init__.py +5 -0
  26. meerschaum/connectors/Connector.py +13 -7
  27. meerschaum/connectors/__init__.py +21 -5
  28. meerschaum/connectors/api/APIConnector.py +3 -0
  29. meerschaum/connectors/api/_jobs.py +30 -3
  30. meerschaum/connectors/parse.py +10 -14
  31. meerschaum/core/Pipe/_bootstrap.py +16 -8
  32. meerschaum/jobs/_Executor.py +69 -0
  33. meerschaum/{utils/jobs → jobs}/_Job.py +169 -21
  34. meerschaum/jobs/_LocalExecutor.py +88 -0
  35. meerschaum/jobs/_SystemdExecutor.py +613 -0
  36. meerschaum/jobs/__init__.py +388 -0
  37. meerschaum/plugins/__init__.py +6 -6
  38. meerschaum/utils/daemon/Daemon.py +7 -0
  39. meerschaum/utils/daemon/RotatingFile.py +5 -2
  40. meerschaum/utils/daemon/StdinFile.py +12 -2
  41. meerschaum/utils/daemon/__init__.py +2 -0
  42. meerschaum/utils/formatting/_jobs.py +49 -25
  43. meerschaum/utils/misc.py +23 -5
  44. meerschaum/utils/packages/_packages.py +7 -4
  45. meerschaum/utils/process.py +9 -9
  46. meerschaum/utils/venv/__init__.py +2 -2
  47. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/METADATA +14 -17
  48. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/RECORD +54 -50
  49. meerschaum/utils/jobs/__init__.py +0 -245
  50. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/LICENSE +0 -0
  51. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/NOTICE +0 -0
  52. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/WHEEL +0 -0
  53. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/entry_points.txt +0 -0
  54. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/top_level.txt +0 -0
  55. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/zip-safe +0 -0
@@ -6,6 +6,8 @@
6
6
  Define the Meerschaum abstraction atop daemons.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import shlex
10
12
  import asyncio
11
13
  import threading
@@ -18,11 +20,17 @@ from functools import partial
18
20
  from datetime import datetime, timezone
19
21
 
20
22
  import meerschaum as mrsm
21
- from meerschaum.utils.typing import List, Optional, Union, SuccessTuple, Any, Dict, Callable
23
+ from meerschaum.utils.typing import (
24
+ List, Optional, Union, SuccessTuple, Any, Dict, Callable, TYPE_CHECKING,
25
+ )
22
26
  from meerschaum._internal.entry import entry
23
27
  from meerschaum.utils.warnings import warn
24
28
  from meerschaum.config.paths import LOGS_RESOURCES_PATH
25
29
  from meerschaum.config import get_config
30
+ from meerschaum.config.static import STATIC_CONFIG
31
+
32
+ if TYPE_CHECKING:
33
+ from meerschaum.jobs._Executor import Executor
26
34
 
27
35
  BANNED_CHARS: List[str] = [
28
36
  ',', ';', "'", '"',
@@ -52,6 +60,10 @@ class Job:
52
60
  sysargs: Union[List[str], str, None] = None,
53
61
  executor_keys: Optional[str] = None,
54
62
  _properties: Optional[Dict[str, Any]] = None,
63
+ _rotating_log = None,
64
+ _stdin_file = None,
65
+ _status_hook = None,
66
+ _result_hook = None,
55
67
  ):
56
68
  """
57
69
  Create a new job to manage a `meerschaum.utils.daemon.Daemon`.
@@ -79,19 +91,48 @@ class Job:
79
91
  if isinstance(sysargs, str):
80
92
  sysargs = shlex.split(sysargs)
81
93
 
82
- if executor_keys == 'local':
83
- executor_keys = None
94
+ and_key = STATIC_CONFIG['system']['arguments']['and_key']
95
+ escaped_and_key = STATIC_CONFIG['system']['arguments']['escaped_and_key']
96
+ if sysargs:
97
+ sysargs = [
98
+ (arg if arg != escaped_and_key else and_key)
99
+ for arg in sysargs
100
+ ]
101
+
102
+ ### NOTE: 'local' and 'systemd' executors are being coalesced.
103
+ if executor_keys is None:
104
+ from meerschaum.jobs import get_executor_keys_from_context
105
+ executor_keys = get_executor_keys_from_context()
106
+
84
107
  self.executor_keys = executor_keys
85
108
  self.name = name
86
109
  try:
87
110
  self._daemon = (
88
111
  Daemon(daemon_id=name)
89
- if executor_keys is not None
112
+ if executor_keys == 'local'
90
113
  else None
91
114
  )
92
115
  except Exception:
93
116
  self._daemon = None
94
117
 
118
+ ### Handle any injected dependencies.
119
+ if _rotating_log is not None:
120
+ self._rotating_log = _rotating_log
121
+ if self._daemon is not None:
122
+ self._daemon._rotating_log = _rotating_log
123
+
124
+ if _stdin_file is not None:
125
+ self._stdin_file = _stdin_file
126
+ if self._daemon is not None:
127
+ self._daemon._stdin_file = _stdin_file
128
+ self._daemon._blocking_stdin_file_path = _stdin_file.blocking_file_path
129
+
130
+ if _status_hook is not None:
131
+ self._status_hook = _status_hook
132
+
133
+ if _result_hook is not None:
134
+ self._result_hook = _result_hook
135
+
95
136
  self._properties_patch = _properties or {}
96
137
 
97
138
  daemon_sysargs = (
@@ -113,6 +154,71 @@ class Job:
113
154
  self._properties_patch.update({'restart': True})
114
155
  break
115
156
 
157
+ if '--systemd' in self._sysargs:
158
+ self._properties_patch.update({'systemd': True})
159
+
160
+ @staticmethod
161
+ def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job:
162
+ """
163
+ Build a `Job` from the PID of a running Meerschaum process.
164
+
165
+ Parameters
166
+ ----------
167
+ pid: int
168
+ The PID of the process.
169
+
170
+ executor_keys: Optional[str], default None
171
+ The executor keys to assign to the job.
172
+ """
173
+ from meerschaum.config.paths import DAEMON_RESOURCES_PATH
174
+
175
+ psutil = mrsm.attempt_import('psutil')
176
+ try:
177
+ process = psutil.Process(pid)
178
+ except psutil.NoSuchProcess as e:
179
+ warn(f"Process with PID {pid} does not exist.", stack=False)
180
+ raise e
181
+
182
+ command_args = process.cmdline()
183
+ is_daemon = command_args[1] == '-c'
184
+
185
+ if is_daemon:
186
+ daemon_id = command_args[-1].split('daemon_id=')[-1].split(')')[0].replace("'", '')
187
+ root_dir = process.environ().get(STATIC_CONFIG['environment']['root'], None)
188
+ if root_dir is None:
189
+ from meerschaum.config.paths import ROOT_DIR_PATH
190
+ root_dir = ROOT_DIR_PATH
191
+ jobs_dir = root_dir / DAEMON_RESOURCES_PATH.name
192
+ daemon_dir = jobs_dir / daemon_id
193
+ pid_file = daemon_dir / 'process.pid'
194
+ properties_path = daemon_dir / 'properties.json'
195
+ pickle_path = daemon_dir / 'pickle.pkl'
196
+
197
+ if pid_file.exists():
198
+ with open(pid_file, 'r', encoding='utf-8') as f:
199
+ daemon_pid = int(f.read())
200
+
201
+ if pid != daemon_pid:
202
+ raise EnvironmentError(f"Differing PIDs: {pid=}, {daemon_pid=}")
203
+ else:
204
+ raise EnvironmentError(f"Is job '{daemon_id}' running?")
205
+
206
+ return Job(daemon_id, executor_keys=executor_keys)
207
+
208
+ from meerschaum._internal.arguments._parse_arguments import parse_arguments
209
+ from meerschaum.utils.daemon import get_new_daemon_name
210
+
211
+ mrsm_ix = 0
212
+ for i, arg in enumerate(command_args):
213
+ if 'mrsm' in arg or 'meerschaum' in arg.lower():
214
+ mrsm_ix = i
215
+ break
216
+
217
+ sysargs = command_args[mrsm_ix+1:]
218
+ kwargs = parse_arguments(sysargs)
219
+ name = kwargs.get('name', get_new_daemon_name())
220
+ return Job(name, sysargs, executor_keys=executor_keys)
221
+
116
222
  def start(self, debug: bool = False) -> SuccessTuple:
117
223
  """
118
224
  Start the job's daemon.
@@ -144,6 +250,8 @@ class Job:
144
250
  if self.daemon.status == 'stopped':
145
251
  if not self.restart:
146
252
  return True, f"{self} is not running."
253
+ elif self.stop_time is not None:
254
+ return True, f"{self} will not restart until manually started."
147
255
 
148
256
  quit_success, quit_msg = self.daemon.quit(timeout=timeout_seconds)
149
257
  if quit_success:
@@ -244,7 +352,7 @@ class Job:
244
352
 
245
353
  stop_event: Optional[asyncio.Event], default None
246
354
  If provided, stop monitoring when this event is set.
247
- You may instead raise `meerschaum.utils.jobs.StopMonitoringLogs`
355
+ You may instead raise `meerschaum.jobs.StopMonitoringLogs`
248
356
  from within `callback_function` to stop monitoring.
249
357
 
250
358
  stop_on_exit: bool, default False
@@ -296,6 +404,9 @@ class Job:
296
404
  stop_on_exit: bool = False,
297
405
  strip_timestamps: bool = False,
298
406
  accept_input: bool = True,
407
+ _logs_path: Optional[pathlib.Path] = None,
408
+ _log = None,
409
+ _stdin_file = None,
299
410
  debug: bool = False,
300
411
  ):
301
412
  """
@@ -317,7 +428,7 @@ class Job:
317
428
 
318
429
  stop_event: Optional[asyncio.Event], default None
319
430
  If provided, stop monitoring when this event is set.
320
- You may instead raise `meerschaum.utils.jobs.StopMonitoringLogs`
431
+ You may instead raise `meerschaum.jobs.StopMonitoringLogs`
321
432
  from within `callback_function` to stop monitoring.
322
433
 
323
434
  stop_on_exit: bool, default False
@@ -355,12 +466,14 @@ class Job:
355
466
  }
356
467
  combined_event = asyncio.Event()
357
468
  emitted_text = False
469
+ stdin_file = _stdin_file if _stdin_file is not None else self.daemon.stdin_file
358
470
 
359
471
  async def check_job_status():
360
472
  nonlocal emitted_text
361
473
  stopped_event = events.get('stopped', None)
362
474
  if stopped_event is None:
363
475
  return
476
+
364
477
  sleep_time = 0.1
365
478
  while sleep_time < 60:
366
479
  if self.status == 'stopped':
@@ -408,7 +521,8 @@ class Job:
408
521
  break
409
522
  if not data.endswith('\n'):
410
523
  data += '\n'
411
- self.daemon.stdin_file.write(data)
524
+
525
+ stdin_file.write(data)
412
526
  await asyncio.sleep(0.1)
413
527
 
414
528
  async def combine_events():
@@ -436,7 +550,7 @@ class Job:
436
550
  check_blocking_on_input_task = asyncio.create_task(check_blocking_on_input())
437
551
  combine_events_task = asyncio.create_task(combine_events())
438
552
 
439
- log = self.daemon.rotating_log
553
+ log = _log if _log is not None else self.daemon.rotating_log
440
554
  lines_to_show = get_config('jobs', 'logs', 'lines_to_show')
441
555
 
442
556
  async def emit_latest_lines():
@@ -474,7 +588,7 @@ class Job:
474
588
 
475
589
  watchfiles = mrsm.attempt_import('watchfiles')
476
590
  async for changes in watchfiles.awatch(
477
- LOGS_RESOURCES_PATH,
591
+ _logs_path or LOGS_RESOURCES_PATH,
478
592
  stop_event=combined_event,
479
593
  ):
480
594
  for change in changes:
@@ -485,7 +599,6 @@ class Job:
485
599
  continue
486
600
 
487
601
  await emit_latest_lines()
488
- await emit_latest_lines()
489
602
 
490
603
  await emit_latest_lines()
491
604
 
@@ -502,20 +615,16 @@ class Job:
502
615
  """
503
616
  Write to a job's daemon's `stdin`.
504
617
  """
505
- ### TODO implement remote method?
506
- if self.executor is not None:
507
- pass
508
-
509
618
  self.daemon.stdin_file.write(data)
510
619
 
511
620
  @property
512
- def executor(self) -> Union['APIConnector', None]:
621
+ def executor(self) -> Union[Executor, None]:
513
622
  """
514
623
  If the job is remote, return the connector to the remote API instance.
515
624
  """
516
625
  return (
517
626
  mrsm.get_connector(self.executor_keys)
518
- if self.executor_keys is not None
627
+ if self.executor_keys != 'local'
519
628
  else None
520
629
  )
521
630
 
@@ -524,10 +633,11 @@ class Job:
524
633
  """
525
634
  Return the running status of the job's daemon.
526
635
  """
636
+ if '_status_hook' in self.__dict__:
637
+ return self._status_hook()
638
+
527
639
  if self.executor is not None:
528
- return self.executor.get_job_metadata(
529
- self.name
530
- ).get('daemon', {}).get('status', 'stopped')
640
+ return self.executor.get_job_status(self.name)
531
641
 
532
642
  return self.daemon.status
533
643
 
@@ -546,6 +656,9 @@ class Job:
546
656
  """
547
657
  Return whether to restart a stopped job.
548
658
  """
659
+ if self.executor is not None:
660
+ return self.executor.get_job_metadata(self.name).get('restart', False)
661
+
549
662
  return self.daemon.properties.get('restart', False)
550
663
 
551
664
  @property
@@ -556,6 +669,15 @@ class Job:
556
669
  if self.is_running():
557
670
  return True, f"{self} is running."
558
671
 
672
+ if '_result_hook' in self.__dict__:
673
+ return self._result_hook()
674
+
675
+ if self.executor is not None:
676
+ return (
677
+ self.executor.get_job_metadata(self.name)
678
+ .get('result', (False, "No result available."))
679
+ )
680
+
559
681
  _result = self.daemon.properties.get('result', None)
560
682
  if _result is None:
561
683
  return False, "No result available."
@@ -570,7 +692,9 @@ class Job:
570
692
  if self._sysargs:
571
693
  return self._sysargs
572
694
 
573
- # target_args = self.daemon.properties.get('target', {}).get('args', None)
695
+ if self.executor is not None:
696
+ return self.executor.get_job_metadata(self.name).get('sysargs', [])
697
+
574
698
  target_args = self.daemon.target_args
575
699
  if target_args is None:
576
700
  return []
@@ -601,6 +725,12 @@ class Job:
601
725
  label=shlex.join(self._sysargs),
602
726
  properties=properties,
603
727
  )
728
+ if '_rotating_log' in self.__dict__:
729
+ self._daemon._rotating_log = self._rotating_log
730
+
731
+ if '_stdin_file' in self.__dict__:
732
+ self._daemon._stdin_file = self._stdin_file
733
+ self._daemon._blocking_stdin_file_path = self._stdin_file.blocking_file_path
604
734
 
605
735
  return self._daemon
606
736
 
@@ -609,6 +739,16 @@ class Job:
609
739
  """
610
740
  The datetime when the job began running.
611
741
  """
742
+ if self.executor is not None:
743
+ began_str = self.executor.get_job_began(name)
744
+ if began_str is None:
745
+ return None
746
+ return (
747
+ datetime.fromisoformat(began_str)
748
+ .astimezone(timezone.utc)
749
+ .replace(tzinfo=None)
750
+ )
751
+
612
752
  began_str = self.daemon.properties.get('process', {}).get('began', None)
613
753
  if began_str is None:
614
754
  return None
@@ -620,6 +760,14 @@ class Job:
620
760
  """
621
761
  The datetime when the job stopped running.
622
762
  """
763
+ if self.executor is not None:
764
+ ended_str = self.executor.get_job_ended(self.name)
765
+ return (
766
+ datetime.fromisoformat(ended_str)
767
+ .astimezone(timezone.utc)
768
+ .replace(tzinfo=None)
769
+ )
770
+
623
771
  ended_str = self.daemon.properties.get('process', {}).get('ended', None)
624
772
  if ended_str is None:
625
773
  return None
@@ -691,7 +839,7 @@ class Job:
691
839
  """
692
840
  Return the job's Daemon label (joined sysargs).
693
841
  """
694
- return shlex.join(self.sysargs)
842
+ return shlex.join(self.sysargs).replace(' + ', '\n+ ')
695
843
 
696
844
  def __str__(self) -> str:
697
845
  sysargs = self.sysargs
@@ -0,0 +1,88 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Run jobs locally.
6
+ """
7
+
8
+ from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union
9
+ from meerschaum.jobs import Job, Executor, make_executor
10
+ from meerschaum.utils.daemon import Daemon, get_daemons
11
+ from meerschaum._internal.entry import entry
12
+
13
+
14
+ @make_executor
15
+ class LocalExecutor(Executor):
16
+ """
17
+ Run jobs locally as Unix daemons.
18
+ """
19
+
20
+ def get_job_daemon(
21
+ self,
22
+ name: str,
23
+ # sysargs: Opt
24
+ debug: bool = False,
25
+ ) -> Union[Daemon, None]:
26
+ """
27
+ Return a job's daemon if it exists.
28
+ """
29
+ try:
30
+ daemon = Daemon(name)
31
+ except Exception:
32
+ daemon = None
33
+
34
+ return daemon
35
+
36
+ def get_daemon_syargs(self, name: str, debug: bool = False) -> Union[List[str], None]:
37
+ """
38
+ Return the list of sysargs from the job's daemon.
39
+ """
40
+ daemon = self.get_job_daemon(name, debug=debug)
41
+
42
+ if daemon is None:
43
+ return None
44
+
45
+ return daemon.properties.get('target', {}).get('args', [None])[0]
46
+
47
+ def get_job_exists(self, name: str, debug: bool = False) -> bool:
48
+ """
49
+ Return whether a job exists.
50
+ """
51
+ daemon = self.get_job_daemon(name, debug=debug)
52
+ if daemon is None:
53
+ return False
54
+
55
+ def get_jobs(self) -> Dict[str, Job]:
56
+ """
57
+ Return a dictionary of names -> Jobs.
58
+ """
59
+
60
+ def create_job(self, name: str, sysargs: List[str], debug: bool = False) -> SuccessTuple:
61
+ """
62
+ Create a new job.
63
+ """
64
+
65
+ def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
66
+ """
67
+ Start a job.
68
+ """
69
+
70
+ def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
71
+ """
72
+ Stop a job.
73
+ """
74
+
75
+ def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
76
+ """
77
+ Pause a job.
78
+ """
79
+
80
+ def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
81
+ """
82
+ Delete a job.
83
+ """
84
+
85
+ def get_logs(self, name: str, debug: bool = False) -> str:
86
+ """
87
+ Return a job's log output.
88
+ """