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.
Files changed (70) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/__init__.py +1 -1
  4. meerschaum/_internal/arguments/_parse_arguments.py +72 -6
  5. meerschaum/_internal/arguments/_parser.py +45 -15
  6. meerschaum/_internal/docs/index.py +265 -8
  7. meerschaum/_internal/entry.py +154 -24
  8. meerschaum/_internal/shell/Shell.py +264 -77
  9. meerschaum/actions/__init__.py +29 -17
  10. meerschaum/actions/api.py +12 -12
  11. meerschaum/actions/attach.py +113 -0
  12. meerschaum/actions/copy.py +68 -41
  13. meerschaum/actions/delete.py +112 -50
  14. meerschaum/actions/edit.py +3 -3
  15. meerschaum/actions/install.py +40 -32
  16. meerschaum/actions/pause.py +44 -27
  17. meerschaum/actions/restart.py +107 -0
  18. meerschaum/actions/show.py +130 -159
  19. meerschaum/actions/start.py +161 -100
  20. meerschaum/actions/stop.py +78 -42
  21. meerschaum/api/_events.py +25 -1
  22. meerschaum/api/_oauth2.py +2 -0
  23. meerschaum/api/_websockets.py +2 -2
  24. meerschaum/api/dash/callbacks/jobs.py +36 -44
  25. meerschaum/api/dash/jobs.py +89 -78
  26. meerschaum/api/routes/__init__.py +1 -0
  27. meerschaum/api/routes/_actions.py +148 -17
  28. meerschaum/api/routes/_jobs.py +407 -0
  29. meerschaum/api/routes/_pipes.py +5 -5
  30. meerschaum/config/_default.py +1 -0
  31. meerschaum/config/_jobs.py +1 -1
  32. meerschaum/config/_paths.py +7 -0
  33. meerschaum/config/_shell.py +8 -3
  34. meerschaum/config/_version.py +1 -1
  35. meerschaum/config/static/__init__.py +17 -0
  36. meerschaum/connectors/Connector.py +13 -7
  37. meerschaum/connectors/__init__.py +28 -15
  38. meerschaum/connectors/api/APIConnector.py +27 -1
  39. meerschaum/connectors/api/_actions.py +71 -6
  40. meerschaum/connectors/api/_jobs.py +368 -0
  41. meerschaum/connectors/api/_pipes.py +85 -84
  42. meerschaum/connectors/parse.py +27 -15
  43. meerschaum/core/Pipe/_bootstrap.py +16 -8
  44. meerschaum/jobs/_Executor.py +69 -0
  45. meerschaum/jobs/_Job.py +899 -0
  46. meerschaum/jobs/__init__.py +396 -0
  47. meerschaum/jobs/systemd.py +694 -0
  48. meerschaum/plugins/__init__.py +97 -12
  49. meerschaum/utils/daemon/Daemon.py +276 -30
  50. meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
  51. meerschaum/utils/daemon/RotatingFile.py +14 -7
  52. meerschaum/utils/daemon/StdinFile.py +121 -0
  53. meerschaum/utils/daemon/__init__.py +15 -7
  54. meerschaum/utils/daemon/_names.py +15 -13
  55. meerschaum/utils/formatting/__init__.py +2 -1
  56. meerschaum/utils/formatting/_jobs.py +115 -62
  57. meerschaum/utils/formatting/_shell.py +6 -0
  58. meerschaum/utils/misc.py +41 -22
  59. meerschaum/utils/packages/_packages.py +9 -6
  60. meerschaum/utils/process.py +9 -9
  61. meerschaum/utils/prompt.py +16 -8
  62. meerschaum/utils/venv/__init__.py +2 -2
  63. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
  64. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/RECORD +70 -61
  65. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
  66. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
  67. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
  68. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
  69. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
  70. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
@@ -7,12 +7,12 @@ Start subsystems (API server, logging daemon, etc.).
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import SuccessTuple, Optional, List, Any
10
+ from meerschaum.utils.typing import SuccessTuple, Optional, List, Any, Union
11
11
 
12
12
  def start(
13
- action: Optional[List[str]] = None,
14
- **kw: Any,
15
- ) -> SuccessTuple:
13
+ action: Optional[List[str]] = None,
14
+ **kw: Any,
15
+ ) -> SuccessTuple:
16
16
  """
17
17
  Start subsystems (API server, background job, etc.).
18
18
  """
