meerschaum 2.3.0rc2__py3-none-any.whl → 2.3.0rc3__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/actions/delete.py +4 -0
- meerschaum/api/dash/callbacks/jobs.py +36 -44
- meerschaum/api/dash/jobs.py +24 -15
- meerschaum/api/routes/_actions.py +6 -5
- meerschaum/api/routes/_jobs.py +19 -1
- meerschaum/config/_jobs.py +1 -1
- meerschaum/config/_version.py +1 -1
- meerschaum/connectors/api/APIConnector.py +1 -0
- meerschaum/connectors/api/_jobs.py +13 -2
- meerschaum/jobs/_Job.py +15 -6
- meerschaum/jobs/_SystemdExecutor.py +72 -8
- meerschaum/jobs/__init__.py +6 -0
- meerschaum/utils/daemon/Daemon.py +14 -0
- meerschaum/utils/daemon/_names.py +1 -1
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/METADATA +1 -1
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/RECORD +22 -22
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/WHEEL +0 -0
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/top_level.txt +0 -0
- {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc3.dist-info}/zip-safe +0 -0
meerschaum/actions/delete.py
CHANGED
@@ -512,6 +512,7 @@ def _complete_delete_jobs(
|
|
512
512
|
get_executor_keys_from_context,
|
513
513
|
)
|
514
514
|
from meerschaum.utils.misc import remove_ansi
|
515
|
+
from meerschaum.connectors.parse import parse_executor_keys
|
515
516
|
|
516
517
|
executor_keys = (
|
517
518
|
executor_keys
|
@@ -520,6 +521,9 @@ def _complete_delete_jobs(
|
|
520
521
|
)
|
521
522
|
)
|
522
523
|
|
524
|
+
if parse_executor_keys(executor_keys, construct=False) is None:
|
525
|
+
return []
|
526
|
+
|
523
527
|
jobs = get_jobs(executor_keys, include_hidden=False)
|
524
528
|
if _get_job_method:
|
525
529
|
method_keys = [_get_job_method] if isinstance(_get_job_method, str) else _get_job_method
|
@@ -24,13 +24,13 @@ from dash import Patch
|
|
24
24
|
html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
|
25
25
|
import dash_bootstrap_components as dbc
|
26
26
|
from meerschaum.api.dash.components import alert_from_success_tuple, build_cards_grid
|
27
|
-
from meerschaum.utils.daemon import Daemon
|
28
27
|
from dash.exceptions import PreventUpdate
|
29
28
|
from meerschaum.api.dash.jobs import (
|
30
29
|
build_manage_job_buttons_div_children,
|
31
30
|
build_status_children,
|
32
31
|
build_process_timestamps_children,
|
33
32
|
)
|
33
|
+
from meerschaum.jobs import Job
|
34
34
|
from meerschaum.api.dash.users import is_session_authenticated
|
35
35
|
|
36
36
|
@dash_app.callback(
|
@@ -53,15 +53,11 @@ def download_job_logs(n_clicks):
|
|
53
53
|
raise PreventUpdate
|
54
54
|
|
55
55
|
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
56
|
-
|
57
|
-
daemon = Daemon(daemon_id=daemon_id)
|
56
|
+
job_name = component_dict['index']
|
58
57
|
now = datetime.now(timezone.utc)
|
59
|
-
filename = (
|
60
|
-
daemon.rotating_log.file_path.name[:(-1 * len('.log'))]
|
61
|
-
+ '_' + str(int(now.timestamp())) + '.log'
|
62
|
-
)
|
58
|
+
filename = job_name + '_' + str(int(now.timestamp())) + '.log'
|
63
59
|
return {
|
64
|
-
'content':
|
60
|
+
'content': job.get_logs(),
|
65
61
|
'filename': filename,
|
66
62
|
}
|
67
63
|
|
@@ -73,12 +69,14 @@ def download_job_logs(n_clicks):
|
|
73
69
|
Output({'type': 'process-timestamps-div', 'index': MATCH}, 'children'),
|
74
70
|
Input({'type': 'manage-job-button', 'action': ALL, 'index': MATCH}, 'n_clicks'),
|
75
71
|
State('session-store', 'data'),
|
72
|
+
State({'type': 'job-label-p', 'index': MATCH}, 'children'),
|
76
73
|
prevent_initial_call = True,
|
77
74
|
)
|
78
75
|
def manage_job_button_click(
|
79
|
-
|
80
|
-
|
81
|
-
|
76
|
+
n_clicks: Optional[int] = None,
|
77
|
+
session_data: Optional[Dict[str, Any]] = None,
|
78
|
+
job_label: Optional[str] = None,
|
79
|
+
):
|
82
80
|
"""
|
83
81
|
Start, stop, pause, or delete the given job.
|
84
82
|
"""
|
@@ -102,20 +100,20 @@ def manage_job_button_click(
|
|
102
100
|
raise PreventUpdate
|
103
101
|
|
104
102
|
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
105
|
-
|
103
|
+
job_name = component_dict['index']
|
106
104
|
manage_job_action = component_dict['action']
|
107
105
|
try:
|
108
|
-
|
106
|
+
job = Job(job_name, job_label.replace('\n', ' ') if job_label else None)
|
109
107
|
except Exception as e:
|
110
|
-
|
111
|
-
if
|
108
|
+
job = None
|
109
|
+
if job is None:
|
112
110
|
raise PreventUpdate
|
113
111
|
|
114
112
|
manage_functions = {
|
115
|
-
'start':
|
116
|
-
'stop':
|
117
|
-
'pause':
|
118
|
-
'delete':
|
113
|
+
'start': job.start,
|
114
|
+
'stop': job.stop,
|
115
|
+
'pause': job.pause,
|
116
|
+
'delete': job.delete,
|
119
117
|
}
|
120
118
|
if manage_job_action not in manage_functions:
|
121
119
|
return (
|
@@ -125,7 +123,7 @@ def manage_job_button_click(
|
|
125
123
|
dash.no_update,
|
126
124
|
)
|
127
125
|
|
128
|
-
old_status =
|
126
|
+
old_status = job.status
|
129
127
|
try:
|
130
128
|
success, msg = manage_functions[manage_job_action]()
|
131
129
|
except Exception as e:
|
@@ -136,15 +134,15 @@ def manage_job_button_click(
|
|
136
134
|
check_interval_seconds = 0.01
|
137
135
|
begin = time.perf_counter()
|
138
136
|
while (time.perf_counter() - begin) < timeout_seconds:
|
139
|
-
if
|
137
|
+
if job.status != old_status:
|
140
138
|
break
|
141
139
|
time.sleep(check_interval_seconds)
|
142
140
|
|
143
141
|
return (
|
144
142
|
alert_from_success_tuple((success, msg)),
|
145
|
-
build_manage_job_buttons_div_children(
|
146
|
-
build_status_children(
|
147
|
-
build_process_timestamps_children(
|
143
|
+
build_manage_job_buttons_div_children(job),
|
144
|
+
build_status_children(job),
|
145
|
+
build_process_timestamps_children(job),
|
148
146
|
)
|
149
147
|
|
150
148
|
dash_app.clientside_callback(
|
@@ -165,7 +163,7 @@ dash_app.clientside_callback(
|
|
165
163
|
}
|
166
164
|
|
167
165
|
const triggered_id = dash_clientside.callback_context.triggered_id;
|
168
|
-
const
|
166
|
+
const job_name = triggered_id["index"];
|
169
167
|
|
170
168
|
iframe = document.getElementById('webterm-iframe');
|
171
169
|
if (!iframe){ return dash_clientside.no_update; }
|
@@ -174,7 +172,7 @@ dash_app.clientside_callback(
|
|
174
172
|
{
|
175
173
|
action: "show",
|
176
174
|
subaction: "logs",
|
177
|
-
subaction_text:
|
175
|
+
subaction_text: job_name,
|
178
176
|
},
|
179
177
|
url
|
180
178
|
);
|
@@ -197,44 +195,38 @@ dash_app.clientside_callback(
|
|
197
195
|
prevent_initial_call = True,
|
198
196
|
)
|
199
197
|
def refresh_jobs_on_interval(
|
200
|
-
|
201
|
-
|
202
|
-
|
198
|
+
n_intervals: Optional[int] = None,
|
199
|
+
session_data: Optional[Dict[str, Any]] = None,
|
200
|
+
):
|
203
201
|
"""
|
204
202
|
When the jobs refresh interval fires, rebuild the jobs' onscreen components.
|
205
203
|
"""
|
206
204
|
session_id = session_data.get('session-id', None)
|
207
205
|
is_authenticated = is_session_authenticated(session_id)
|
208
206
|
|
209
|
-
|
207
|
+
job_names = [
|
210
208
|
component_dict['id']['index']
|
211
209
|
for component_dict in dash.callback_context.outputs_grouping[0]
|
212
210
|
]
|
213
211
|
|
214
|
-
### NOTE: The
|
215
|
-
|
216
|
-
for daemon_id in daemon_ids:
|
217
|
-
try:
|
218
|
-
daemon = Daemon(daemon_id=daemon_id)
|
219
|
-
except Exception as e:
|
220
|
-
daemon = None
|
221
|
-
daemons.append(daemon)
|
212
|
+
### NOTE: The job may have been deleted, but the card may still exist.
|
213
|
+
jobs = [Job(name) for name in job_names]
|
222
214
|
|
223
215
|
return (
|
224
216
|
[
|
225
217
|
(
|
226
|
-
build_manage_job_buttons_div_children(
|
218
|
+
build_manage_job_buttons_div_children(job)
|
227
219
|
if is_authenticated
|
228
220
|
else []
|
229
221
|
)
|
230
|
-
for
|
222
|
+
for job in jobs
|
231
223
|
],
|
232
224
|
[
|
233
|
-
build_status_children(
|
234
|
-
for
|
225
|
+
build_status_children(job)
|
226
|
+
for job in jobs
|
235
227
|
],
|
236
228
|
[
|
237
|
-
build_process_timestamps_children(
|
238
|
-
for
|
229
|
+
build_process_timestamps_children(job)
|
230
|
+
for job in jobs
|
239
231
|
],
|
240
232
|
)
|
meerschaum/api/dash/jobs.py
CHANGED
@@ -15,12 +15,12 @@ from meerschaum.api.dash.users import is_session_authenticated
|
|
15
15
|
from meerschaum.api import CHECK_UPDATE
|
16
16
|
dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
|
17
17
|
html, dcc = import_html(), import_dcc()
|
18
|
-
dateutil_parser = attempt_import('dateutil.parser', check_update=CHECK_UPDATE)
|
19
18
|
from meerschaum.jobs import (
|
20
19
|
get_jobs,
|
21
20
|
get_running_jobs,
|
22
21
|
get_paused_jobs,
|
23
22
|
get_stopped_jobs,
|
23
|
+
get_executor_keys_from_context,
|
24
24
|
Job,
|
25
25
|
)
|
26
26
|
from meerschaum.config import get_config
|
@@ -33,20 +33,21 @@ STATUS_EMOJI: Dict[str, str] = {
|
|
33
33
|
'dne': get_config('formatting', 'emoji', 'failure')
|
34
34
|
}
|
35
35
|
|
36
|
+
EXECUTOR_KEYS: str = get_executor_keys_from_context()
|
37
|
+
|
36
38
|
def get_jobs_cards(state: WebState):
|
37
39
|
"""
|
38
40
|
Build cards and alerts lists for jobs.
|
39
41
|
"""
|
40
|
-
jobs = get_jobs(include_hidden=False)
|
42
|
+
jobs = get_jobs(executor_keys=EXECUTOR_KEYS, include_hidden=False)
|
41
43
|
session_id = state['session-store.data'].get('session-id', None)
|
42
44
|
is_authenticated = is_session_authenticated(session_id)
|
43
45
|
|
44
46
|
cards = []
|
45
47
|
|
46
48
|
for name, job in jobs.items():
|
47
|
-
d = job.daemon
|
48
49
|
footer_children = html.Div(
|
49
|
-
build_process_timestamps_children(
|
50
|
+
build_process_timestamps_children(job),
|
50
51
|
id = {'type': 'process-timestamps-div', 'index': name},
|
51
52
|
)
|
52
53
|
follow_logs_button = dbc.DropdownMenuItem(
|
@@ -62,7 +63,7 @@ def get_jobs_cards(state: WebState):
|
|
62
63
|
)
|
63
64
|
header_children = [
|
64
65
|
html.Div(
|
65
|
-
build_status_children(
|
66
|
+
build_status_children(job),
|
66
67
|
id={'type': 'manage-job-status-div', 'index': name},
|
67
68
|
style={'float': 'left'},
|
68
69
|
),
|
@@ -83,15 +84,16 @@ def get_jobs_cards(state: WebState):
|
|
83
84
|
html.H4(html.B(name), className="card-title"),
|
84
85
|
html.Div(
|
85
86
|
html.P(
|
86
|
-
|
87
|
+
job.label,
|
87
88
|
className="card-text job-card-text",
|
88
89
|
style={"word-wrap": "break-word"},
|
90
|
+
id={'type': 'job-label-p', 'index': name},
|
89
91
|
),
|
90
92
|
style={"white-space": "pre-wrap"},
|
91
93
|
),
|
92
94
|
html.Div(
|
93
95
|
(
|
94
|
-
build_manage_job_buttons_div_children(
|
96
|
+
build_manage_job_buttons_div_children(job)
|
95
97
|
if is_authenticated
|
96
98
|
else []
|
97
99
|
),
|
@@ -179,13 +181,14 @@ def build_manage_job_buttons(job: Job):
|
|
179
181
|
},
|
180
182
|
)
|
181
183
|
buttons = []
|
182
|
-
|
184
|
+
status = job.status
|
185
|
+
if status in ('stopped', 'paused'):
|
183
186
|
buttons.append(start_button)
|
184
|
-
if
|
187
|
+
if status == 'stopped':
|
185
188
|
buttons.append(delete_button)
|
186
|
-
if
|
189
|
+
if status in ('running',):
|
187
190
|
buttons.append(pause_button)
|
188
|
-
if
|
191
|
+
if status in ('running', 'paused'):
|
189
192
|
buttons.append(stop_button)
|
190
193
|
|
191
194
|
return buttons
|
@@ -193,7 +196,7 @@ def build_manage_job_buttons(job: Job):
|
|
193
196
|
|
194
197
|
def build_status_children(job: Job) -> List[html.P]:
|
195
198
|
"""
|
196
|
-
Return the status HTML component for this
|
199
|
+
Return the status HTML component for this Job.
|
197
200
|
"""
|
198
201
|
if job is None:
|
199
202
|
return STATUS_EMOJI['dne']
|
@@ -217,10 +220,16 @@ def build_process_timestamps_children(job: Job) -> List[dbc.Row]:
|
|
217
220
|
return []
|
218
221
|
|
219
222
|
children = []
|
220
|
-
for timestamp_key,
|
221
|
-
|
223
|
+
for timestamp_key, timestamp in sorted_dict(
|
224
|
+
{
|
225
|
+
'began': job.began,
|
226
|
+
'paused': job.paused,
|
227
|
+
'ended': job.ended,
|
228
|
+
}
|
222
229
|
).items():
|
223
|
-
timestamp
|
230
|
+
if timestamp is None:
|
231
|
+
continue
|
232
|
+
|
224
233
|
timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M UTC')
|
225
234
|
children.append(
|
226
235
|
dbc.Row(
|
@@ -26,6 +26,7 @@ from meerschaum.actions import actions
|
|
26
26
|
import meerschaum.core
|
27
27
|
from meerschaum.config import get_config
|
28
28
|
from meerschaum._internal.arguments._parse_arguments import parse_dict_to_sysargs, parse_arguments
|
29
|
+
from meerschaum.api.routes._jobs import clean_sysargs
|
29
30
|
|
30
31
|
actions_endpoint = endpoints['actions']
|
31
32
|
|
@@ -115,11 +116,11 @@ async def do_action_websocket(websocket: WebSocket):
|
|
115
116
|
if not auth_success:
|
116
117
|
await websocket.close()
|
117
118
|
|
118
|
-
sysargs = await websocket.receive_json()
|
119
|
-
kwargs = parse_arguments(sysargs)
|
120
|
-
_ = kwargs.pop('executor_keys', None)
|
121
|
-
_ = kwargs.pop('shell', None)
|
122
|
-
sysargs = parse_dict_to_sysargs(kwargs)
|
119
|
+
sysargs = clean_sysargs(await websocket.receive_json())
|
120
|
+
# kwargs = parse_arguments(sysargs)
|
121
|
+
# _ = kwargs.pop('executor_keys', None)
|
122
|
+
# _ = kwargs.pop('shell', None)
|
123
|
+
# sysargs = parse_dict_to_sysargs(kwargs)
|
123
124
|
|
124
125
|
job = Job(
|
125
126
|
job_name,
|
meerschaum/api/routes/_jobs.py
CHANGED
@@ -107,6 +107,24 @@ def get_job(
|
|
107
107
|
}
|
108
108
|
|
109
109
|
|
110
|
+
def clean_sysargs(sysargs: List[str]) -> List[str]:
|
111
|
+
"""
|
112
|
+
Remove the executor flag or leading `api {label}` action.
|
113
|
+
"""
|
114
|
+
clean_sysargs = []
|
115
|
+
executor_flag = False
|
116
|
+
for arg in sysargs:
|
117
|
+
if arg in ('-e', '--executor', 'api'):
|
118
|
+
executor_flag = True
|
119
|
+
continue
|
120
|
+
if executor_flag:
|
121
|
+
executor_flag = False
|
122
|
+
continue
|
123
|
+
|
124
|
+
clean_sysargs.append(arg)
|
125
|
+
return clean_sysargs
|
126
|
+
|
127
|
+
|
110
128
|
@app.post(endpoints['jobs'] + '/{name}', tags=['Jobs'])
|
111
129
|
def create_job(
|
112
130
|
name: str,
|
@@ -118,7 +136,7 @@ def create_job(
|
|
118
136
|
"""
|
119
137
|
Create and start a new job.
|
120
138
|
"""
|
121
|
-
job = Job(name, sysargs, executor_keys=EXECUTOR_KEYS)
|
139
|
+
job = Job(name, clean_sysargs(sysargs), executor_keys=EXECUTOR_KEYS)
|
122
140
|
if job.exists():
|
123
141
|
raise fastapi.HTTPException(
|
124
142
|
status_code=409,
|
meerschaum/config/_jobs.py
CHANGED
meerschaum/config/_version.py
CHANGED
@@ -112,7 +112,7 @@ def get_job_status(self, name: str, debug: bool = False) -> str:
|
|
112
112
|
metadata = self.get_job_metadata(name, debug=debug)
|
113
113
|
return metadata.get('status', 'stopped')
|
114
114
|
|
115
|
-
def get_job_began(self, name: str, debug: bool = False) -> str:
|
115
|
+
def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
|
116
116
|
"""
|
117
117
|
Return a job's `began` timestamp, if it exists.
|
118
118
|
"""
|
@@ -123,7 +123,7 @@ def get_job_began(self, name: str, debug: bool = False) -> str:
|
|
123
123
|
|
124
124
|
return began_str
|
125
125
|
|
126
|
-
def get_job_ended(self, name: str, debug: bool = False) -> str:
|
126
|
+
def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
|
127
127
|
"""
|
128
128
|
Return a job's `ended` timestamp, if it exists.
|
129
129
|
"""
|
@@ -134,6 +134,17 @@ def get_job_ended(self, name: str, debug: bool = False) -> str:
|
|
134
134
|
|
135
135
|
return ended_str
|
136
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
|
+
|
137
148
|
def get_job_exists(self, name: str, debug: bool = False) -> bool:
|
138
149
|
"""
|
139
150
|
Return whether a job exists.
|
meerschaum/jobs/_Job.py
CHANGED
@@ -740,7 +740,7 @@ class Job:
|
|
740
740
|
The datetime when the job began running.
|
741
741
|
"""
|
742
742
|
if self.executor is not None:
|
743
|
-
began_str = self.executor.get_job_began(name)
|
743
|
+
began_str = self.executor.get_job_began(self.name)
|
744
744
|
if began_str is None:
|
745
745
|
return None
|
746
746
|
return (
|
@@ -762,6 +762,8 @@ class Job:
|
|
762
762
|
"""
|
763
763
|
if self.executor is not None:
|
764
764
|
ended_str = self.executor.get_job_ended(self.name)
|
765
|
+
if ended_str is None:
|
766
|
+
return None
|
765
767
|
return (
|
766
768
|
datetime.fromisoformat(ended_str)
|
767
769
|
.astimezone(timezone.utc)
|
@@ -779,6 +781,16 @@ class Job:
|
|
779
781
|
"""
|
780
782
|
The datetime when the job was suspended while running.
|
781
783
|
"""
|
784
|
+
if self.executor is not None:
|
785
|
+
paused_str = self.executor.get_job_paused(self.name)
|
786
|
+
if paused_str is None:
|
787
|
+
return None
|
788
|
+
return (
|
789
|
+
datetime.fromisoformat(paused_str)
|
790
|
+
.astimezone(timezone.utc)
|
791
|
+
.replace(tzinfo=None)
|
792
|
+
)
|
793
|
+
|
782
794
|
paused_str = self.daemon.properties.get('process', {}).get('paused', None)
|
783
795
|
if paused_str is None:
|
784
796
|
return None
|
@@ -796,11 +808,8 @@ class Job:
|
|
796
808
|
if not self.daemon.stop_path.exists():
|
797
809
|
return None
|
798
810
|
|
799
|
-
|
800
|
-
|
801
|
-
stop_data = json.load(f)
|
802
|
-
except Exception as e:
|
803
|
-
warn(f"Failed to read stop file for {self}:\n{e}")
|
811
|
+
stop_data = self.daemon._read_stop_file()
|
812
|
+
if not stop_data:
|
804
813
|
return None
|
805
814
|
|
806
815
|
stop_time_str = stop_data.get('stop_time', None)
|
@@ -19,6 +19,7 @@ from functools import partial
|
|
19
19
|
import meerschaum as mrsm
|
20
20
|
from meerschaum.jobs import Job, Executor, make_executor
|
21
21
|
from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union, Optional, Callable
|
22
|
+
from meerschaum.config import get_config
|
22
23
|
from meerschaum.config.static import STATIC_CONFIG
|
23
24
|
from meerschaum.utils.warnings import warn, dprint
|
24
25
|
from meerschaum._internal.arguments._parse_arguments import parse_arguments
|
@@ -141,7 +142,8 @@ class SystemdExecutor(Executor):
|
|
141
142
|
STATIC_CONFIG['environment']['systemd_log_path']: service_logs_path.as_posix(),
|
142
143
|
STATIC_CONFIG['environment']['systemd_result_path']: result_path.as_posix(),
|
143
144
|
STATIC_CONFIG['environment']['systemd_stdin_path']: socket_path.as_posix(),
|
144
|
-
|
145
|
+
'LINES': get_config('jobs', 'terminal', 'lines'),
|
146
|
+
'COLUMNS': get_config('jobs', 'terminal', 'columns'),
|
145
147
|
})
|
146
148
|
environment_lines = [
|
147
149
|
f"Environment={key}={val}"
|
@@ -259,7 +261,10 @@ class SystemdExecutor(Executor):
|
|
259
261
|
return None
|
260
262
|
|
261
263
|
psutil = mrsm.attempt_import('psutil')
|
262
|
-
|
264
|
+
try:
|
265
|
+
return psutil.Process(pid)
|
266
|
+
except Exception:
|
267
|
+
return None
|
263
268
|
|
264
269
|
def get_job_status(self, name: str, debug: bool = False) -> str:
|
265
270
|
"""
|
@@ -271,13 +276,19 @@ class SystemdExecutor(Executor):
|
|
271
276
|
debug=debug,
|
272
277
|
)
|
273
278
|
|
279
|
+
if output == 'activating':
|
280
|
+
return 'running'
|
281
|
+
|
274
282
|
if output == 'active':
|
275
283
|
process = self.get_job_process(name, debug=debug)
|
276
284
|
if process is None:
|
277
285
|
return 'stopped'
|
278
286
|
|
279
|
-
|
280
|
-
|
287
|
+
try:
|
288
|
+
if process.status() == 'stopped':
|
289
|
+
return 'paused'
|
290
|
+
except Exception:
|
291
|
+
return 'stopped'
|
281
292
|
|
282
293
|
return 'running'
|
283
294
|
|
@@ -310,7 +321,7 @@ class SystemdExecutor(Executor):
|
|
310
321
|
|
311
322
|
return None
|
312
323
|
|
313
|
-
def get_job_began(self, name: str, debug: bool = False) -> Union[
|
324
|
+
def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
|
314
325
|
"""
|
315
326
|
Return when a job began running.
|
316
327
|
"""
|
@@ -326,10 +337,62 @@ class SystemdExecutor(Executor):
|
|
326
337
|
if not output.startswith('ActiveEnterTimestamp'):
|
327
338
|
return None
|
328
339
|
|
340
|
+
dt_str = output.split('=')[-1]
|
341
|
+
if not dt_str:
|
342
|
+
return None
|
343
|
+
|
329
344
|
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
330
|
-
|
345
|
+
try:
|
346
|
+
dt = dateutil_parser.parse(dt_str)
|
347
|
+
except Exception as e:
|
348
|
+
warn(f"Cannot parse '{output}' as a datetime:\n{e}")
|
349
|
+
return None
|
350
|
+
|
331
351
|
return dt.astimezone(timezone.utc).isoformat()
|
332
352
|
|
353
|
+
def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
|
354
|
+
"""
|
355
|
+
Return when a job began running.
|
356
|
+
"""
|
357
|
+
output = self.run_command(
|
358
|
+
[
|
359
|
+
'show',
|
360
|
+
self.get_service_name(name, debug=debug),
|
361
|
+
'--property=InactiveEnterTimestamp'
|
362
|
+
],
|
363
|
+
as_output=True,
|
364
|
+
debug=debug,
|
365
|
+
)
|
366
|
+
if not output.startswith('InactiveEnterTimestamp'):
|
367
|
+
return None
|
368
|
+
|
369
|
+
dt_str = output.split('=')[-1]
|
370
|
+
if not dt_str:
|
371
|
+
return None
|
372
|
+
|
373
|
+
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
374
|
+
|
375
|
+
try:
|
376
|
+
dt = dateutil_parser.parse(dt_str)
|
377
|
+
except Exception as e:
|
378
|
+
warn(f"Cannot parse '{output}' as a datetime:\n{e}")
|
379
|
+
return None
|
380
|
+
return dt.astimezone(timezone.utc).isoformat()
|
381
|
+
|
382
|
+
def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
|
383
|
+
"""
|
384
|
+
Return when a job was paused.
|
385
|
+
"""
|
386
|
+
job = self.get_hidden_job(name, debug=debug)
|
387
|
+
if self.get_job_status(name, debug=debug) != 'paused':
|
388
|
+
return None
|
389
|
+
|
390
|
+
stop_time = job.stop_time
|
391
|
+
if stop_time is None:
|
392
|
+
return None
|
393
|
+
|
394
|
+
return stop_time.isoformat()
|
395
|
+
|
333
396
|
def get_job_result(self, name: str, debug: bool = False) -> SuccessTuple:
|
334
397
|
"""
|
335
398
|
Return the job's result SuccessTuple.
|
@@ -495,12 +558,13 @@ class SystemdExecutor(Executor):
|
|
495
558
|
debug=debug,
|
496
559
|
)
|
497
560
|
|
561
|
+
check_timeout_interval = get_config('jobs', 'check_timeout_interval_seconds')
|
498
562
|
loop_start = time.perf_counter()
|
499
|
-
while (time.perf_counter() - loop_start) <
|
563
|
+
while (time.perf_counter() - loop_start) < get_config('jobs', 'timeout_seconds'):
|
500
564
|
if self.get_job_status(name, debug=debug) == 'stopped':
|
501
565
|
return True, 'Success'
|
502
566
|
|
503
|
-
time.sleep(
|
567
|
+
time.sleep(check_timeout_interval)
|
504
568
|
|
505
569
|
return self.run_command(
|
506
570
|
['stop', self.get_service_name(name, debug=debug)],
|
meerschaum/jobs/__init__.py
CHANGED
@@ -102,6 +102,12 @@ def get_jobs(
|
|
102
102
|
)
|
103
103
|
return {**local_jobs, **systemd_jobs}
|
104
104
|
|
105
|
+
if executor_keys == 'local':
|
106
|
+
return _get_local_jobs()
|
107
|
+
|
108
|
+
if executor_keys == 'systemd':
|
109
|
+
return _get_systemd_jobs()
|
110
|
+
|
105
111
|
try:
|
106
112
|
_ = parse_executor_keys(executor_keys, construct=False)
|
107
113
|
conn = parse_executor_keys(executor_keys)
|
@@ -623,6 +623,20 @@ class Daemon:
|
|
623
623
|
|
624
624
|
return True, "Success"
|
625
625
|
|
626
|
+
def _read_stop_file(self) -> Dict[str, Any]:
|
627
|
+
"""
|
628
|
+
Read the stop file if it exists.
|
629
|
+
"""
|
630
|
+
if not self.stop_path.exists():
|
631
|
+
return {}
|
632
|
+
|
633
|
+
try:
|
634
|
+
with open(self.stop_path, 'r', encoding='utf-8') as f:
|
635
|
+
data = json.load(f)
|
636
|
+
return data
|
637
|
+
except Exception:
|
638
|
+
return {}
|
639
|
+
|
626
640
|
def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
|
627
641
|
"""
|
628
642
|
Handle `SIGTERM` within the `Daemon` context.
|
@@ -28,7 +28,7 @@ meerschaum/actions/bootstrap.py,sha256=9D3cBHzgZbZyWy-Y7iQgk9bpTbKEhumFKbIIThZgP
|
|
28
28
|
meerschaum/actions/clear.py,sha256=OoFZE0bK5m8s3GLNZcixuVT0DMj1izXVxGCATcmUGbI,4851
|
29
29
|
meerschaum/actions/copy.py,sha256=hLRc81oVBjnV22lpeSDb-HsDX2on9K2u6C1H8qJITNQ,6844
|
30
30
|
meerschaum/actions/deduplicate.py,sha256=puYyxeFYEUy1Sd2IOcZB2e6MrNxAZl2bTLmNzFDkCiw,1167
|
31
|
-
meerschaum/actions/delete.py,sha256=
|
31
|
+
meerschaum/actions/delete.py,sha256=OidLnYT4E3OEHGHOeHOHbL3FFJonE7iR_3sPCj0azK0,18974
|
32
32
|
meerschaum/actions/drop.py,sha256=Hd5h4rrWd7qL2rTqglsTonUsEoH7qQlsfqNFSHGeqr0,2453
|
33
33
|
meerschaum/actions/edit.py,sha256=6d1Y8Ejd4zQWVARvx04cvbGyRA2r4ihJXWqMFiFX0aM,11740
|
34
34
|
meerschaum/actions/install.py,sha256=jdhOrR_KlvinTKr0YJNkUHsnh5EY6OzA7cRq0Vnp1oU,7494
|
@@ -61,7 +61,7 @@ meerschaum/api/dash/actions.py,sha256=eUClPPdNVNSCtyq8Ecr1saasxj5VBgd1gcXejvQxXE
|
|
61
61
|
meerschaum/api/dash/components.py,sha256=oGX7HFzNCi37iMww747iL3bbB52ZTt9tFa2iLohffg4,6399
|
62
62
|
meerschaum/api/dash/connectors.py,sha256=nJxBOFldtCMJLYjUSVYZwX5BO-LNjTNHgoEaXe-0XMo,843
|
63
63
|
meerschaum/api/dash/graphs.py,sha256=wJUDWzcLN8-C3xko6rj0F2v7Rt8YDkSXoVkkXJjYGIk,2046
|
64
|
-
meerschaum/api/dash/jobs.py,sha256=
|
64
|
+
meerschaum/api/dash/jobs.py,sha256=PbQgo1i3iPyysMJkkMRzZZOuV9vHzFShjJltiWsWUKo,7456
|
65
65
|
meerschaum/api/dash/keys.py,sha256=hzEVeN60SAfVTVSO5lajGaykxRIKGhj9Ph00HRJnNoE,12598
|
66
66
|
meerschaum/api/dash/pipes.py,sha256=RVVYIJJy4G01cXou9OT8Gr5ZsygQ_GUqJDDhU1akhi8,20615
|
67
67
|
meerschaum/api/dash/plugins.py,sha256=JiQjeeCT21MoPDo81pkTXw9viqIH6xpivQgZEO4qtzQ,3676
|
@@ -78,7 +78,7 @@ meerschaum/api/dash/assets/logo_500x500.png,sha256=9EUtf6wQcEZTXHKfQ2kjNXod6Rn_4
|
|
78
78
|
meerschaum/api/dash/callbacks/__init__.py,sha256=CKOkt7PIHbYxX9aDab2xPDuufIAQjSR5MVX-hU8Zi2w,450
|
79
79
|
meerschaum/api/dash/callbacks/custom.py,sha256=N9pVolAF8sIuJD3V6xBSgS7k8THJo_f8d1qAoh1Kg60,1161
|
80
80
|
meerschaum/api/dash/callbacks/dashboard.py,sha256=8Rjxuw8uGww6oRfwRUC_mjm6i7aD0dVW0CeY2a7r7Yo,33101
|
81
|
-
meerschaum/api/dash/callbacks/jobs.py,sha256=
|
81
|
+
meerschaum/api/dash/callbacks/jobs.py,sha256=Xvw17za3tNvgu-yHzNku_0pZVK17vfEWKEy_DLtgo70,7407
|
82
82
|
meerschaum/api/dash/callbacks/login.py,sha256=oUGAo7gokAQDFgF4dBHaGf-An04JjkrGFHxSGB-ZMqU,2825
|
83
83
|
meerschaum/api/dash/callbacks/plugins.py,sha256=7CrwwbBI2N3DR4Yb1bKmrtJ3cAQ3dVv1l6E_AtZ6Wng,2777
|
84
84
|
meerschaum/api/dash/callbacks/register.py,sha256=9AgTcl--TPt6oETKzirQCkH9Azlm3XrU9U6XJM2b8Lk,3600
|
@@ -115,10 +115,10 @@ meerschaum/api/resources/templates/old_index.html,sha256=BDeOlcXhSsBH3-NaRtuX4Z1
|
|
115
115
|
meerschaum/api/resources/templates/secret.html,sha256=0QWkm4ZoN81Aw1pd2-62rGCvx3nXPHfFUoegj3Iy8Ls,141
|
116
116
|
meerschaum/api/resources/templates/termpage.html,sha256=qspXRuOkzqOn2mXw9mmUldzsvOHq_LyaywQ29CUevp0,4527
|
117
117
|
meerschaum/api/routes/__init__.py,sha256=jbkeFNl51Tg8aT5gWe560ZLZLojFJsLMe5IENRjRkb0,606
|
118
|
-
meerschaum/api/routes/_actions.py,sha256
|
118
|
+
meerschaum/api/routes/_actions.py,sha256=-UYsRkDvdv9HG9AkaaLjAGWXkdBYYGLFWz85vkvHtKc,4592
|
119
119
|
meerschaum/api/routes/_connectors.py,sha256=NNbcn5xWhKqw2PqueSEaqRaZ95hFGDKazG5lE7gsssc,1849
|
120
120
|
meerschaum/api/routes/_index.py,sha256=QI6CBo6pI2Zi0a6fJHDjZfiLa9f4okb0BGe3A_JD0kM,578
|
121
|
-
meerschaum/api/routes/_jobs.py,sha256=
|
121
|
+
meerschaum/api/routes/_jobs.py,sha256=593jLAdF2N28USjLEfyIABpD3c4CuajLDbVZu41WuOU,10977
|
122
122
|
meerschaum/api/routes/_login.py,sha256=psPKmFkXgYVX83NepqwIhaLsQ5uWgOc4F2QZtPGxY1A,2482
|
123
123
|
meerschaum/api/routes/_misc.py,sha256=05--9ZVFeaCgZrHER2kA3SYdK4TyfkEXOCjLvPbum-w,2469
|
124
124
|
meerschaum/api/routes/_pipes.py,sha256=1gBuE4E-QvIK_kmbmiw7uLcXjnIobFI1t4tb2skpp6E,21592
|
@@ -133,14 +133,14 @@ meerschaum/config/_default.py,sha256=8ryAQV9yYkpIZaVYe9W7dm5c-WAu_lX1jSKGLLo4FtQ
|
|
133
133
|
meerschaum/config/_edit.py,sha256=_kabgFbJdI5kcLs-JIsoaTo0JdyxnPnBdrlTyTAgPm8,8236
|
134
134
|
meerschaum/config/_environment.py,sha256=Vv4DLDfc2vKLbCLsMvkQDj77K4kEvHKEBmUBo-wCrgo,4419
|
135
135
|
meerschaum/config/_formatting.py,sha256=OMuqS1EWOsj_34wSs2tOqGIWci3bTMIZ5l-uelZgsIM,6672
|
136
|
-
meerschaum/config/_jobs.py,sha256=
|
136
|
+
meerschaum/config/_jobs.py,sha256=gS_4mMGdmVP7WB4V5Sz8kYP0HmhWcMY2jSWGR7jX6cw,1279
|
137
137
|
meerschaum/config/_patch.py,sha256=21N30q1ANmWMDQ-2RUjpMx7KafWfPQ3lKx9rrMqg1s4,1526
|
138
138
|
meerschaum/config/_paths.py,sha256=d6CKJfMmc9JrAW0E9Kpyy6bKQiB6yeHqZx91-cxKJIQ,9677
|
139
139
|
meerschaum/config/_preprocess.py,sha256=-AEA8m_--KivZwTQ1sWN6LTn5sio_fUr2XZ51BO6wLs,1220
|
140
140
|
meerschaum/config/_read_config.py,sha256=WFZKIXZMDe_ca0ES7ivgM_mnwShvFxLdoeisT_X5-h0,14720
|
141
141
|
meerschaum/config/_shell.py,sha256=46_m49Txc5q1rGfCgO49ca48BODx45DQJi8D0zz1R18,4245
|
142
142
|
meerschaum/config/_sync.py,sha256=oK2ZujO2T1he08BXCFyiniBUevNGWSQKXLcS_jRv_7Y,4155
|
143
|
-
meerschaum/config/_version.py,sha256=
|
143
|
+
meerschaum/config/_version.py,sha256=Z6PjBfAKMgVlUvxdR1JT93ZJZE4LQbhVOjo8PSU0nm4,74
|
144
144
|
meerschaum/config/paths.py,sha256=JjibeGN3YAdSNceRwsd42aNmeUrIgM6ndzC8qZAmNI0,621
|
145
145
|
meerschaum/config/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
146
146
|
meerschaum/config/stack/__init__.py,sha256=Yt7GNzC_hz7iUDZ4gVho_lugJO2DnXgnMtsMG_ReoRg,9114
|
@@ -153,11 +153,11 @@ meerschaum/connectors/Connector.py,sha256=utNV3Fy5DhUVbQE-vtm7enH5rH2gxQERmgmP7P
|
|
153
153
|
meerschaum/connectors/__init__.py,sha256=b1RdpMUNWtA5Q4KbymeUt9HtEiMWUHf0BBXzcl4itkc,12557
|
154
154
|
meerschaum/connectors/parse.py,sha256=sDeI2OIS9Inwhbn9jkFAXxOPnmmAHqsuHiiHfWjVnSA,4307
|
155
155
|
meerschaum/connectors/poll.py,sha256=gIY9TvFBqMvMNQvR0O2No7koLLz2PjfExBr_Dsosgpg,7363
|
156
|
-
meerschaum/connectors/api/APIConnector.py,sha256=
|
156
|
+
meerschaum/connectors/api/APIConnector.py,sha256=S_QL44WODA2uisIcC8wk_mix_3GhBzUXZjfon29P5lk,4958
|
157
157
|
meerschaum/connectors/api/__init__.py,sha256=JwKrGtuE5aOd2VnsRwudFBYyBf5IxczOwPVdNvCUgSQ,205
|
158
158
|
meerschaum/connectors/api/_actions.py,sha256=KS2Ib5_yHuN0Fw86wyg0bXORV2xY4m3DQrBtcqIIjnY,2343
|
159
159
|
meerschaum/connectors/api/_fetch.py,sha256=Khq9AFr1nk8Dsmcedb77aWhAuHw0JGgVeahDG95Q5MQ,2072
|
160
|
-
meerschaum/connectors/api/_jobs.py,sha256=
|
160
|
+
meerschaum/connectors/api/_jobs.py,sha256=XlZXuTbkqDzrp6WgoD8OskGUfrKf2VbayhuB_WbRkzw,11066
|
161
161
|
meerschaum/connectors/api/_login.py,sha256=5GsD-B214vr5EYfM3XrTUs1sTFApxZA-9dNxq8oNSyg,2050
|
162
162
|
meerschaum/connectors/api/_misc.py,sha256=OZRZBYOokKIEjmQaR8jUYgu6ZRn9VzXBChzR8CfDv_w,1092
|
163
163
|
meerschaum/connectors/api/_pipes.py,sha256=wf-_jqFBiTNw2s2msp0Ye6IyTl5JWiyC9aLBI0PQuPs,20962
|
@@ -201,10 +201,10 @@ meerschaum/core/Plugin/__init__.py,sha256=UXg64EvJPgI1PCxkY_KM02-ZmBm4FZpLPIQR_u
|
|
201
201
|
meerschaum/core/User/_User.py,sha256=CApB7Y0QJL6S9QOCCfrG4SbPuPXJ9AsAYQ5pASMP_Aw,6527
|
202
202
|
meerschaum/core/User/__init__.py,sha256=lJ7beIZTG9sO4dAi3367fFBl17dXYEWHKi7HoaPlDyk,193
|
203
203
|
meerschaum/jobs/_Executor.py,sha256=ZUsQl0hgU0HW_F9bQCfm8KwrdyTBLCrfxsSnbJOwtWI,1575
|
204
|
-
meerschaum/jobs/_Job.py,sha256=
|
204
|
+
meerschaum/jobs/_Job.py,sha256=x1KXKOewv1hutf89i31yvRx_zlHnxn03ZbISHLXNbLM,29013
|
205
205
|
meerschaum/jobs/_LocalExecutor.py,sha256=v_zvU1vgJDy-iiztXS5gnMayJ0UZnW48qJj3OSB6o-k,2195
|
206
|
-
meerschaum/jobs/_SystemdExecutor.py,sha256=
|
207
|
-
meerschaum/jobs/__init__.py,sha256=
|
206
|
+
meerschaum/jobs/_SystemdExecutor.py,sha256=ek2pO7u8qt0PTbfPPJLXM_j5rlP3ScUUzPBwXK5HhII,22960
|
207
|
+
meerschaum/jobs/__init__.py,sha256=hGlyB80olsP1BSkYhmHHyg6hi0UuIOU7pWMX7Ry8EFU,10800
|
208
208
|
meerschaum/plugins/_Plugin.py,sha256=q1B4gVAO2BGA-PIR0HI3-O9q1n8BaT7tSANIyW7gUPg,34084
|
209
209
|
meerschaum/plugins/__init__.py,sha256=3Hg9yyfkN0TxPPTnlt6pi53znjRIAR92WpG3zEKYm10,26152
|
210
210
|
meerschaum/plugins/bootstrap.py,sha256=qg9MQ1YAU8ShwGqWDl38WjiXLIxDPl95pSIGDLN9rOw,11423
|
@@ -224,12 +224,12 @@ meerschaum/utils/threading.py,sha256=3N8JXPAnwqJiSjuQcbbJg3Rv9-CCUMJpeQRfKFR7MaA
|
|
224
224
|
meerschaum/utils/typing.py,sha256=U3MC347sh1umpa3Xr1k71eADyDmk4LB6TnVCpq8dVzI,2830
|
225
225
|
meerschaum/utils/warnings.py,sha256=IDiwYspsfjIi1gtk3V9cSo9vNLckB9bCsHhRClpPJTc,6639
|
226
226
|
meerschaum/utils/yaml.py,sha256=Da9ZtNdT8f68sqz6g4eLQM3jz8QQ2J9_FglX-fw5VXY,3901
|
227
|
-
meerschaum/utils/daemon/Daemon.py,sha256=
|
227
|
+
meerschaum/utils/daemon/Daemon.py,sha256=8MPsHwZT_X9zZ9nxNlwa80PpwkB_NJYp3aBsfxTEW54,41640
|
228
228
|
meerschaum/utils/daemon/FileDescriptorInterceptor.py,sha256=MJKMO0Syf3d8yWUs6xXcQzg8Ptsuvh2aCRRoglOjusA,5257
|
229
229
|
meerschaum/utils/daemon/RotatingFile.py,sha256=ePm_svjwyFDWh6V1k-bp1RHXCSWlyxDtlFu4SU4XvPU,24369
|
230
230
|
meerschaum/utils/daemon/StdinFile.py,sha256=CECjb40If-TKTXwj_3nFkUfY2xTrrIx73M32mcn5hS8,3172
|
231
231
|
meerschaum/utils/daemon/__init__.py,sha256=o9jWb4lRTIyny4EPt7fPXFgV_vIf1mUofsTwoE1ZecA,8751
|
232
|
-
meerschaum/utils/daemon/_names.py,sha256=
|
232
|
+
meerschaum/utils/daemon/_names.py,sha256=QInmWQHBA8P8d3lEjVb-ukYvXXK8T87G7xljjOJtL1Q,4330
|
233
233
|
meerschaum/utils/dtypes/__init__.py,sha256=JR9PViJTzhukZhq0QoPIs73HOnXZZr8OmfhAAD4OAUA,6261
|
234
234
|
meerschaum/utils/dtypes/sql.py,sha256=IkEOyB63je-rCLHM6WwFzGbCerYk1zobL1cXkWqmTa4,14638
|
235
235
|
meerschaum/utils/formatting/__init__.py,sha256=NcNPxXRSnKZAIBxAD0ictCjVBzYzFtOwpf2CDMfr4_Y,15354
|
@@ -242,11 +242,11 @@ meerschaum/utils/packages/_packages.py,sha256=GzbJ0kxW_EQogXmY4vguRkUyad42cshFs7
|
|
242
242
|
meerschaum/utils/packages/lazy_loader.py,sha256=VHnph3VozH29R4JnSSBfwtA5WKZYZQFT_GeQSShCnuc,2540
|
243
243
|
meerschaum/utils/venv/_Venv.py,sha256=sBnlmxHdAh2bx8btfVoD79-H9-cYsv5lP02IIXkyECs,3553
|
244
244
|
meerschaum/utils/venv/__init__.py,sha256=bLAWnllKDuE_z6bLk7gLh4mI3Sp1j5hsboTqPKOQq84,24361
|
245
|
-
meerschaum-2.3.
|
246
|
-
meerschaum-2.3.
|
247
|
-
meerschaum-2.3.
|
248
|
-
meerschaum-2.3.
|
249
|
-
meerschaum-2.3.
|
250
|
-
meerschaum-2.3.
|
251
|
-
meerschaum-2.3.
|
252
|
-
meerschaum-2.3.
|
245
|
+
meerschaum-2.3.0rc3.dist-info/LICENSE,sha256=jG2zQEdRNt88EgHUWPpXVWmOrOduUQRx7MnYV9YIPaw,11359
|
246
|
+
meerschaum-2.3.0rc3.dist-info/METADATA,sha256=sOTBNkKdN7uRPVbNXQUXxGb2gDU1q30FWsl3KqZtxyE,24009
|
247
|
+
meerschaum-2.3.0rc3.dist-info/NOTICE,sha256=OTA9Fcthjf5BRvWDDIcBC_xfLpeDV-RPZh3M-HQBRtQ,114
|
248
|
+
meerschaum-2.3.0rc3.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
249
|
+
meerschaum-2.3.0rc3.dist-info/entry_points.txt,sha256=5YBVzibw-0rNA_1VjB16z5GABsOGf-CDhW4yqH8C7Gc,88
|
250
|
+
meerschaum-2.3.0rc3.dist-info/top_level.txt,sha256=bNoSiDj0El6buocix-FRoAtJOeq1qOF5rRm2u9i7Q6A,11
|
251
|
+
meerschaum-2.3.0rc3.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
252
|
+
meerschaum-2.3.0rc3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|