meerschaum 2.4.11__py3-none-any.whl → 2.4.12__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 (31) hide show
  1. meerschaum/_internal/arguments/_parse_arguments.py +15 -1
  2. meerschaum/_internal/docs/index.py +1 -0
  3. meerschaum/_internal/shell/Shell.py +19 -9
  4. meerschaum/_internal/shell/ShellCompleter.py +11 -6
  5. meerschaum/actions/bootstrap.py +120 -15
  6. meerschaum/actions/clear.py +41 -30
  7. meerschaum/actions/edit.py +89 -0
  8. meerschaum/actions/start.py +3 -2
  9. meerschaum/api/dash/callbacks/dashboard.py +2 -1
  10. meerschaum/api/dash/callbacks/jobs.py +53 -7
  11. meerschaum/api/dash/callbacks/pipes.py +1 -1
  12. meerschaum/api/dash/jobs.py +86 -60
  13. meerschaum/api/dash/pages/__init__.py +1 -0
  14. meerschaum/api/dash/pages/job.py +21 -0
  15. meerschaum/api/routes/_jobs.py +3 -3
  16. meerschaum/config/_version.py +1 -1
  17. meerschaum/connectors/sql/_fetch.py +65 -61
  18. meerschaum/connectors/sql/_pipes.py +36 -29
  19. meerschaum/utils/formatting/__init__.py +32 -16
  20. meerschaum/utils/formatting/_pipes.py +1 -1
  21. meerschaum/utils/formatting/_shell.py +4 -3
  22. meerschaum/utils/prompt.py +16 -15
  23. meerschaum/utils/sql.py +107 -35
  24. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/METADATA +1 -1
  25. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/RECORD +31 -30
  26. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/WHEEL +1 -1
  27. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/LICENSE +0 -0
  28. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/NOTICE +0 -0
  29. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/entry_points.txt +0 -0
  30. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/top_level.txt +0 -0
  31. {meerschaum-2.4.11.dist-info → meerschaum-2.4.12.dist-info}/zip-safe +0 -0