@@ -23,21 +23,29 @@ def start(
23
23
  'gui': _start_gui,
24
24
  'webterm': _start_webterm,
25
25
  'connectors': _start_connectors,
26
+ 'pipeline': _start_pipeline,
26
27
  }
27
28
  return choose_subaction(action, options, **kw)
28
29
 
29
30
 
30
31
  def _complete_start(
31
- action: Optional[List[str]] = None,
32
- **kw: Any
33
- ) -> List[str]:
32
+ action: Optional[List[str]] = None,
33
+ **kw: Any
34
+ ) -> List[str]:
34
35
  """
35
36
  Override the default Meerschaum `complete_` function.
36
37
  """
38
+ from meerschaum.actions.delete import _complete_delete_jobs
39
+ from functools import partial
37
40
 
38
41
  if action is None:
39
42
  action = []
40
43
 
44
+ _complete_start_jobs = partial(
45
+ _complete_delete_jobs,
46
+ _get_job_method=['stopped', 'paused'],
47
+ )
48
+
41
49
  options = {
42
50
  'job': _complete_start_jobs,
43
51
  'jobs': _complete_start_jobs,
@@ -75,11 +83,15 @@ def _start_api(action: Optional[List[str]] = None, **kw):
75
83
  from meerschaum.actions import actions
76
84
  return actions['api'](action=['start'], **kw)
77
85
 
86
+
78
87
  def _start_jobs(
79
- action: Optional[List[str]] = None,
80
- name: Optional[str] = None,
81
- **kw
82
- ) -> SuccessTuple:
88
+ action: Optional[List[str]] = None,
89
+ name: Optional[str] = None,
90
+ sysargs: Optional[List[str]] = None,
91
+ executor_keys: Optional[str] = None,
92
+ debug: bool = False,
93
+ **kw
94
+ ) -> SuccessTuple:
83
95
  """
84
96
  Run a Meerschaum action as a background job.
85
97
 
@@ -109,23 +121,24 @@ def _start_jobs(
109
121
  Start the job 'happy_seal' but via the `--name` flag.
110
122
  This only applies when no text follows the words 'start job'.
111
123
  """
112
- import textwrap
113
124
  from meerschaum.utils.warnings import warn, info
114
- from meerschaum.utils.daemon import (
115
- daemon_action, Daemon, get_daemon_ids, get_daemons, get_filtered_daemons,
116
- get_stopped_daemons, get_running_daemons, get_paused_daemons,
117
- )
118
125
  from meerschaum.utils.daemon._names import get_new_daemon_name
119
- from meerschaum._internal.arguments._parse_arguments import parse_arguments
126
+ from meerschaum.jobs import (
127
+ Job,
128
+ get_filtered_jobs,
129
+ get_stopped_jobs,
130
+ get_running_jobs,
131
+ get_paused_jobs,
132
+ _install_healthcheck_job,
133
+ )
120
134
  from meerschaum.actions import actions
121
135
  from meerschaum.utils.prompt import yes_no
122
136
  from meerschaum.utils.formatting import print_tuple
123
- from meerschaum.utils.formatting._jobs import pprint_job, pprint_jobs
124
- from meerschaum.utils.formatting._shell import clear_screen
137
+ from meerschaum.utils.formatting._jobs import pprint_jobs
125
138
  from meerschaum.utils.misc import items_str
126
139
 
127
140
  names = []
128
- daemon_ids = get_daemon_ids()
141
+ jobs = get_filtered_jobs(executor_keys, action, debug=debug)
129
142
 
130
143
  new_job = len(list(action)) > 0
131
144
  _potential_jobs = {'known': [], 'unknown': []}
@@ -134,7 +147,7 @@ def _start_jobs(
134
147
  for a in action:
135
148
  _potential_jobs[(
136
149
  'known'
137
- if a in daemon_ids
150
+ if a in jobs
138
151
  else 'unknown'
139
152
  )].append(a)
140
153
 
@@ -158,7 +171,7 @@ def _start_jobs(
158
171
  + items_str(_potential_jobs['unknown'])
159
172
  + " will be ignored."
160
173
  ),
161
- stack = False
174
+ stack=False
162
175
  )
