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.
- meerschaum/__init__.py +3 -2
- meerschaum/__main__.py +0 -5
- meerschaum/_internal/arguments/__init__.py +1 -1
- meerschaum/_internal/arguments/_parse_arguments.py +56 -2
- meerschaum/_internal/arguments/_parser.py +6 -2
- meerschaum/_internal/entry.py +94 -23
- meerschaum/_internal/shell/Shell.py +112 -35
- meerschaum/actions/attach.py +12 -7
- meerschaum/actions/copy.py +68 -41
- meerschaum/actions/delete.py +75 -26
- meerschaum/actions/edit.py +3 -3
- meerschaum/actions/install.py +40 -32
- meerschaum/actions/pause.py +44 -27
- meerschaum/actions/restart.py +107 -0
- meerschaum/actions/show.py +8 -8
- meerschaum/actions/start.py +27 -46
- meerschaum/actions/stop.py +11 -4
- meerschaum/api/_events.py +10 -3
- meerschaum/api/dash/jobs.py +69 -70
- meerschaum/api/routes/_actions.py +8 -3
- meerschaum/api/routes/_jobs.py +37 -19
- meerschaum/config/_default.py +1 -1
- meerschaum/config/_paths.py +5 -0
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +5 -0
- meerschaum/connectors/Connector.py +13 -7
- meerschaum/connectors/__init__.py +21 -5
- meerschaum/connectors/api/APIConnector.py +3 -0
- meerschaum/connectors/api/_jobs.py +30 -3
- meerschaum/connectors/parse.py +10 -14
- meerschaum/core/Pipe/_bootstrap.py +16 -8
- meerschaum/jobs/_Executor.py +69 -0
- meerschaum/{utils/jobs → jobs}/_Job.py +169 -21
- meerschaum/jobs/_LocalExecutor.py +88 -0
- meerschaum/jobs/_SystemdExecutor.py +613 -0
- meerschaum/jobs/__init__.py +388 -0
- meerschaum/plugins/__init__.py +6 -6
- meerschaum/utils/daemon/Daemon.py +7 -0
- meerschaum/utils/daemon/RotatingFile.py +5 -2
- meerschaum/utils/daemon/StdinFile.py +12 -2
- meerschaum/utils/daemon/__init__.py +2 -0
- meerschaum/utils/formatting/_jobs.py +49 -25
- meerschaum/utils/misc.py +23 -5
- meerschaum/utils/packages/_packages.py +7 -4
- meerschaum/utils/process.py +9 -9
- meerschaum/utils/venv/__init__.py +2 -2
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/METADATA +14 -17
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/RECORD +54 -50
- meerschaum/utils/jobs/__init__.py +0 -245
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/WHEEL +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/top_level.txt +0 -0
- {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
|
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
|
-
|
83
|
-
|
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
|
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.
|
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.
|
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
|
-
|
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[
|
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
|
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.
|
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
|
-
|
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
|
+
"""
|