@@ -256,7 +256,8 @@ def parse_synonyms(
256
256
 
257
257
 
258
258
  def parse_dict_to_sysargs(
259
- args_dict: Dict[str, Any]
259
+ args_dict: Dict[str, Any],
260
+ coerce_dates: bool = True,
260
261
  ) -> List[str]:
261
262
  """Revert an arguments dictionary back to a command line list."""
262
263
  import shlex
@@ -304,6 +305,19 @@ def parse_dict_to_sysargs(
304
305
  if len(args_dict[a]) > 0:
305
306
  sysargs += [t[0], json.dumps(args_dict[a], separators=(',', ':'))]
306
307
 
308
+ ### Preserve the original datetime strings if possible
309
+ elif a in ('begin', 'end') and 'sysargs' in args_dict:
310
+ flag = t[0]
311
+ flag_ix = args_dict['sysargs'].index(flag)
312
+ if flag_ix < 0:
313
+ continue
314
+ try:
315
+ flag_val = args_dict['sysargs'][flag_ix + 1]
316
+ except IndexError:
317
+ flag_val = str(args_dict[a])
318
+
319
+ sysargs += [flag, str(flag_val)]
320
+
307
321
  ### Account for None and other values
308
322
  elif (args_dict[a] is not None) or (args_dict[a] is None and a in allow_none_args):
309
323
  sysargs += [t[0], str(args_dict[a])]
@@ -742,6 +742,7 @@ def init_dash(dash_app):
742
742
  <li><code>meerschaum.utils.sql.get_db_version()</code></li>
743
743
  <li><code>meerschaum.utils.sql.get_rename_table_queries()</code></li>
744
744
  <li><code>meerschaum.utils.sql.get_create_table_query()</code></li>
745
+ <li><code>meerschaum.utils.sql.wrap_query_with_cte()</code></li>
745
746
  <li><code>meerschaum.utils.sql.format_cte_subquery()</code></li>
746
747
  <li><code>meerschaum.utils.sql.session_execute()</code></li>
747
748
  </ul>
@@ -235,6 +235,23 @@ def get_shell_intro(with_color: bool = True) -> str:
235
235
  **get_config('shell', 'ansi', 'intro', 'rich')
236
236
  )
237
237
 
238
+ def get_shell_session():
239
+ """
240
+ Return the `prompt_toolkit` prompt session.
241
+ """
242
+ from meerschaum.config._paths import SHELL_HISTORY_PATH
243
+ if 'session' in shell_attrs:
244
+ return shell_attrs['session']
245
+
246
+ shell_attrs['session'] = prompt_toolkit_shortcuts.PromptSession(
247
+ history=prompt_toolkit_history.FileHistory(SHELL_HISTORY_PATH.as_posix()),
248
+ auto_suggest=ValidAutoSuggest(),
249
+ completer=ShellCompleter(),
250
+ complete_while_typing=True,
251
+ reserve_space_for_menu=False,
252
+ )
253
+ return shell_attrs['session']
254
+
238
255
 
239
256
  class Shell(cmd.Cmd):
240
257
  """
@@ -264,14 +281,7 @@ class Shell(cmd.Cmd):
264
281
  except AttributeError:
265
282
  pass
266
283
 
267
- from meerschaum.config._paths import SHELL_HISTORY_PATH
268
- shell_attrs['session'] = prompt_toolkit_shortcuts.PromptSession(
269
- history=prompt_toolkit_history.FileHistory(SHELL_HISTORY_PATH.as_posix()),
270
- auto_suggest=ValidAutoSuggest(),
271
- completer=ShellCompleter(),
272
- complete_while_typing=True,
273
- reserve_space_for_menu=False,
274
- )
284
+ _ = get_shell_session()
275
285
 
276
286
  super().__init__()
277
287
 
@@ -626,7 +636,7 @@ class Shell(cmd.Cmd):
626
636
  step_action_name = step_action[0] if step_action else None
627
637
  ### NOTE: For `stack`, revert argument parsing.
628
638
  step_sysargs = (
629
- parse_dict_to_sysargs(step_kwargs)
639
+ parse_dict_to_sysargs(step_kwargs, coerce_dates=False)
630
640
  if step_action_name != 'stack'
631
641
  else chained_sysargs[i]
632
642
  )
@@ -31,15 +31,18 @@ class ShellCompleter(Completer):
31
31
  ensure_readline()
32
32
  parts = document.text.split('-')
33
33
  ends_with_space = parts[0].endswith(' ')
34
- part_0_subbed_spaces = parts[0].replace(' ', '_')
35
- parsed_text = part_0_subbed_spaces + '-'.join(parts[1:])
34
+ last_action_line = parts[0].split('+')[-1]
35
+ part_0_subbed_spaces = last_action_line.replace(' ', '_')
36
+ parsed_text = (part_0_subbed_spaces + '-'.join(parts[1:]))
36
37
 
38
+ if not parsed_text:
39
+ return
37
40
 
38
41
  ### Index is the rank order (0 is closest match).
39
42
  ### Break when no results are returned.
40
43
  for i, a in enumerate(shell_actions):
41
44
  try:
42
- poss = shell.complete(parsed_text, i)
45
+ poss = shell.complete(parsed_text.lstrip('_'), i)
43
46
  if poss:
44
47
  poss = poss.replace('_', ' ')
45
48
  ### Having issues with readline on portable Windows.
@@ -50,7 +53,9 @@ class ShellCompleter(Completer):
50
53
  yield Completion(poss, start_position=(-1 * len(poss)))
51
54
  yielded.append(poss)
52
55
 
53
- args = parse_line(document.text)
56
+ line = document.text
57
+ current_action_line = line.split('+')[-1].lstrip()
58
+ args = parse_line(current_action_line)
54
59
  action_function = get_action(args['action'], _actions=shell_attrs.get('_actions', None))
55
60
  if action_function is None:
56
61
  return
@@ -74,8 +79,8 @@ class ShellCompleter(Completer):
74
79
  shell,
75
80
  complete_function_name
76
81
  )(
77
- document.text.split(' ')[-1],
78
- document.text,
82
+ current_action_line.split(' ')[-1],
83
+ current_action_line,
79
84
  0,
80
85
  0
81
86
  )
@@ -8,12 +8,15 @@ Functions for bootstrapping elements
8
8
  """
9
9
 
10
10
  from __future__ import annotations
11
+
12
+ import meerschaum as mrsm
11
13
  from meerschaum.utils.typing import Union, Any, Sequence, SuccessTuple, Optional, Tuple, List
12
14
 
15
+
13
16
  def bootstrap(
14
- action: Optional[List[str]] = None,
15
- **kw: Any
16
- ) -> SuccessTuple:
17
+ action: Optional[List[str]] = None,
18
+ **kw: Any
19
+ ) -> SuccessTuple:
17
20
  """
18
21
  Launch an interactive wizard to bootstrap pipes or connectors.
19
22
 
@@ -26,23 +29,24 @@ def bootstrap(
26
29
  'pipes' : _bootstrap_pipes,
27
30
  'connectors' : _bootstrap_connectors,
28
31
  'plugins' : _bootstrap_plugins,
32
+ 'jobs' : _bootstrap_jobs,
29
33
  }
30
34
  return choose_subaction(action, options, **kw)
31
35
 
32
36
 
33
37
  def _bootstrap_pipes(
34
- action: Optional[List[str]] = None,
35
- connector_keys: Optional[List[str]] = None,
36
- metric_keys: Optional[List[str]] = None,
37
- location_keys: Optional[List[Optional[str]]] = None,
38
- yes: bool = False,
39
- force: bool = False,
40
- noask: bool = False,
41
- debug: bool = False,
42
- mrsm_instance: Optional[str] = None,
43
- shell: bool = False,
44
- **kw: Any
45
- ) -> SuccessTuple:
38
+ action: Optional[List[str]] = None,
39
+ connector_keys: Optional[List[str]] = None,
40
+ metric_keys: Optional[List[str]] = None,
41
+ location_keys: Optional[List[Optional[str]]] = None,
42
+ yes: bool = False,
43
+ force: bool = False,
44
+ noask: bool = False,
45
+ debug: bool = False,
46
+ mrsm_instance: Optional[str] = None,
47
+ shell: bool = False,
48
+ **kw: Any
49
+ ) -> SuccessTuple:
46
50
  """