163
176
 
164
177
  ### Determine the `names` list.
@@ -182,88 +195,83 @@ def _start_jobs(
182
195
 
183
196
  ### No action or --name was provided. Ask to start all stopped jobs.
184
197
  else:
185
- _running_daemons = get_running_daemons()
186
- _paused_daemons = get_paused_daemons()
187
- _stopped_daemons = get_stopped_daemons()
188
- if not _stopped_daemons and not _paused_daemons:
189
- if not _running_daemons:
190
- return False, "No jobs to start."
198
+ running_jobs = get_running_jobs(executor_keys, jobs, debug=debug)
199
+ paused_jobs = get_paused_jobs(executor_keys, jobs, debug=debug)
200
+ stopped_jobs = get_stopped_jobs(executor_keys, jobs, debug=debug)
201
+
202
+ if not stopped_jobs and not paused_jobs:
203
+ if not running_jobs:
204
+ return False, "No jobs to start"
191
205
  return True, "All jobs are running."
192
206
 
193
- names = [d.daemon_id for d in _stopped_daemons + _paused_daemons]
207
+ names = [
208
+ name
209
+ for name in list(stopped_jobs) + list(paused_jobs)
210
+ ]
194
211
 
195
212
  def _run_new_job(name: Optional[str] = None):
196
- kw['action'] = action
197
213
  name = name or get_new_daemon_name()
198
- kw['name'] = name
199
- _action_success_tuple = daemon_action(daemon_id=name, **kw)
200
- return _action_success_tuple, name
201
-
202
- def _run_existing_job(name: Optional[str] = None):
203
- daemon = Daemon(daemon_id=name)
204
- if daemon.process is not None:
205
- if daemon.status == 'paused':
206
- return daemon.resume(), daemon.daemon_id
207
- return (True, f"Job '{name}' is already running."), daemon.daemon_id
208
-
209
- if not daemon.path.exists():
210
- if not kw.get('nopretty', False):
211
- warn(f"There isn't a job with the name '{name}'.", stack=False)
212
- print(
213
- f"You can start a new job named '{name}' with `start job "
214
- + "{options}" + f" --name {name}`"
215
- )
216
- return (False, f"Job '{name}' does not exist."), daemon.daemon_id
214
+ job = Job(name, sysargs, executor_keys=executor_keys)
215
+ return job.start(debug=debug), name
217
216
 
218
- return daemon.run(allow_dirty_run=True), daemon.daemon_id
217
+ def _run_existing_job(name: str):
218
+ job = Job(name, executor_keys=executor_keys)
219
+ return job.start(debug=debug), name
219
220
 
220
221
  if not names:
221
222
  return False, "No jobs to start."
222
223
 
223
224
  ### Get user permission to clear logs.
224
- _filtered_daemons = get_filtered_daemons(names)
225
- if not kw.get('force', False) and _filtered_daemons:
226
- _filtered_running_daemons = get_running_daemons(_filtered_daemons)
227
- _skipped_daemons = []
228
- if _filtered_running_daemons:
229
- pprint_jobs(_filtered_running_daemons)
225
+ _filtered_jobs = get_filtered_jobs(executor_keys, names, debug=debug)
226
+ if not kw.get('force', False) and _filtered_jobs:
227
+ _filtered_running_jobs = get_running_jobs(executor_keys, _filtered_jobs, debug=debug)
228
+ _skipped_jobs = []
229
+ if _filtered_running_jobs:
230
+ pprint_jobs(_filtered_running_jobs)
230
231
  if yes_no(
231
232
  "Do you want to first stop these jobs?",
232
- default = 'n',
233
- yes = kw.get('yes', False),
234
- noask = kw.get('noask', False)
233
+ default='n',
234
+ yes=kw.get('yes', False),
235
+ noask=kw.get('noask', False)
235
236
  ):
236
237
  stop_success_tuple = actions['stop'](
237
- action = ['jobs'] + [d.daemon_id for d in _filtered_running_daemons],
238
- force = True,
238
+ action=['jobs'] + [_name for _name in _filtered_running_jobs],
239
+ force=True,
240
+ executor_keys=executor_keys,
241
+ debug=debug,
239
242
  )
240
243
  if not stop_success_tuple[0]:
241
244
  warn(
242
- "Failed to stop job" + ("s" if len(_filtered_running_daemons) != 1 else '')
243
- + items_str([d.daemon_id for d in _filtered_running_daemons])
244
- + ".",
245
- stack = False
245
+ (
246
+ "Failed to stop job"
247
+ + ("s" if len(_filtered_running_jobs) != 1 else '')
248
+ + items_str([_name for _name in _filtered_running_jobs])
249
+ + "."
250
+ ),
251
+ stack=False
246
252
  )
247
- for d in _filtered_running_daemons:
248
- names.remove(d.daemon_id)
249
- _filtered_daemons.remove(d)
253
+ for _name in _filtered_running_jobs:
254
+ names.remove(_name)
255
+ _filtered_jobs.pop(_name)
250
256
  else:
251
257
  info(
252
258
  "Skipping already running job"
253
- + ("s" if len(_filtered_running_daemons) != 1 else '') + ' '
254
- + items_str([d.daemon_id for d in _filtered_running_daemons]) + '.'
259
+ + ("s" if len(_filtered_running_jobs) != 1 else '')
260
+ + ' '
261
+ + items_str([_name for _name in _filtered_running_jobs])
262
+ + '.'
255
263
  )
256
- for d in _filtered_running_daemons:
257
- names.remove(d.daemon_id)
258
- _filtered_daemons.remove(d)
259
- _skipped_daemons.append(d)
264
+ for _name in _filtered_running_jobs:
265
+ names.remove(_name)
266
+ _filtered_jobs.pop(_name)
267
+ _skipped_jobs.append(_name)
260
268
 
261
- if not _filtered_daemons:
262
- return len(_skipped_daemons) > 0, "No jobs to start."
269
+ if not _filtered_jobs:
270
+ return len(_skipped_jobs) > 0, "No jobs to start."
263
271
 
264
- pprint_jobs(_filtered_daemons, nopretty=kw.get('nopretty', False))
272
+ pprint_jobs(_filtered_jobs, nopretty=kw.get('nopretty', False))
265
273
  info(
266
- f"Starting the job"
274
+ "Starting the job"
267
275
  + ("s" if len(names) != 1 else "")
268
276
  + " " + items_str(names)
269
277
  + "..."
@@ -278,7 +286,11 @@ def _start_jobs(
278
286
  )
279
287
  if not kw.get('nopretty', False):
280
288
  print_tuple(success_tuple)
281
- _successes.append(_name) if success_tuple[0] else _failures.append(_name)
289
+
290
+ if success_tuple[0]:
291
+ _successes.append(_name)
292
+ else:
293
+ _failures.append(_name)
282
294
 
283
295
  msg = (
284
296
  (("Successfully started job" + ("s" if len(_successes) != 1 else '')
@@ -287,29 +299,9 @@ def _start_jobs(
287
299
  + ("Failed to start job" + ("s" if len(_failures) != 1 else '')
288
300
  + f" {items_str(_failures)}." if _failures else '')
289
301
  )
302
+ _install_healthcheck_job()
290
303
  return len(_failures) == 0, msg
291
304
 
292
- def _complete_start_jobs(
293
- action: Optional[List[str]] = None,
294
- line: str = '',
295
- **kw
296
- ) -> List[str]:
297
- from meerschaum.utils.daemon import get_daemon_ids
298
- daemon_ids = get_daemon_ids()
299
- if not action:
300
- return daemon_ids
301
- possibilities = []
302
- _line_end = line.split(' ')[-1]
303
- for daemon_id in daemon_ids:
304
- if daemon_id in action:
305
- continue
306
- if _line_end == '':
307
- possibilities.append(daemon_id)
308
- continue
309
- if daemon_id.startswith(action[-1]):
310
- possibilities.append(daemon_id)
311
- return possibilities
312
-
313
305
 
314
306
  def _start_gui(
315
307
  action: Optional[List[str]] = None,
@@ -546,6 +538,75 @@ def _complete_start_connectors(**kw) -> List[str]:
546
538
  return _complete_show_connectors(**kw)
547
539
 
548
540
 
541
+ def _start_pipeline(
542
+ action: Optional[List[str]] = None,
543
+ sub_args: Optional[List[str]] = None,
544
+ loop: bool = False,
545
+ min_seconds: Union[float, int, None] = 1.0,
546
+ params: Optional[Dict[str, Any]] = None,
547
+ **kwargs
548
+ ) -> SuccessTuple:
549
+ """
550
+ Run a series of Meerschaum commands as a single action.
551
+
552
+ Add `:` to the end of chained arguments to apply additional flags to the pipeline.
553
+
554
+ Examples
555
+ --------
556
+
557
+ `sync pipes -i sql:local + sync pipes -i sql:main :: -s 'daily'`
558
+
559
+ `show version + show arguments :: --loop`
560
+
561
+ """
562
+ import time
563
+ from meerschaum._internal.entry import entry
564
+ from meerschaum.utils.warnings import info, warn
565
+ from meerschaum.utils.misc import is_int
566
+
567
+ do_n_times = (
568
+ int(action[0].lstrip('x'))
569
+ if action and is_int(action[0].lstrip('x'))
570
+ else 1
571
+ )
572
+
573
+ if not sub_args:
574
+ return False, "Nothing to do."
575
+
576
+ if min_seconds is None:
577
+ min_seconds = 1.0
578
+
579
+ ran_n_times = 0
580
+ success, msg = False, "Did not run pipeline."
581
+ def run_loop():
582
+ nonlocal ran_n_times, success, msg
583
+ while True:
584
+ success, msg = entry(sub_args, _patch_args=params)
585
+ ran_n_times += 1
586
+
587
+ if not loop and do_n_times == 1:
588
+ break
589
+
590
+ if min_seconds != 0 and ran_n_times != do_n_times:
591
+ info(f"Sleeping for {min_seconds} seconds...")
592
+ time.sleep(min_seconds)
593
+
594
+ if loop:
595
+ continue
596
+
597
+ if ran_n_times >= do_n_times:
598
+ break
599
+
600
+ try:
601
+ run_loop()
602
+ except KeyboardInterrupt:
603
+ warn("Cancelled pipeline.", stack=False)
604
+
605
+ if do_n_times != 1:
606
+ info(f"Ran pipeline {ran_n_times} time" + ('s' if ran_n_times != 1 else '') + '.')
607
+ return success, msg
608
+
609
+
549
610
  ### NOTE: This must be the final statement of the module.
550
611
  ### Any subactions added below these lines will not
551
612
  ### be added to the `help` docstring.
@@ -27,13 +27,20 @@ def _complete_stop(
27
27
  """
28
28
  Override the default Meerschaum `complete_` function.
29
29
  """
30
- from meerschaum.actions.start import _complete_start_jobs
30
+ from meerschaum.actions.delete import _complete_delete_jobs
31
+ from functools import partial
32
+
31
33
  if action is None:
32
34
  action = []
33
35
 
36
+ _complete_stop_jobs = partial(
37
+ _complete_delete_jobs,
38
+ _get_job_method=('running', 'paused', 'restart'),
39
+ )
40
+
34
41
  options = {
35
- 'job' : _complete_start_jobs,
36
- 'jobs' : _complete_start_jobs,
42
+ 'job' : _complete_stop_jobs,
43
+ 'jobs' : _complete_stop_jobs,
37
44
  }
38
45
 
39
46
  if (
@@ -50,11 +57,13 @@ def _complete_stop(
50
57
 
51
58
  def _stop_jobs(
52
59
  action: Optional[List[str]] = None,
60
+ executor_keys: Optional[str] = None,
53
61
  timeout_seconds: Optional[int] = None,
54
62
  noask: bool = False,
55
63
  force: bool = False,
56
64
  yes: bool = False,
57
65
  nopretty: bool = False,
66
+ debug: bool = False,
58
67
  **kw
59
68
  ) -> SuccessTuple:
60
69
  """
@@ -62,66 +71,93 @@ def _stop_jobs(
62
71
 
63
72
  To see running processes, run `show jobs`.
64
73
  """
74
+ from meerschaum.jobs import (
75
+ get_filtered_jobs,
76
+ get_running_jobs,
77
+ get_paused_jobs,
78
+ get_stopped_jobs,
79
+ get_restart_jobs,
80
+ )
65
81
  from meerschaum.utils.formatting._jobs import pprint_jobs
66
82
  from meerschaum.utils.daemon import (
67
83
  get_filtered_daemons, get_running_daemons, get_stopped_daemons, get_paused_daemons,
68
84
  )
69
85
  from meerschaum.utils.warnings import warn
70
86
  from meerschaum.utils.prompt import yes_no
71
- daemons = get_filtered_daemons(action, warn=(not nopretty))
72
- _running_daemons = get_running_daemons(daemons)
73
- _paused_daemons = get_paused_daemons(daemons)
74
- _stopped_daemons = get_stopped_daemons(daemons)
75
- if action and _stopped_daemons and not nopretty:
87
+
88
+ jobs = get_filtered_jobs(executor_keys, action, warn=(not nopretty))
89
+ running_jobs = get_running_jobs(executor_keys, jobs)
90
+ paused_jobs = get_paused_jobs(executor_keys, jobs)
91
+ restart_jobs = get_restart_jobs(executor_keys, jobs)
92
+ stopped_jobs = {
93
+ name: job
94
+ for name, job in get_stopped_jobs(executor_keys, jobs).items()
95
+ if name not in restart_jobs
96
+ }
97
+
98
+ jobs_to_stop = {
99
+ **running_jobs,
100
+ **paused_jobs,
101
+ **restart_jobs,
102
+ }
103
+
104
+ if action and stopped_jobs and not nopretty:
76
105
  warn(
77
- f"Skipping stopped job" + ("s" if len(_stopped_daemons) > 1 else '') + " '" +
78
- ("', '".join(d.daemon_id for d in _stopped_daemons)) + "'.",
79
- stack = False
106
+ "Skipping stopped job"
107
+ + ("s" if len(stopped_jobs) != 1 else '')
108
+ + " '"
109
+ + ("', '".join(name for name in stopped_jobs)) + "'.",
110
+ stack=False,
80
111
  )
81
112
 
82
- daemons_to_stop = _running_daemons + _paused_daemons
83
- if not daemons_to_stop:
84
- return False, "No running or paused jobs to stop."
113
+ if not jobs_to_stop:
114
+ return False, "No running, paused or restarting jobs to stop."
85
115
 
86
116
  if not action:
87
117
  if not force:
88
- pprint_jobs(daemons_to_stop)
118
+ pprint_jobs(jobs_to_stop)
89
119
  if not yes_no(
90
120
  "Stop the above jobs?",
91
121
  noask=noask, yes=yes, default='n'
92
122
  ):
93
123
  return False, "No jobs were stopped."
94
124
 
95
- _quit_daemons, _kill_daemons = [], []
96
- for d in daemons_to_stop:
97
- quit_success, quit_msg = d.quit(timeout=timeout_seconds)
98
- if quit_success:
99
- _quit_daemons.append(d)
100
- continue
101
- else:
102
- warn(
103
- f"Failed to gracefully quit job '{d.daemon_id}', attempting to terminate:\n "
104
- + f"{quit_msg}",
105
- stack = False,
106
- )
107
-
108
- kill_success, kill_msg = d.kill(timeout=timeout_seconds)
109
- if kill_success:
110
- _kill_daemons.append(d)
111
- continue
112
- if not nopretty:
113
- warn(f"Failed to kill job '{d.daemon_id}' (PID {d.pid}):\n{kill_msg}", stack=False)
125
+ job_success_tuples = {}
126
+ for name, job in jobs_to_stop.items():
127
+ stop_success, stop_msg = job.stop(
128
+ timeout_seconds=timeout_seconds,
129
+ debug=debug,
130
+ )
131
+ job_success_tuples[name] = (stop_success, stop_msg)
114
132
 
133
+ num_success = sum(
134
+ (
135
+ 1
136
+ for name, (stop_success, stop_msg) in job_success_tuples.items()
137
+ if stop_success
138
+ )
139
+ )
140
+ num_fail = sum(
141
+ (
142
+ 1
143
+ for name, (stop_success, stop_msg) in job_success_tuples.items()
144
+ if not stop_success
145
+ )
146
+ )
147
+ success = num_success > 0
115
148
  msg = (
116
- (("Stopped job" + ("s" if len(_quit_daemons) != 1 else '') +
117
- " '" + "', '".join([d.daemon_id for d in _quit_daemons]) + "'.")
118
- if _quit_daemons else '')
119
- + (("\n" if _quit_daemons else "")
120
- + ("Killed job" + ("s" if len(_kill_daemons) != 1 else '') +
121
- " '" + "', '".join([d.daemon_id for d in _kill_daemons]) + "'.")
122
- if _kill_daemons else '')
149
+ f"Stopped {num_success} job"
150
+ + ('s' if num_success != 1 else '')
151
+ + '.'
123
152
  )
124
- return (len(_quit_daemons + _kill_daemons) > 0), msg
153
+ if num_fail > 0:
154
+ msg += (
155
+ f"\nFailed to stop {num_fail} job"
156
+ + ('s' if num_fail != 1 else '')
157
+ + '.'
158
+ )
159
+
160
+ return success, msg
125
161
 
126
162
 
127
163
  ### NOTE: This must be the final statement of the module.
meerschaum/api/_events.py CHANGED
@@ -19,14 +19,24 @@ from meerschaum.utils.debug import dprint
19
19
  from meerschaum.connectors.poll import retry_connect
20
20
  from meerschaum.utils.warnings import warn
21
21
  from meerschaum._internal.term.tools import is_webterm_running
22
+ from meerschaum.jobs import (
23
+ start_check_jobs_thread,
24
+ stop_check_jobs_thread,
25
+ get_executor_keys_from_context,
26
+ )
27
+
28
+ _check_jobs_thread = None
22
29
 
23
30
  @app.on_event("startup")
24
31
  async def startup():
25
- conn = get_api_connector()
32
+ """
33
+ Connect to the instance database and begin monitoring jobs.
34
+ """
26
35
  try:
27
36
  if not no_dash:
28
37
  from meerschaum.api.dash.webterm import start_webterm
29
38
  start_webterm()
39
+
30
40
  connected = retry_connect(
31
41
  get_api_connector(),
32
42
  workers = get_uvicorn_config().get('workers', None),
@@ -36,18 +46,32 @@ async def startup():
36
46
  import traceback
37
47
  traceback.print_exc()
38
48
  connected = False
49
+
39
50
  if not connected:
40
51
  await shutdown()
41
52
  os._exit(1)
42
53
 
54
+ if get_executor_keys_from_context() == 'local':
55
+ start_check_jobs_thread()
56
+
43
57
 
44
58
  @app.on_event("shutdown")
45
59
  async def shutdown():
60
+ """
61
+ Close the database connection and stop monitoring jobs.
62
+ """
46
63
  if debug:
47
64
  dprint("Closing connection...")
48
65
  if get_api_connector().type == 'sql':
49
66
  get_api_connector().engine.dispose()
50
67
 
68
+ if get_executor_keys_from_context() == 'local':
69
+ stop_check_jobs_thread()
70
+
71
+ from meerschaum.api.routes._actions import _temp_jobs
72
+ for name, job in _temp_jobs.items():
73
+ job.delete()
74
+
51
75
  ### Terminate any running jobs left over.
52
76
  if 'meerschaum.api.dash' in sys.modules:
53
77
  from meerschaum.api.dash.actions import running_jobs, stop_action
meerschaum/api/_oauth2.py CHANGED
@@ -30,6 +30,7 @@ class CustomOAuth2PasswordRequestForm:
30
30
  self.client_id = client_id
31
31
  self.client_secret = client_secret
32
32
 
33
+
33
34
  LoginManager = fastapi_login.LoginManager
34
35
  def generate_secret_key() -> str:
35
36
  """
@@ -46,5 +47,6 @@ def generate_secret_key() -> str:
46
47
 
47
48
  return secret_key.encode('utf-8')
48
49
 
50
+
49
51
  SECRET = generate_secret_key()
50
52
  manager = LoginManager(SECRET, token_url=endpoints['login'])
@@ -23,8 +23,8 @@ sessions = {}
23
23
  @app.websocket('/dash/ws')
24
24
  @app.websocket(_websocket_endpoint)
25
25
  async def websocket_endpoint(
26
- websocket: fastapi.WebSocket,
27
- ):
26
+ websocket: fastapi.WebSocket,
27
+ ):
28
28
  """
29
29
  Communicate with the Web Interface over a websocket.
30
30
  """