meerschaum 2.2.7__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.
- meerschaum/__init__.py +6 -1
- meerschaum/__main__.py +0 -5
- meerschaum/_internal/arguments/__init__.py +1 -1
- meerschaum/_internal/arguments/_parse_arguments.py +72 -6
- meerschaum/_internal/arguments/_parser.py +45 -15
- meerschaum/_internal/docs/index.py +265 -8
- meerschaum/_internal/entry.py +154 -24
- meerschaum/_internal/shell/Shell.py +264 -77
- meerschaum/actions/__init__.py +29 -17
- meerschaum/actions/api.py +12 -12
- meerschaum/actions/attach.py +113 -0
- meerschaum/actions/copy.py +68 -41
- meerschaum/actions/delete.py +112 -50
- 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 +130 -159
- meerschaum/actions/start.py +161 -100
- meerschaum/actions/stop.py +78 -42
- meerschaum/api/_events.py +25 -1
- meerschaum/api/_oauth2.py +2 -0
- meerschaum/api/_websockets.py +2 -2
- meerschaum/api/dash/callbacks/jobs.py +36 -44
- meerschaum/api/dash/jobs.py +89 -78
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +148 -17
- meerschaum/api/routes/_jobs.py +407 -0
- meerschaum/api/routes/_pipes.py +5 -5
- meerschaum/config/_default.py +1 -0
- meerschaum/config/_jobs.py +1 -1
- meerschaum/config/_paths.py +7 -0
- meerschaum/config/_shell.py +8 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +17 -0
- meerschaum/connectors/Connector.py +13 -7
- meerschaum/connectors/__init__.py +28 -15
- meerschaum/connectors/api/APIConnector.py +27 -1
- meerschaum/connectors/api/_actions.py +71 -6
- meerschaum/connectors/api/_jobs.py +368 -0
- meerschaum/connectors/api/_pipes.py +85 -84
- meerschaum/connectors/parse.py +27 -15
- meerschaum/core/Pipe/_bootstrap.py +16 -8
- meerschaum/jobs/_Executor.py +69 -0
- meerschaum/jobs/_Job.py +899 -0
- meerschaum/jobs/__init__.py +396 -0
- meerschaum/jobs/systemd.py +694 -0
- meerschaum/plugins/__init__.py +97 -12
- meerschaum/utils/daemon/Daemon.py +276 -30
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
- meerschaum/utils/daemon/RotatingFile.py +14 -7
- meerschaum/utils/daemon/StdinFile.py +121 -0
- meerschaum/utils/daemon/__init__.py +15 -7
- meerschaum/utils/daemon/_names.py +15 -13
- meerschaum/utils/formatting/__init__.py +2 -1
- meerschaum/utils/formatting/_jobs.py +115 -62
- meerschaum/utils/formatting/_shell.py +6 -0
- meerschaum/utils/misc.py +41 -22
- meerschaum/utils/packages/_packages.py +9 -6
- meerschaum/utils/process.py +9 -9
- meerschaum/utils/prompt.py +16 -8
- meerschaum/utils/venv/__init__.py +2 -2
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/RECORD +70 -61
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
@@ -23,6 +23,8 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
23
23
|
'pipes': '/pipes',
|
24
24
|
'metadata': '/metadata',
|
25
25
|
'actions': '/actions',
|
26
|
+
'jobs': '/jobs',
|
27
|
+
'logs': '/logs',
|
26
28
|
'users': '/users',
|
27
29
|
'login': '/login',
|
28
30
|
'connectors': '/connectors',
|
@@ -40,6 +42,11 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
40
42
|
},
|
41
43
|
'webterm_job_name': '_webterm',
|
42
44
|
'default_timeout': 600,
|
45
|
+
'jobs': {
|
46
|
+
'stdin_message': 'MRSM_STDIN',
|
47
|
+
'stop_message': 'MRSM_STOP',
|
48
|
+
'metadata_cache_seconds': 5,
|
49
|
+
},
|
43
50
|
},
|
44
51
|
'sql': {
|
45
52
|
'internal_schema': '_mrsm_internal',
|
@@ -63,6 +70,9 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
63
70
|
'noask': 'MRSM_NOASK',
|
64
71
|
'id': 'MRSM_SERVER_ID',
|
65
72
|
'daemon_id': 'MRSM_DAEMON_ID',
|
73
|
+
'systemd_log_path': 'MRSM_SYSTEMD_LOG_PATH',
|
74
|
+
'systemd_stdin_path': 'MRSM_SYSTEMD_STDIN_PATH',
|
75
|
+
'systemd_result_path': 'MRSM_SYSTEMD_RESULT_PATH',
|
66
76
|
'uri_regex': r'MRSM_([a-zA-Z0-9]*)_(\d*[a-zA-Z][a-zA-Z0-9-_+]*$)',
|
67
77
|
'prefix': 'MRSM_',
|
68
78
|
},
|
@@ -78,6 +88,10 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
78
88
|
),
|
79
89
|
'underscore_standin': '<UNDERSCORE>', ### Temporary replacement for parsing.
|
80
90
|
'failure_key': '_argparse_exception',
|
91
|
+
'and_key': '+',
|
92
|
+
'escaped_and_key': '++',
|
93
|
+
'pipeline_key': ':',
|
94
|
+
'escaped_pipeline_key': '::',
|
81
95
|
},
|
82
96
|
'urls': {
|
83
97
|
'get-pip.py': 'https://bootstrap.pypa.io/get-pip.py',
|
@@ -129,6 +143,9 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
129
143
|
},
|
130
144
|
'exists_timeout_seconds': 5.0,
|
131
145
|
},
|
146
|
+
'jobs': {
|
147
|
+
'check_restart_seconds': 1.0,
|
148
|
+
},
|
132
149
|
'setup': {
|
133
150
|
'name': 'meerschaum',
|
134
151
|
'formal_name': 'Meerschaum',
|
@@ -21,11 +21,11 @@ class Connector(metaclass=abc.ABCMeta):
|
|
21
21
|
The base connector class to hold connection attributes.
|
22
22
|
"""
|
23
23
|
def __init__(
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
self,
|
25
|
+
type: Optional[str] = None,
|
26
|
+
label: Optional[str] = None,
|
27
|
+
**kw: Any
|
28
|
+
):
|
29
29
|
"""
|
30
30
|
Set the given keyword arguments as attributes.
|
31
31
|
|
@@ -101,7 +101,7 @@ class Connector(metaclass=abc.ABCMeta):
|
|
101
101
|
|
102
102
|
### load user config into self._attributes
|
103
103
|
if self.type in conn_configs and self.label in conn_configs[self.type]:
|
104
|
-
self._attributes.update(conn_configs[self.type][self.label])
|
104
|
+
self._attributes.update(conn_configs[self.type][self.label] or {})
|
105
105
|
|
106
106
|
### load system config into self._sys_config
|
107
107
|
### (deep copy so future Connectors don't inherit changes)
|
@@ -200,7 +200,13 @@ class Connector(metaclass=abc.ABCMeta):
|
|
200
200
|
_type = self.__dict__.get('type', None)
|
201
201
|
if _type is None:
|
202
202
|
import re
|
203
|
-
|
203
|
+
is_executor = self.__class__.__name__.lower().endswith('executor')
|
204
|
+
suffix_regex = (
|
205
|
+
r'connector$'
|
206
|
+
if not is_executor
|
207
|
+
else r'executor$'
|
208
|
+
)
|
209
|
+
_type = re.sub(suffix_regex, '', self.__class__.__name__.lower())
|
204
210
|
self.__dict__['type'] = _type
|
205
211
|
return _type
|
206
212
|
|
@@ -36,9 +36,9 @@ __all__ = (
|
|
36
36
|
### store connectors partitioned by
|
37
37
|
### type, label for reuse
|
38
38
|
connectors: Dict[str, Dict[str, Connector]] = {
|
39
|
-
'api'
|
40
|
-
'sql'
|
41
|
-
'plugin': {},
|
39
|
+
'api' : {},
|
40
|
+
'sql' : {},
|
41
|
+
'plugin' : {},
|
42
42
|
}
|
43
43
|
instance_types: List[str] = ['sql', 'api']
|
44
44
|
_locks: Dict[str, RLock] = {
|
@@ -70,12 +70,12 @@ _loaded_plugin_connectors: bool = False
|
|
70
70
|
|
71
71
|
|
72
72
|
def get_connector(
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
73
|
+
type: str = None,
|
74
|
+
label: str = None,
|
75
|
+
refresh: bool = False,
|
76
|
+
debug: bool = False,
|
77
|
+
**kw: Any
|
78
|
+
) -> Connector:
|
79
79
|
"""
|
80
80
|
Return existing connector or create new connection and store for reuse.
|
81
81
|
|
@@ -127,10 +127,13 @@ def get_connector(
|
|
127
127
|
global _loaded_plugin_connectors
|
128
128
|
if isinstance(type, str) and not label and ':' in type:
|
129
129
|
type, label = type.split(':', maxsplit=1)
|
130
|
+
|
130
131
|
with _locks['_loaded_plugin_connectors']:
|
131
132
|
if not _loaded_plugin_connectors:
|
132
133
|
load_plugin_connectors()
|
134
|
+
_load_builtin_custom_connectors()
|
133
135
|
_loaded_plugin_connectors = True
|
136
|
+
|
134
137
|
if type is None and label is None:
|
135
138
|
default_instance_keys = get_config('meerschaum', 'instance', patch=True)
|
136
139
|
### recursive call to get_connector
|
@@ -274,9 +277,7 @@ def is_connected(keys: str, **kw) -> bool:
|
|
274
277
|
return False
|
275
278
|
|
276
279
|
|
277
|
-
def make_connector(
|
278
|
-
cls,
|
279
|
-
):
|
280
|
+
def make_connector(cls, _is_executor: bool = False):
|
280
281
|
"""
|
281
282
|
Register a class as a `Connector`.
|
282
283
|
The `type` will be the lower case of the class name, without the suffix `connector`.
|
@@ -302,7 +303,12 @@ def make_connector(
|
|
302
303
|
>>>
|
303
304
|
"""
|
304
305
|
import re
|
305
|
-
|
306
|
+
suffix_regex = (
|
307
|
+
r'connector$'
|
308
|
+
if not _is_executor
|
309
|
+
else r'executor$'
|
310
|
+
)
|
311
|
+
typ = re.sub(suffix_regex, '', cls.__name__.lower())
|
306
312
|
with _locks['types']:
|
307
313
|
types[typ] = cls
|
308
314
|
with _locks['custom_types']:
|
@@ -338,8 +344,8 @@ def load_plugin_connectors():
|
|
338
344
|
|
339
345
|
|
340
346
|
def get_connector_plugin(
|
341
|
-
|
342
|
-
|
347
|
+
connector: Connector,
|
348
|
+
) -> Union[str, None, mrsm.Plugin]:
|
343
349
|
"""
|
344
350
|
Determine the plugin for a connector.
|
345
351
|
This is useful for handling virtual environments for custom instance connectors.
|
@@ -365,3 +371,10 @@ def get_connector_plugin(
|
|
365
371
|
)
|
366
372
|
plugin = mrsm.Plugin(plugin_name)
|
367
373
|
return plugin if plugin.is_installed() else None
|
374
|
+
|
375
|
+
|
376
|
+
def _load_builtin_custom_connectors():
|
377
|
+
"""
|
378
|
+
Import custom connectors decorated with `@make_connector` or `@make_executor`.
|
379
|
+
"""
|
380
|
+
import meerschaum.jobs.systemd
|
@@ -33,7 +33,12 @@ class APIConnector(Connector):
|
|
33
33
|
delete,
|
34
34
|
wget,
|
35
35
|
)
|
36
|
-
from ._actions import
|
36
|
+
from ._actions import (
|
37
|
+
get_actions,
|
38
|
+
do_action,
|
39
|
+
do_action_async,
|
40
|
+
do_action_legacy,
|
41
|
+
)
|
37
42
|
from ._misc import get_mrsm_version, get_chaining_status
|
38
43
|
from ._pipes import (
|
39
44
|
register_pipe,
|
@@ -72,6 +77,27 @@ class APIConnector(Connector):
|
|
72
77
|
get_user_attributes,
|
73
78
|
)
|
74
79
|
from ._uri import from_uri
|
80
|
+
from ._jobs import (
|
81
|
+
get_jobs,
|
82
|
+
get_job,
|
83
|
+
get_job_metadata,
|
84
|
+
get_job_properties,
|
85
|
+
get_job_exists,
|
86
|
+
delete_job,
|
87
|
+
start_job,
|
88
|
+
create_job,
|
89
|
+
stop_job,
|
90
|
+
pause_job,
|
91
|
+
get_logs,
|
92
|
+
get_job_stop_time,
|
93
|
+
monitor_logs,
|
94
|
+
monitor_logs_async,
|
95
|
+
get_job_is_blocking_on_stdin,
|
96
|
+
get_job_began,
|
97
|
+
get_job_ended,
|
98
|
+
get_job_paused,
|
99
|
+
get_job_status,
|
100
|
+
)
|
75
101
|
|
76
102
|
def __init__(
|
77
103
|
self,
|
@@ -7,22 +7,87 @@ Functions to interact with /mrsm/actions
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
|
-
from meerschaum.utils.typing import SuccessTuple, Optional, List
|
11
10
|
|
12
|
-
|
11
|
+
import json
|
12
|
+
import asyncio
|
13
|
+
from functools import partial
|
14
|
+
|
15
|
+
import meerschaum as mrsm
|
16
|
+
from meerschaum.utils.typing import SuccessTuple, List, Callable, Optional
|
17
|
+
from meerschaum.config.static import STATIC_CONFIG
|
18
|
+
|
19
|
+
ACTIONS_ENDPOINT: str = STATIC_CONFIG['api']['endpoints']['actions']
|
20
|
+
|
21
|
+
|
22
|
+
def get_actions(self):
|
13
23
|
"""Get available actions from the API instance."""
|
14
|
-
|
15
|
-
|
24
|
+
return self.get(ACTIONS_ENDPOINT)
|
25
|
+
|
26
|
+
|
27
|
+
def do_action(self, sysargs: List[str]) -> SuccessTuple:
|
28
|
+
"""
|
29
|
+
Execute a Meerschaum action remotely.
|
30
|
+
"""
|
31
|
+
return asyncio.run(self.do_action_async(sysargs))
|
32
|
+
|
33
|
+
|
34
|
+
async def do_action_async(
|
35
|
+
self,
|
36
|
+
sysargs: List[str],
|
37
|
+
callback_function: Callable[[str], None] = partial(print, end=''),
|
38
|
+
) -> SuccessTuple:
|
39
|
+
"""
|
40
|
+
Monitor a job's log files and await a callback with the changes.
|
41
|
+
"""
|
42
|
+
websockets, websockets_exceptions = mrsm.attempt_import('websockets', 'websockets.exceptions')
|
43
|
+
protocol = 'ws' if self.URI.startswith('http://') else 'wss'
|
44
|
+
port = self.port if 'port' in self.__dict__ else ''
|
45
|
+
uri = f"{protocol}://{self.host}:{port}{ACTIONS_ENDPOINT}/ws"
|
46
|
+
if sysargs and sysargs[0] == 'api' and len(sysargs) > 2:
|
47
|
+
sysargs = sysargs[2:]
|
48
|
+
|
49
|
+
sysargs_str = json.dumps(sysargs)
|
50
|
+
|
51
|
+
async with websockets.connect(uri) as websocket:
|
52
|
+
try:
|
53
|
+
await websocket.send(self.token or 'no-login')
|
54
|
+
response = await websocket.recv()
|
55
|
+
init_data = json.loads(response)
|
56
|
+
if not init_data.get('is_authenticated'):
|
57
|
+
return False, "Cannot authenticate with actions endpoint."
|
16
58
|
|
59
|
+
await websocket.send(sysargs_str)
|
60
|
+
except websockets_exceptions.ConnectionClosedOK:
|
61
|
+
return False, "Connection was closed."
|
17
62
|
|
18
|
-
|
63
|
+
while True:
|
64
|
+
try:
|
65
|
+
line = await websocket.recv()
|
66
|
+
if asyncio.iscoroutinefunction(callback_function):
|
67
|
+
await callback_function(line)
|
68
|
+
else:
|
69
|
+
callback_function(line)
|
70
|
+
except KeyboardInterrupt:
|
71
|
+
await websocket.close()
|
72
|
+
break
|
73
|
+
except websockets_exceptions.ConnectionClosedOK:
|
74
|
+
break
|
75
|
+
|
76
|
+
return True, "Success"
|
77
|
+
|
78
|
+
|
79
|
+
def do_action_legacy(
|
19
80
|
self,
|
20
81
|
action: Optional[List[str]] = None,
|
21
82
|
sysargs: Optional[List[str]] = None,
|
22
83
|
debug: bool = False,
|
23
84
|
**kw
|
24
85
|
) -> SuccessTuple:
|
25
|
-
"""
|
86
|
+
"""
|
87
|
+
NOTE: This method is deprecated.
|
88
|
+
Please use `do_action()` or `do_action_async()`.
|
89
|
+
|
90
|
+
Execute a Meerschaum action remotely.
|
26
91
|
|
27
92
|
If `sysargs` are provided, parse those instead.
|
28
93
|
Otherwise infer everything from keyword arguments.
|
@@ -0,0 +1,368 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# vim:fenc=utf-8
|
4
|
+
|
5
|
+
"""
|
6
|
+
Manage jobs via the Meerschaum API.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import asyncio
|
10
|
+
import time
|
11
|
+
import json
|
12
|
+
from datetime import datetime
|
13
|
+
|
14
|
+
import meerschaum as mrsm
|
15
|
+
from meerschaum.utils.typing import Dict, Any, SuccessTuple, List, Union, Callable
|
16
|
+
from meerschaum.jobs import Job
|
17
|
+
from meerschaum.config.static import STATIC_CONFIG
|
18
|
+
from meerschaum.utils.warnings import warn, dprint
|
19
|
+
|
20
|
+
JOBS_ENDPOINT: str = STATIC_CONFIG['api']['endpoints']['jobs']
|
21
|
+
LOGS_ENDPOINT: str = STATIC_CONFIG['api']['endpoints']['logs']
|
22
|
+
JOBS_STDIN_MESSAGE: str = STATIC_CONFIG['api']['jobs']['stdin_message']
|
23
|
+
JOBS_STOP_MESSAGE: str = STATIC_CONFIG['api']['jobs']['stop_message']
|
24
|
+
JOB_METADATA_CACHE_SECONDS: int = STATIC_CONFIG['api']['jobs']['metadata_cache_seconds']
|
25
|
+
|
26
|
+
|
27
|
+
def get_jobs(self, debug: bool = False) -> Dict[str, Job]:
|
28
|
+
"""
|
29
|
+
Return a dictionary of remote jobs.
|
30
|
+
"""
|
31
|
+
response = self.get(JOBS_ENDPOINT, debug=debug)
|
32
|
+
if not response:
|
33
|
+
warn(f"Failed to get remote jobs from {self}.")
|
34
|
+
return {}
|
35
|
+
return {
|
36
|
+
name: Job(
|
37
|
+
name,
|
38
|
+
job_meta['sysargs'],
|
39
|
+
executor_keys=str(self),
|
40
|
+
_properties=job_meta['daemon']['properties']
|
41
|
+
)
|
42
|
+
for name, job_meta in response.json().items()
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
def get_job(self, name: str, debug: bool = False) -> Job:
|
47
|
+
"""
|
48
|
+
Return a single Job object.
|
49
|
+
"""
|
50
|
+
metadata = self.get_job_metadata(name, debug=debug)
|
51
|
+
if not metadata:
|
52
|
+
raise ValueError(f"Job '{name}' does not exist.")
|
53
|
+
|
54
|
+
return Job(
|
55
|
+
name,
|
56
|
+
metadata['sysargs'],
|
57
|
+
executor_keys=str(self),
|
58
|
+
_properties=metadata['daemon']['properties'],
|
59
|
+
)
|
60
|
+
|
61
|
+
|
62
|
+
def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]:
|
63
|
+
"""
|
64
|
+
Return the metadata for a single job.
|
65
|
+
"""
|
66
|
+
now = time.perf_counter()
|
67
|
+
_job_metadata_cache = self.__dict__.get('_job_metadata_cache', None)
|
68
|
+
_job_metadata_timestamp = (
|
69
|
+
_job_metadata_cache.get(name, {}).get('timestamp', None)
|
70
|
+
) if _job_metadata_cache is not None else None
|
71
|
+
|
72
|
+
if (
|
73
|
+
_job_metadata_timestamp is not None
|
74
|
+
and (now - _job_metadata_timestamp) < JOB_METADATA_CACHE_SECONDS
|
75
|
+
):
|
76
|
+
if debug:
|
77
|
+
dprint(f"Returning cached metadata for job '{name}'.")
|
78
|
+
return _job_metadata_cache[name]['metadata']
|
79
|
+
|
80
|
+
response = self.get(JOBS_ENDPOINT + f"/{name}", debug=debug)
|
81
|
+
if not response:
|
82
|
+
if debug:
|
83
|
+
msg = (
|
84
|
+
response.json()['detail']
|
85
|
+
if 'detail' in response.text
|
86
|
+
else response.text
|
87
|
+
)
|
88
|
+
warn(f"Failed to get metadata for job '{name}':\n{msg}")
|
89
|
+
return {}
|
90
|
+
|
91
|
+
metadata = response.json()
|
92
|
+
if _job_metadata_cache is None:
|
93
|
+
self._job_metadata_cache = {}
|
94
|
+
|
95
|
+
self._job_metadata_cache[name] = {
|
96
|
+
'timestamp': now,
|
97
|
+
'metadata': metadata,
|
98
|
+
}
|
99
|
+
return metadata
|
100
|
+
|
101
|
+
def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]:
|
102
|
+
"""
|
103
|
+
Return the daemon properties for a single job.
|
104
|
+
"""
|
105
|
+
metadata = self.get_job_metadata(name, debug=debug)
|
106
|
+
return metadata.get('daemon', {}).get('properties', {})
|
107
|
+
|
108
|
+
def get_job_status(self, name: str, debug: bool = False) -> str:
|
109
|
+
"""
|
110
|
+
Return the job's status.
|
111
|
+
"""
|
112
|
+
metadata = self.get_job_metadata(name, debug=debug)
|
113
|
+
return metadata.get('status', 'stopped')
|
114
|
+
|
115
|
+
def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
|
116
|
+
"""
|
117
|
+
Return a job's `began` timestamp, if it exists.
|
118
|
+
"""
|
119
|
+
properties = self.get_job_properties(name, debug=debug)
|
120
|
+
began_str = properties.get('daemon', {}).get('began', None)
|
121
|
+
if began_str is None:
|
122
|
+
return None
|
123
|
+
|
124
|
+
return began_str
|
125
|
+
|
126
|
+
def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
|
127
|
+
"""
|
128
|
+
Return a job's `ended` timestamp, if it exists.
|
129
|
+
"""
|
130
|
+
properties = self.get_job_properties(name, debug=debug)
|
131
|
+
ended_str = properties.get('daemon', {}).get('ended', None)
|
132
|
+
if ended_str is None:
|
133
|
+
return None
|
134
|
+
|
135
|
+
return ended_str
|
136
|
+
|
137
|
+
def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
|
138
|
+
"""
|
139
|
+
Return a job's `paused` timestamp, if it exists.
|
140
|
+
"""
|
141
|
+
properties = self.get_job_properties(name, debug=debug)
|
142
|
+
paused_str = properties.get('daemon', {}).get('paused', None)
|
143
|
+
if paused_str is None:
|
144
|
+
return None
|
145
|
+
|
146
|
+
return paused_str
|
147
|
+
|
148
|
+
def get_job_exists(self, name: str, debug: bool = False) -> bool:
|
149
|
+
"""
|
150
|
+
Return whether a job exists.
|
151
|
+
"""
|
152
|
+
response = self.get(JOBS_ENDPOINT + f'/{name}/exists', debug=debug)
|
153
|
+
if not response:
|
154
|
+
warn(f"Failed to determine whether job '{name}' exists.")
|
155
|
+
return False
|
156
|
+
|
157
|
+
return response.json()
|
158
|
+
|
159
|
+
|
160
|
+
def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
161
|
+
"""
|
162
|
+
Delete a job.
|
163
|
+
"""
|
164
|
+
response = self.delete(JOBS_ENDPOINT + f"/{name}", debug=debug)
|
165
|
+
if not response:
|
166
|
+
if 'detail' in response.text:
|
167
|
+
return False, response.json()['detail']
|
168
|
+
|
169
|
+
return False, response.text
|
170
|
+
|
171
|
+
return tuple(response.json())
|
172
|
+
|
173
|
+
|
174
|
+
def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
175
|
+
"""
|
176
|
+
Start a job.
|
177
|
+
"""
|
178
|
+
response = self.post(JOBS_ENDPOINT + f"/{name}/start", debug=debug)
|
179
|
+
if not response:
|
180
|
+
if 'detail' in response.text:
|
181
|
+
return False, response.json()['detail']
|
182
|
+
return False, response.text
|
183
|
+
|
184
|
+
return tuple(response.json())
|
185
|
+
|
186
|
+
|
187
|
+
def create_job(self, name: str, sysargs: List[str], debug: bool = False) -> SuccessTuple:
|
188
|
+
"""
|
189
|
+
Create a job.
|
190
|
+
"""
|
191
|
+
response = self.post(JOBS_ENDPOINT + f"/{name}", json=sysargs, debug=debug)
|
192
|
+
if not response:
|
193
|
+
if 'detail' in response.text:
|
194
|
+
return False, response.json()['detail']
|
195
|
+
return False, response.text
|
196
|
+
|
197
|
+
return tuple(response.json())
|
198
|
+
|
199
|
+
|
200
|
+
def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
201
|
+
"""
|
202
|
+
Stop a job.
|
203
|
+
"""
|
204
|
+
response = self.post(JOBS_ENDPOINT + f"/{name}/stop", debug=debug)
|
205
|
+
if not response:
|
206
|
+
if 'detail' in response.text:
|
207
|
+
return False, response.json()['detail']
|
208
|
+
return False, response.text
|
209
|
+
|
210
|
+
return tuple(response.json())
|
211
|
+
|
212
|
+
|
213
|
+
def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
214
|
+
"""
|
215
|
+
Pause a job.
|
216
|
+
"""
|
217
|
+
response = self.post(JOBS_ENDPOINT + f"/{name}/pause", debug=debug)
|
218
|
+
if not response:
|
219
|
+
if 'detail' in response.text:
|
220
|
+
return False, response.json()['detail']
|
221
|
+
return False, response.text
|
222
|
+
|
223
|
+
return tuple(response.json())
|
224
|
+
|
225
|
+
|
226
|
+
def get_logs(self, name: str, debug: bool = False) -> str:
|
227
|
+
"""
|
228
|
+
Return the logs for a job.
|
229
|
+
"""
|
230
|
+
response = self.get(LOGS_ENDPOINT + f"/{name}")
|
231
|
+
if not response:
|
232
|
+
raise ValueError(f"Cannot fetch logs for job '{name}':\n{response.text}")
|
233
|
+
|
234
|
+
return response.json()
|
235
|
+
|
236
|
+
|
237
|
+
def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]:
|
238
|
+
"""
|
239
|
+
Return the job's manual stop time.
|
240
|
+
"""
|
241
|
+
response = self.get(JOBS_ENDPOINT + f"/{name}/stop_time")
|
242
|
+
if not response:
|
243
|
+
warn(f"Failed to get stop time for job '{name}':\n{response.text}")
|
244
|
+
return None
|
245
|
+
|
246
|
+
data = response.json()
|
247
|
+
if data is None:
|
248
|
+
return None
|
249
|
+
|
250
|
+
return datetime.fromisoformat(data)
|
251
|
+
|
252
|
+
|
253
|
+
async def monitor_logs_async(
|
254
|
+
self,
|
255
|
+
name: str,
|
256
|
+
callback_function: Callable[[Any], Any],
|
257
|
+
input_callback_function: Callable[[], str],
|
258
|
+
stop_callback_function: Callable[[SuccessTuple], str],
|
259
|
+
stop_on_exit: bool = False,
|
260
|
+
strip_timestamps: bool = False,
|
261
|
+
accept_input: bool = True,
|
262
|
+
debug: bool = False,
|
263
|
+
):
|
264
|
+
"""
|
265
|
+
Monitor a job's log files and await a callback with the changes.
|
266
|
+
"""
|
267
|
+
from meerschaum.jobs import StopMonitoringLogs
|
268
|
+
from meerschaum.utils.formatting._jobs import strip_timestamp_from_line
|
269
|
+
|
270
|
+
websockets, websockets_exceptions = mrsm.attempt_import('websockets', 'websockets.exceptions')
|
271
|
+
protocol = 'ws' if self.URI.startswith('http://') else 'wss'
|
272
|
+
port = self.port if 'port' in self.__dict__ else ''
|
273
|
+
uri = f"{protocol}://{self.host}:{port}{LOGS_ENDPOINT}/{name}/ws"
|
274
|
+
|
275
|
+
async def _stdin_callback(client):
|
276
|
+
if input_callback_function is None:
|
277
|
+
return
|
278
|
+
|
279
|
+
if asyncio.iscoroutinefunction(input_callback_function):
|
280
|
+
data = await input_callback_function()
|
281
|
+
else:
|
282
|
+
data = input_callback_function()
|
283
|
+
|
284
|
+
await client.send(data)
|
285
|
+
|
286
|
+
async def _stop_callback(client):
|
287
|
+
try:
|
288
|
+
result = tuple(json.loads(await client.recv()))
|
289
|
+
except Exception as e:
|
290
|
+
warn(traceback.format_exc())
|
291
|
+
result = False, str(e)
|
292
|
+
|
293
|
+
if stop_callback_function is not None:
|
294
|
+
if asyncio.iscoroutinefunction(stop_callback_function):
|
295
|
+
await stop_callback_function(result)
|
296
|
+
else:
|
297
|
+
stop_callback_function(result)
|
298
|
+
|
299
|
+
if stop_on_exit:
|
300
|
+
raise StopMonitoringLogs
|
301
|
+
|
302
|
+
message_callbacks = {
|
303
|
+
JOBS_STDIN_MESSAGE: _stdin_callback,
|
304
|
+
JOBS_STOP_MESSAGE: _stop_callback,
|
305
|
+
}
|
306
|
+
|
307
|
+
async with websockets.connect(uri) as websocket:
|
308
|
+
try:
|
309
|
+
await websocket.send(self.token or 'no-login')
|
310
|
+
except websockets_exceptions.ConnectionClosedOK:
|
311
|
+
pass
|
312
|
+
|
313
|
+
while True:
|
314
|
+
try:
|
315
|
+
response = await websocket.recv()
|
316
|
+
callback = message_callbacks.get(response, None)
|
317
|
+
if callback is not None:
|
318
|
+
await callback(websocket)
|
319
|
+
continue
|
320
|
+
|
321
|
+
if strip_timestamps:
|
322
|
+
response = strip_timestamp_from_line(response)
|
323
|
+
|
324
|
+
if asyncio.iscoroutinefunction(callback_function):
|
325
|
+
await callback_function(response)
|
326
|
+
else:
|
327
|
+
callback_function(response)
|
328
|
+
except (KeyboardInterrupt, StopMonitoringLogs):
|
329
|
+
await websocket.close()
|
330
|
+
break
|
331
|
+
|
332
|
+
|
333
|
+
def monitor_logs(
|
334
|
+
self,
|
335
|
+
name: str,
|
336
|
+
callback_function: Callable[[Any], Any],
|
337
|
+
input_callback_function: Callable[[None], str],
|
338
|
+
stop_callback_function: Callable[[None], str],
|
339
|
+
stop_on_exit: bool = False,
|
340
|
+
strip_timestamps: bool = False,
|
341
|
+
accept_input: bool = True,
|
342
|
+
debug: bool = False,
|
343
|
+
):
|
344
|
+
"""
|
345
|
+
Monitor a job's log files and execute a callback with the changes.
|
346
|
+
"""
|
347
|
+
return asyncio.run(
|
348
|
+
self.monitor_logs_async(
|
349
|
+
name,
|
350
|
+
callback_function,
|
351
|
+
input_callback_function=input_callback_function,
|
352
|
+
stop_callback_function=stop_callback_function,
|
353
|
+
stop_on_exit=stop_on_exit,
|
354
|
+
strip_timestamps=strip_timestamps,
|
355
|
+
accept_input=accept_input,
|
356
|
+
debug=debug
|
357
|
+
)
|
358
|
+
)
|
359
|
+
|
360
|
+
def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
|
361
|
+
"""
|
362
|
+
Return whether a remote job is blocking on stdin.
|
363
|
+
"""
|
364
|
+
response = self.get(JOBS_ENDPOINT + f'/{name}/is_blocking_on_stdin', debug=debug)
|
365
|
+
if not response:
|
366
|
+
return False
|
367
|
+
|
368
|
+
return response.json()
|