47
51
  Create a new pipe.
48
52
  If no keys are provided, guide the user through the steps required.
@@ -433,6 +437,107 @@ def _bootstrap_plugins(
433
437
  return True, "Success"
434
438
 
435
439
 
440
+ def _bootstrap_jobs(
441
+ action: Optional[List[str]] = None,
442
+ executor_keys: Optional[str] = None,
443
+ debug: bool = False,
444
+ **kwargs: Any
445
+ ) -> SuccessTuple:
446
+ """
447
+ Launch an interactive wizard to create new jobs.
448
+ """
449
+ import shlex
450
+ from meerschaum.utils.prompt import prompt, yes_no
451
+ from meerschaum.actions import actions
452
+ from meerschaum.utils.formatting import print_options, make_header
453
+ from meerschaum.utils.formatting._shell import clear_screen
454
+ from meerschaum.utils.warnings import info
455
+ from meerschaum._internal.arguments import (
456
+ split_pipeline_sysargs,
457
+ split_chained_sysargs,
458
+ )
459
+ from meerschaum.utils.misc import items_str
460
+ from meerschaum._internal.shell.ShellCompleter import ShellCompleter
461
+
462
+ if not action:
463
+ action = [prompt("What is the name of the job you'd like to create?")]
464
+
465
+ new_jobs = {}
466
+ for name in action:
467
+ clear_screen(debug=debug)
468
+ job = mrsm.Job(name, executor_keys=executor_keys)
469
+ if job.exists():
470
+ edit_success, edit_msg = actions['edit'](['job', name], **kwargs)
471
+ if not edit_success:
472
+ return edit_success, edit_msg
473
+ continue
474
+
475
+ info(
476
+ "Press [Esc + Enter] to submit, [CTRL + C] to exit.\n"
477
+ " Tip: join multiple actions with `+`, add pipeline arguments with `:`.\n"
478
+ " https://meerschaum.io/reference/actions/#chaining-actions\n"
479
+ )
480
+ try:
481
+ new_sysargs_str = prompt(
482
+ f"Arguments for job '{name}':",
483
+ multiline=True,
484
+ icon=False,
485
+ completer=ShellCompleter(),
486
+ )
487
+ except KeyboardInterrupt:
488
+ return True, "Nothing was changed."
489
+
490
+ new_sysargs = shlex.split(new_sysargs_str)
491
+ new_sysargs, pipeline_args = split_pipeline_sysargs(new_sysargs)
492
+ chained_sysargs = split_chained_sysargs(new_sysargs)
493
+
494
+ if len(chained_sysargs) > 1:
495
+ print_options(
496
+ [
497
+ shlex.join(step_sysargs)
498
+ for step_sysargs in chained_sysargs
499
+ ],
500
+ header=f"Steps in Job '{name}':",
501
+ number_options=True,
502
+ **kwargs
503
+ )
504
+ else:
505
+ print('\n' + make_header(f"Action for Job '{name}':"))
506
+ print(shlex.join(new_sysargs))
507
+
508
+ if pipeline_args:
509
+ print('\n' + make_header("Pipeline Arguments:"))
510
+ print(shlex.join(pipeline_args))
511
+
512
+ if not yes_no(
513
+ (
514
+ f"Are you sure you want to create job '{name}' with the above arguments?\n"
515
+ + " The job will be started if you continue."
516
+ ),
517
+ default='n',
518
+ **kwargs
519
+ ):
520
+ return True, "Nothing was changed."
521
+
522
+ new_job = mrsm.Job(name, new_sysargs_str, executor_keys=executor_keys)
523
+ start_success, start_msg = new_job.start()
524
+ if not start_success:
525
+ return start_success, start_msg
526
+
527
+ new_jobs[name] = new_job
528
+
529
+ if not new_jobs:
530
+ return False, "No new jobs were created."
531
+
532
+ msg = (
533
+ "Successfully bootstrapped job"
534
+ + ('s' if len(new_jobs) != 1 else '')
535
+ + items_str(list(new_jobs.keys()))
536
+ + '.'
537
+ )
538
+ return True, msg
539
+
540
+
436
541
  ### NOTE: This must be the final statement of the module.
437
542
  ### Any subactions added below these lines will not
438
543
  ### be added to the `help` docstring.
@@ -6,12 +6,16 @@ Functions for clearing pipes.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ import meerschaum as mrsm
9
12
  from meerschaum.utils.typing import List, SuccessTuple, Any, Optional
10
13
 
14
+
11
15
  def clear(
12
- action: Optional[List[str]] = None,
13
- **kw: Any
14
- ) -> SuccessTuple:
16
+ action: Optional[List[str]] = None,
17
+ **kw: Any
18
+ ) -> SuccessTuple:
15
19
  """
16
20
  Clear pipes of their data, or clear the screen.
17
21
 
@@ -34,24 +38,25 @@ def clear(
34
38
 
35
39
 
36
40
  def _clear_pipes(
37
- action: Optional[List[str]] = None,
38
- begin: Optional[datetime.datetime] = None,
39
- end: Optional[datetime.datetime] = None,
40
- connector_keys: Optional[List[str]] = None,
41
- metric_keys: Optional[List[str]] = None,
42
- mrsm_instance: Optional[str] = None,
43
- location_keys: Optional[List[str]] = None,
44
- force: bool = False,
45
- debug: bool = False,
46
- **kw: Any
47
- ) -> SuccessTuple:
41
+ action: Optional[List[str]] = None,
42
+ begin: Optional[datetime] = None,
43
+ end: Optional[datetime] = None,
44
+ connector_keys: Optional[List[str]] = None,
45
+ metric_keys: Optional[List[str]] = None,
46
+ mrsm_instance: Optional[str] = None,
47
+ location_keys: Optional[List[str]] = None,
48
+ yes: bool = False,
49
+ force: bool = False,
50
+ debug: bool = False,
51
+ **kw: Any
52
+ ) -> SuccessTuple:
48
53
  """
