meerschaum 2.3.0.dev1__py3-none-any.whl → 2.3.0rc1__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 (56) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/_parse_arguments.py +10 -3
  4. meerschaum/_internal/arguments/_parser.py +6 -2
  5. meerschaum/_internal/entry.py +36 -6
  6. meerschaum/_internal/shell/Shell.py +32 -20
  7. meerschaum/actions/__init__.py +8 -6
  8. meerschaum/actions/attach.py +31 -13
  9. meerschaum/actions/copy.py +68 -41
  10. meerschaum/actions/delete.py +64 -21
  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 +26 -41
  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 +86 -37
  22. meerschaum/config/_default.py +1 -1
  23. meerschaum/config/_paths.py +5 -0
  24. meerschaum/config/_shell.py +1 -1
  25. meerschaum/config/_version.py +1 -1
  26. meerschaum/config/static/__init__.py +6 -1
  27. meerschaum/connectors/Connector.py +13 -7
  28. meerschaum/connectors/__init__.py +21 -5
  29. meerschaum/connectors/api/APIConnector.py +3 -0
  30. meerschaum/connectors/api/_jobs.py +108 -11
  31. meerschaum/connectors/parse.py +10 -13
  32. meerschaum/core/Pipe/_bootstrap.py +16 -8
  33. meerschaum/jobs/_Executor.py +69 -0
  34. meerschaum/{utils/jobs → jobs}/_Job.py +206 -40
  35. meerschaum/jobs/_LocalExecutor.py +88 -0
  36. meerschaum/jobs/_SystemdExecutor.py +608 -0
  37. meerschaum/jobs/__init__.py +365 -0
  38. meerschaum/plugins/__init__.py +6 -6
  39. meerschaum/utils/daemon/Daemon.py +7 -0
  40. meerschaum/utils/daemon/RotatingFile.py +5 -2
  41. meerschaum/utils/daemon/StdinFile.py +12 -2
  42. meerschaum/utils/daemon/__init__.py +2 -0
  43. meerschaum/utils/formatting/_jobs.py +52 -16
  44. meerschaum/utils/misc.py +23 -5
  45. meerschaum/utils/packages/_packages.py +7 -4
  46. meerschaum/utils/process.py +9 -9
  47. meerschaum/utils/venv/__init__.py +2 -2
  48. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/METADATA +14 -17
  49. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/RECORD +55 -51
  50. meerschaum/utils/jobs/__init__.py +0 -245
  51. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/LICENSE +0 -0
  52. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/NOTICE +0 -0
  53. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/WHEEL +0 -0
  54. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/entry_points.txt +0 -0
  55. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/top_level.txt +0 -0
  56. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.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,40 @@ 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
+ ### NOTE: 'local' and 'systemd' executors are being coalesced.
95
+ if executor_keys is None:
96
+ from meerschaum.jobs import get_executor_keys_from_context
97
+ executor_keys = get_executor_keys_from_context()
98
+
84
99
  self.executor_keys = executor_keys
85
100
  self.name = name
86
101
  try:
87
102
  self._daemon = (
88
103
  Daemon(daemon_id=name)
89
- if executor_keys is not None
104
+ if executor_keys == 'local'
90
105
  else None
91
106
  )
92
107
  except Exception:
93
108
  self._daemon = None
94
109
 
110
+ ### Handle any injected dependencies.
111
+ if _rotating_log is not None:
112
+ self._rotating_log = _rotating_log
113
+ if self._daemon is not None:
114
+ self._daemon._rotating_log = _rotating_log
115
+
116
+ if _stdin_file is not None:
117
+ self._stdin_file = _stdin_file
118
+ if self._daemon is not None:
119
+ self._daemon._stdin_file = _stdin_file
120
+ self._daemon._blocking_stdin_file_path = _stdin_file.blocking_file_path
121
+
122
+ if _status_hook is not None:
123
+ self._status_hook = _status_hook
124
+
125
+ if _result_hook is not None:
126
+ self._result_hook = _result_hook
127
+
95
128
  self._properties_patch = _properties or {}