49
54
  Clear pipes' data without dropping any tables.
50
55
 
51
56
  """
52
57
  from meerschaum import get_pipes
53
58
  from meerschaum.utils.formatting import print_tuple
54
-
59
+
55
60
  successes = {}
56
61
  fails = {}
57
62
 
@@ -61,13 +66,19 @@ def _clear_pipes(
61
66
  )
62
67
 
63
68
  if not force:
64
- if not _ask_with_rowcounts(pipes, begin=begin, end=end, debug=debug, **kw):
69
+ if not _ask_with_rowcounts(pipes, begin=begin, end=end, debug=debug, yes=yes, **kw):
65
70
  return False, "No rows were deleted."
66
71
 
67
72
  for pipe in pipes:
68
- success, msg = pipe.clear(begin=begin, end=end, debug=debug, **kw)
69
- print_tuple((success, msg))
70
- (successes if success else fails)[pipe] = msg
73
+ clear_success, clear_msg = pipe.clear(
74
+ begin=begin,
75
+ end=end,
76
+ yes=yes,
77
+ debug=debug,
78
+ **kw
79
+ )
80
+ print_tuple((clear_success, clear_msg))
81
+ (successes if clear_success else fails)[pipe] = clear_msg
71
82
 
72
83
  success = len(successes) > 0
73
84
  msg = (
@@ -79,15 +90,15 @@ def _clear_pipes(
79
90
 
80
91
 
81
92
  def _ask_with_rowcounts(
82
- pipes: List[meerschaum.Pipe],
83
- begin: Optional[datetime.datetime] = None,
84
- end: Optional[datetime.datetime] = None,
85
- yes: bool = False,
86
- nopretty: bool = False,
87
- noask: bool = False,
88
- debug: bool = False,
89
- **kw
90
- ) -> bool:
93
+ pipes: List[mrsm.Pipe],
94
+ begin: Optional[datetime] = None,
95
+ end: Optional[datetime] = None,
96
+ yes: bool = False,
97
+ nopretty: bool = False,
98
+ noask: bool = False,
99
+ debug: bool = False,
100
+ **kw
101
+ ) -> bool:
91
102
  """
92
103
  Count all of the pipes' rowcounts and confirm with the user that these rows need to be deleted.
93
104
 
@@ -109,13 +120,13 @@ def _ask_with_rowcounts(
109
120
  warn(
110
121
  f"No datetime could be determined for {pipe}!\n"
111
122
  + " THIS WILL DELETE THE ENTIRE TABLE!",
112
- stack = False
123
+ stack=False
113
124
  )
114
125
  else:
115
126
  warn(
116
127
  f"A datetime wasn't specified for {pipe}.\n"
117
128
  + f" Using column \"{_dt}\" for datetime bounds...",
118
- stack = False
129
+ stack=False
119
130
  )
120
131
 
121
132
 
@@ -7,9 +7,11 @@ Functions for editing elements belong here.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
10
11
  import meerschaum as mrsm
11
12
  from meerschaum.utils.typing import List, Any, SuccessTuple, Optional, Dict
12
13
 
14
+
13
15
  def edit(
14
16
  action: Optional[List[str]] = None,
15
17
  **kw: Any
@@ -24,6 +26,7 @@ def edit(
24
26
  'definition': _edit_definition,
25
27
  'users' : _edit_users,
26
28
  'plugins' : _edit_plugins,
29
+ 'jobs' : _edit_jobs,
27
30
  }
28
31
  return choose_subaction(action, options, **kw)
29
32
 
@@ -372,6 +375,92 @@ def _complete_edit_plugins(
372
375
  return possibilities
373
376
 
374
377
 
378
+ def _edit_jobs(
379
+ action: Optional[List[str]] = None,
380
+ executor_keys: Optional[str] = None,
381
+ debug: bool = False,
382
+ **kwargs: Any
383
+ ) -> mrsm.SuccessTuple:
384
+ """
385
+ Edit existing jobs.
386
+ """
387
+ import shlex
388
+ from meerschaum.jobs import get_filtered_jobs
389
+ from meerschaum.utils.prompt import prompt, yes_no
390
+ from meerschaum._internal.arguments import (
391
+ split_pipeline_sysargs,
392
+ split_chained_sysargs,
393
+ )
394
+ from meerschaum.utils.formatting import make_header, print_options
395
+ from meerschaum.utils.warnings import info
396
+ from meerschaum.actions import actions
397
+ jobs = get_filtered_jobs(executor_keys, action, debug=debug)
398
+ if not jobs:
399
+ return False, "No jobs to edit."
400
+
401
+ info(
402
+ "Press [Esc + Enter] to submit, [CTRL + C] to exit.\n"
403
+ " Tip: join multiple actions with `+`, add pipeline arguments with `:`.\n"
404
+ " https://meerschaum.io/reference/actions/#chaining-actions\n"
405
+ )
406
+
407
+ for name, job in jobs.items():
408
+ sysargs_str = shlex.join(job.sysargs)
409
+
410
+ try:
411
+ new_sysargs_str = prompt(
412
+ f"Arguments for job '{name}':",
413
+ default_editable=sysargs_str.lstrip().rstrip(),
414
+ multiline=True,
415
+ icon=False,
416
+ )
417
+ except KeyboardInterrupt:
418
+ return True, "Nothing was changed."
419
+
420
+ new_sysargs = shlex.split(new_sysargs_str)
421
+ new_sysargs, pipeline_args = split_pipeline_sysargs(new_sysargs)
422
+ chained_sysargs = split_chained_sysargs(new_sysargs)
423
+
424
+ if len(chained_sysargs) > 1:
425
+ print_options(
426
+ [
427
+ shlex.join(step_sysargs)
428
+ for step_sysargs in chained_sysargs
429
+ ],
430
+ header=f"Steps in Job '{name}':",
431
+ number_options=True,
432
+ **kwargs
433
+ )
434
+ else:
435
+ print('\n' + make_header(f"Action for Job '{name}':"))
436
+ print(shlex.join(new_sysargs))
437
+
438
+ if pipeline_args:
439
+ print('\n' + make_header("Pipeline Arguments:"))
440
+ print(shlex.join(pipeline_args))
441
+
442
+ if not yes_no(
443
+ (
444
+ f"Are you sure you want to recreate job '{name}' with the above arguments?\n"
445
+ + " The job will be started if you continue."
446
+ ),
447
+ default='n',
448
+ **kwargs
449
+ ):
450
+ return True, "Nothing was changed."
451
+
452
+ delete_success, delete_msg = job.delete()
453
+ if not delete_success:
454
+ return delete_success, delete_msg
455
+
456
+ new_job = mrsm.Job(name, new_sysargs_str, executor_keys=executor_keys)
457
+ start_success, start_msg = new_job.start()
458
+ if not start_success:
459
+ return start_success, start_msg
460
+
461
+ return True, "Success"
462
+
463
+
375
464
  ### NOTE: This must be the final statement of the module.
376
465
  ### Any subactions added below these lines will not
377
466
  ### be added to the `help` docstring.
@@ -9,6 +9,7 @@ Start subsystems (API server, logging daemon, etc.).
9
9
  from __future__ import annotations
10
10
  from meerschaum.utils.typing import SuccessTuple, Optional, List, Any, Union, Dict
11
11
 
12
+
12
13
  def start(
13
14
  action: Optional[List[str]] = None,
14
15
  **kw: Any,
@@ -375,8 +376,8 @@ def _start_gui(
375
376
  webview.create_window(
376
377
  'Meerschaum Shell',
377
378
  f'http://127.0.0.1:{port}',
378
- height = 650,
379
- width = 1000
379
+ height=650,
380
+ width=1000
380
381
  )
381
382
  webview.start(debug=debug)
382
383
  except Exception as e:
@@ -98,6 +98,7 @@ _paths = {
98
98
  'plugins' : pages.plugins.layout,
99
99
  'register': pages.register.layout,
100
100
  'pipes' : pages.pipes.layout,
101
+ 'job' : pages.job.layout,
101
102
  }
102
103
  _required_login = {''}
103
104
 
@@ -121,7 +122,7 @@ def update_page_layout_div(
121
122
  ----------
122
123
  pathname: str
123
124
  The path in the browser.
124
-
125
+
125
126
  session_store_data: Dict[str, Any]:
126
127
  The stored session data.
127
128
 
@@ -12,6 +12,7 @@ import json
12
12
  import time
13
13
  import traceback
14
14
  from datetime import datetime, timezone
15
+
15
16
  from meerschaum.utils.typing import Optional, Dict, Any
16
17
  from meerschaum.api import CHECK_UPDATE
17
18
  from meerschaum.api.dash import dash_app
@@ -19,17 +20,19 @@ from meerschaum.api.dash.sessions import get_username_from_session
19
20
  from meerschaum.utils.packages import attempt_import, import_dcc, import_html
20
21
  from meerschaum.api.dash.components import alert_from_success_tuple
21
22
  from meerschaum.api.dash.jobs import (
23
+ build_job_card,
22
24
  build_manage_job_buttons_div_children,
23
25
  build_status_children,
24
26
  build_process_timestamps_children,
25
27
  )
26
- from meerschaum.jobs import Job
28
+ from meerschaum.api.routes._jobs import _get_job
27
29
  from meerschaum.api.dash.sessions import is_session_authenticated
28
30
  dash = attempt_import('dash', lazy=False, check_update=CHECK_UPDATE)
29
31
  html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
30
32
  from dash.exceptions import PreventUpdate
31
33
  from dash.dependencies import Input, Output, State, ALL, MATCH
32
34
  import dash_bootstrap_components as dbc
35
+ from dash import no_update
33
36
 
34
37
 
35
38
  @dash_app.callback(
@@ -53,6 +56,13 @@ def download_job_logs(n_clicks):
53
56
 
54
57
  component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
55
58
  job_name = component_dict['index']
59
+ try:
60
+ job = _get_job(job_name)
61
+ except Exception:
62
+ job = None
63
+ if job is None or not job.exists():
64
+ raise PreventUpdate
65
+
56
66
  now = datetime.now(timezone.utc)
57
67
  filename = job_name + '_' + str(int(now.timestamp())) + '.log'
58
68
  return {
@@ -69,7 +79,7 @@ def download_job_logs(n_clicks):
69
79
  Input({'type': 'manage-job-button', 'action': ALL, 'index': MATCH}, 'n_clicks'),
70
80
  State('session-store', 'data'),
71
81
  State({'type': 'job-label-p', 'index': MATCH}, 'children'),
72
- prevent_initial_call = True,
82
+ prevent_initial_call=True,
73
83
  )
74
84
  def manage_job_button_click(
75
85
  n_clicks: Optional[int] = None,
@@ -102,10 +112,10 @@ def manage_job_button_click(
102
112
  job_name = component_dict['index']
103
113
  manage_job_action = component_dict['action']
104
114
  try:
105
- job = Job(job_name, job_label.replace('\n', ' ') if job_label else None)
106
- except Exception as e:
115
+ job = _get_job(job_name, job_label.replace('\n', ' ') if job_label else None)
116
+ except Exception:
107
117
  job = None
108
- if job is None:
118
+ if job is None or not job.exists():
109
119
  raise PreventUpdate
110
120
 
111
121
  manage_functions = {
@@ -191,7 +201,7 @@ dash_app.clientside_callback(
191
201
  Output({'type': 'process-timestamps-div', 'index': ALL}, 'children'),
192
202
  Input('refresh-jobs-interval', 'n_intervals'),
193
203
  State('session-store', 'data'),
194
- prevent_initial_call = True,
204
+ prevent_initial_call=True,
195
205
  )
196
206
  def refresh_jobs_on_interval(
197
207
  n_intervals: Optional[int] = None,
@@ -209,7 +219,7 @@ def refresh_jobs_on_interval(
209
219
  ]
210
220
 
211
221
  ### NOTE: The job may have been deleted, but the card may still exist.
212
- jobs = [Job(name) for name in job_names]
222
+ jobs = [_get_job(name) for name in job_names]
213
223
 
214
224
  return (
215
225
  [
@@ -229,3 +239,39 @@ def refresh_jobs_on_interval(
229
239
  for job in jobs
230
240
  ],
231
241
  )
242
+
243
+
244
+ @dash_app.callback(
245
+ Output('job-output-div', 'children'),
246
+ Input('job-location', 'pathname'),
247
+ State('session-store', 'data'),
248
+ )
249
+ def render_job_page_from_url(
250
+ pathname: str,
251
+ session_data: Optional[Dict[str, Any]],
252
+ ):
253
+ """
254
+ Load the `/job/{name}` page.
255
+ """
256
+ if not str(pathname).startswith('/dash/job'):
257
+ return no_update
258
+
259
+ session_id = (session_data or {}).get('session-id', None)
260
+ authenticated = is_session_authenticated(str(session_id))
261
+
262
+ job_name = pathname.replace('/dash/job', '').lstrip('/').rstrip('/')
263
+ if not job_name:
264
+ return no_update
265
+
266
+ job = _get_job(job_name)
267
+ if not job.exists():
268
+ return [
269
+ html.Br(),
270
+ html.H2("404: Job does not exist."),
271
+ ]
272
+
273
+ return [
274
+ html.Br(),
275
+ build_job_card(job, authenticated=authenticated, include_follow=False),
276
+ html.Br(),
277
+ ]