96
129
 
97
130
  daemon_sysargs = (
@@ -113,6 +146,71 @@ class Job:
113
146
  self._properties_patch.update({'restart': True})
114
147
  break
115
148
 
149
+ if '--systemd' in self._sysargs:
150
+ self._properties_patch.update({'systemd': True})
151
+
152
+ @staticmethod
153
+ def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job:
154
+ """
155
+ Build a `Job` from the PID of a running Meerschaum process.
156
+
157
+ Parameters
158
+ ----------
159
+ pid: int
160
+ The PID of the process.
161
+
162
+ executor_keys: Optional[str], default None
163
+ The executor keys to assign to the job.
164
+ """
165
+ from meerschaum.config.paths import DAEMON_RESOURCES_PATH
166
+
167
+ psutil = mrsm.attempt_import('psutil')
168
+ try:
169
+ process = psutil.Process(pid)
170
+ except psutil.NoSuchProcess as e:
171
+ warn(f"Process with PID {pid} does not exist.", stack=False)
172
+ raise e
173
+
174
+ command_args = process.cmdline()
175
+ is_daemon = command_args[1] == '-c'
176
+
177
+ if is_daemon:
178
+ daemon_id = command_args[-1].split('daemon_id=')[-1].split(')')[0].replace("'", '')
179
+ root_dir = process.environ().get(STATIC_CONFIG['environment']['root'], None)
180
+ if root_dir is None:
181
+ from meerschaum.config.paths import ROOT_DIR_PATH
182
+ root_dir = ROOT_DIR_PATH
183
+ jobs_dir = root_dir / DAEMON_RESOURCES_PATH.name
184
+ daemon_dir = jobs_dir / daemon_id
185
+ pid_file = daemon_dir / 'process.pid'
186
+ properties_path = daemon_dir / 'properties.json'
187
+ pickle_path = daemon_dir / 'pickle.pkl'
188
+
189
+ if pid_file.exists():
190
+ with open(pid_file, 'r', encoding='utf-8') as f:
191
+ daemon_pid = int(f.read())
192
+
193
+ if pid != daemon_pid:
194
+ raise EnvironmentError(f"Differing PIDs: {pid=}, {daemon_pid=}")
195
+ else:
196
+ raise EnvironmentError(f"Is job '{daemon_id}' running?")
197
+
198
+ return Job(daemon_id, executor_keys=executor_keys)
199
+
200
+ from meerschaum._internal.arguments._parse_arguments import parse_arguments
201
+ from meerschaum.utils.daemon import get_new_daemon_name
202
+
203
+ mrsm_ix = 0
204
+ for i, arg in enumerate(command_args):
205
+ if 'mrsm' in arg or 'meerschaum' in arg.lower():
206
+ mrsm_ix = i
207
+ break
208
+
209
+ sysargs = command_args[mrsm_ix+1:]
210
+ kwargs = parse_arguments(sysargs)
211
+ name = kwargs.get('name', get_new_daemon_name())
212
+ return Job(name, sysargs, executor_keys=executor_keys)
213
+
116
214
  def start(self, debug: bool = False) -> SuccessTuple:
117
215
  """
118
216
  Start the job's daemon.
@@ -144,6 +242,8 @@ class Job:
144
242
  if self.daemon.status == 'stopped':
145
243
  if not self.restart:
146
244
  return True, f"{self} is not running."
245
+ elif self.stop_time is not None:
246
+ return True, f"{self} will not restart until manually started."
147
247
 
148
248
  quit_success, quit_msg = self.daemon.quit(timeout=timeout_seconds)
149
249
  if quit_success:
@@ -218,7 +318,8 @@ class Job:
218
318
  self,
219
319
  callback_function: Callable[[str], None] = partial(print, end=''),
220
320
  input_callback_function: Optional[Callable[[], str]] = None,
221
- stop_event: Optional[threading.Event] = None,
321
+ stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None,
322
+ stop_event: Optional[asyncio.Event] = None,
222
323
  stop_on_exit: bool = False,
223
324
  strip_timestamps: bool = False,
224
325
  accept_input: bool = True,
@@ -237,9 +338,13 @@ class Job:
237
338
  If provided, execute this callback when the daemon is blocking on stdin.
238
339
  Defaults to `sys.stdin.readline()`.
239
340
 
341
+ stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None
342
+ If provided, execute this callback when the daemon stops.
343
+ The job's SuccessTuple will be passed to the callback.
344
+
240
345
  stop_event: Optional[asyncio.Event], default None
241
346
  If provided, stop monitoring when this event is set.
242
- You may instead raise `meerschaum.utils.jobs.StopMonitoringLogs`
347
+ You may instead raise `meerschaum.jobs.StopMonitoringLogs`
243
348
  from within `callback_function` to stop monitoring.
244
349
 
245
350
  stop_on_exit: bool, default False
@@ -251,13 +356,29 @@ class Job:
251
356
  accept_input: bool, default True
252
357
  If `True`, accept input when the daemon blocks on stdin.
253
358
  """
359
+ def default_input_callback_function():
360
+ return sys.stdin.readline()
361
+
362
+ if input_callback_function is None:
363
+ input_callback_function = default_input_callback_function
364
+
254
365
  if self.executor is not None:
255
- self.executor.monitor_logs(self.name, callback_function)
366
+ self.executor.monitor_logs(
367
+ self.name,
368
+ callback_function,
369
+ input_callback_function=input_callback_function,
370
+ stop_callback_function=stop_callback_function,
371
+ stop_on_exit=stop_on_exit,
372
+ accept_input=accept_input,
373
+ strip_timestamps=strip_timestamps,
374
+ debug=debug,
375
+ )
256
376
  return
257
377
 
258
378
  monitor_logs_coroutine = self.monitor_logs_async(
259
379
  callback_function=callback_function,
260
380
  input_callback_function=input_callback_function,
381
+ stop_callback_function=stop_callback_function,
261
382
  stop_event=stop_event,
262
383
  stop_on_exit=stop_on_exit,
263
384
  strip_timestamps=strip_timestamps,
@@ -270,10 +391,14 @@ class Job:
270
391
  self,
271
392
  callback_function: Callable[[str], None] = partial(print, end='', flush=True),
272
393
  input_callback_function: Optional[Callable[[], str]] = None,
394
+ stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None,
273
395
  stop_event: Optional[asyncio.Event] = None,
274
396
  stop_on_exit: bool = False,
275
397
  strip_timestamps: bool = False,
276
398
  accept_input: bool = True,
399
+ _logs_path: Optional[pathlib.Path] = None,
400
+ _log = None,
401
+ _stdin_file = None,
277
402
  debug: bool = False,
278
403
  ):
279
404
  """
@@ -289,9 +414,13 @@ class Job:
289
414
  If provided, execute this callback when the daemon is blocking on stdin.
290
415
  Defaults to `sys.stdin.readline()`.
291
416
 
417
+ stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None
418
+ If provided, execute this callback when the daemon stops.
419
+ The job's SuccessTuple will be passed to the callback.
420
+
292
421
  stop_event: Optional[asyncio.Event], default None
293
422
  If provided, stop monitoring when this event is set.
294
- You may instead raise `meerschaum.utils.jobs.StopMonitoringLogs`
423
+ You may instead raise `meerschaum.jobs.StopMonitoringLogs`
295
424
  from within `callback_function` to stop monitoring.
296
425
 
297
426
  stop_on_exit: bool, default False
@@ -314,6 +443,8 @@ class Job:
314
443
  self.name,
315
444
  callback_function,
316
445
  input_callback_function=input_callback_function,
446
+ stop_callback_function=stop_callback_function,
447
+ stop_on_exit=stop_on_exit,
317
448
  accept_input=accept_input,
318
449
  debug=debug,
319
450
  )
@@ -323,16 +454,18 @@ class Job:
323
454
 
324
455
  events = {
325
456
  'user': stop_event,
326
- 'stopped': (asyncio.Event() if stop_on_exit else None),
457
+ 'stopped': asyncio.Event(),
327
458
  }
328
459
  combined_event = asyncio.Event()
329
460
  emitted_text = False
461
+ stdin_file = _stdin_file if _stdin_file is not None else self.daemon.stdin_file
330
462
 
331
463
  async def check_job_status():
332
464
  nonlocal emitted_text
333
465
  stopped_event = events.get('stopped', None)
334
466
  if stopped_event is None:
335
467
  return
468
+
336
469
  sleep_time = 0.1
337
470
  while sleep_time < 60:
338
471
  if self.status == 'stopped':
@@ -340,7 +473,19 @@ class Job:
340
473
  await asyncio.sleep(sleep_time)
341
474
  sleep_time = round(sleep_time * 1.1, 2)
342
475
  continue
343
- events['stopped'].set()
476
+
477
+ if stop_callback_function is not None:
478
+ try:
479
+ if asyncio.iscoroutinefunction(stop_callback_function):
480
+ await stop_callback_function(self.result)
481
+ else:
482
+ stop_callback_function(self.result)
483
+ except Exception:
484
+ warn(traceback.format_exc())
485
+
486
+ if stop_on_exit:
487
+ events['stopped'].set()
488
+
344
489
  break
345
490
  await asyncio.sleep(0.1)
346
491
 
@@ -360,7 +505,7 @@ class Job:
360
505
 
361
506
  try:
362
507
  print('', end='', flush=True)
363
- if asyncio.iscoroutinefunction(callback_function):
508
+ if asyncio.iscoroutinefunction(input_callback_function):
364
509
  data = await input_callback_function()
365
510
  else:
366
511
  data = input_callback_function()
@@ -368,7 +513,8 @@ class Job:
368
513
  break
369
514
  if not data.endswith('\n'):
370
515
  data += '\n'
371
- self.daemon.stdin_file.write(data)
516
+
517
+ stdin_file.write(data)
372
518
  await asyncio.sleep(0.1)
373
519
 
374
520
  async def combine_events():
@@ -396,7 +542,7 @@ class Job:
396
542
  check_blocking_on_input_task = asyncio.create_task(check_blocking_on_input())
397
543
  combine_events_task = asyncio.create_task(combine_events())
398
544
 
399
- log = self.daemon.rotating_log
545
+ log = _log if _log is not None else self.daemon.rotating_log
400
546
  lines_to_show = get_config('jobs', 'logs', 'lines_to_show')
401
547
 
402
548
  async def emit_latest_lines():
@@ -434,7 +580,7 @@ class Job:
434
580
 
435
581
  watchfiles = mrsm.attempt_import('watchfiles')
436
582
  async for changes in watchfiles.awatch(
437
- LOGS_RESOURCES_PATH,
583
+ _logs_path or LOGS_RESOURCES_PATH,
438
584
  stop_event=combined_event,
439
585
  ):
440
586
  for change in changes:
@@ -444,23 +590,7 @@ class Job:
444
590
  if latest_subfile_path != file_path:
445
591
  continue
446
592
 
447
- lines = log.readlines()
448
- for line in lines:
449
- if strip_timestamps:
450
- line = strip_timestamp_from_line(line)
451
- try:
452
- if asyncio.iscoroutinefunction(callback_function):
453
- await callback_function(line)
454
- else:
455
- callback_function(line)
456
- emitted_text = True
457
- except RuntimeError:
458
- return
459
- except StopMonitoringLogs:
460
- return
461
- except Exception:
462
- warn(f"Error in logs callback:\n{traceback.format_exc()}")
463
- return
593
+ await emit_latest_lines()
464
594
 
465
595
  await emit_latest_lines()
466
596
 
@@ -477,19 +607,16 @@ class Job:
477
607
  """
478
608
  Write to a job's daemon's `stdin`.
479
609
  """
480
- if self.executor is not None:
481
- pass
482
-
483
610
  self.daemon.stdin_file.write(data)
484
611
 
485
612
  @property
486
- def executor(self) -> Union['APIConnector', None]:
613
+ def executor(self) -> Union[Executor, None]:
487
614
  """
488
615
  If the job is remote, return the connector to the remote API instance.
489
616
  """
490
617
  return (
491
618
  mrsm.get_connector(self.executor_keys)
492
- if self.executor_keys is not None
619
+ if self.executor_keys != 'local'
493
620
  else None
494
621
  )
495
622
 
@@ -498,10 +625,11 @@ class Job:
498
625
  """
499
626
  Return the running status of the job's daemon.
500
627
  """
628
+ if '_status_hook' in self.__dict__:
629
+ return self._status_hook()
630
+
501
631
  if self.executor is not None:
502
- return self.executor.get_job_metadata(
503
- self.name
504
- ).get('daemon', {}).get('status', 'stopped')
632
+ return self.executor.get_job_status(self.name)
505
633
 
506
634
  return self.daemon.status
507
635
 
@@ -520,6 +648,9 @@ class Job:
520
648
  """
521
649
  Return whether to restart a stopped job.
522
650
  """
651
+ if self.executor is not None:
652
+ return self.executor.get_job_metadata(self.name).get('restart', False)
653
+
523
654
  return self.daemon.properties.get('restart', False)
524
655
 
525
656
  @property
@@ -530,6 +661,15 @@ class Job:
530
661
  if self.is_running():
531
662
  return True, f"{self} is running."
532
663
 
664
+ if '_result_hook' in self.__dict__:
665
+ return self._result_hook()
666
+
667
+ if self.executor is not None:
668
+ return (
669
+ self.executor.get_job_metadata(self.name)
670
+ .get('result', (False, "No result available."))
671
+ )
672
+
533
673
  _result = self.daemon.properties.get('result', None)
534
674
  if _result is None:
535
675
  return False, "No result available."
@@ -544,7 +684,9 @@ class Job:
544
684
  if self._sysargs:
545
685
  return self._sysargs
546
686
 
547
- # target_args = self.daemon.properties.get('target', {}).get('args', None)
687
+ if self.executor is not None:
688
+ return self.executor.get_job_metadata(self.name).get('sysargs', [])
689
+
548
690
  target_args = self.daemon.target_args
549
691
  if target_args is None:
550
692
  return []
@@ -575,6 +717,12 @@ class Job:
575
717
  label=shlex.join(self._sysargs),
576
718
  properties=properties,
577
719
  )
720
+ if '_rotating_log' in self.__dict__:
721
+ self._daemon._rotating_log = self._rotating_log
722
+
723
+ if '_stdin_file' in self.__dict__:
724
+ self._daemon._stdin_file = self._stdin_file
725
+ self._daemon._blocking_stdin_file_path = self._stdin_file.blocking_file_path
578
726
 
579
727
  return self._daemon
580
728
 
@@ -583,6 +731,16 @@ class Job:
583
731
  """
584
732
  The datetime when the job began running.
585
733
  """
734
+ if self.executor is not None:
735
+ began_str = self.executor.get_job_began(name)
736
+ if began_str is None:
737
+ return None
738
+ return (
739
+ datetime.fromisoformat(began_str)
740
+ .astimezone(timezone.utc)
741
+ .replace(tzinfo=None)
742
+ )
743
+
586
744
  began_str = self.daemon.properties.get('process', {}).get('began', None)
587
745
  if began_str is None:
588
746
  return None
@@ -594,6 +752,14 @@ class Job:
594
752
  """
595
753
  The datetime when the job stopped running.
596
754
  """
755
+ if self.executor is not None:
756
+ ended_str = self.executor.get_job_ended(self.name)
757
+ return (
758
+ datetime.fromisoformat(ended_str)
759
+ .astimezone(timezone.utc)
760
+ .replace(tzinfo=None)
761
+ )
762
+
597
763
  ended_str = self.daemon.properties.get('process', {}).get('ended', None)
598
764
  if ended_str is None:
599
765
  return None
@@ -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
+ """