meerschaum 2.1.7__py3-none-any.whl → 2.2.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 (59) hide show
  1. meerschaum/__main__.py +1 -1
  2. meerschaum/_internal/arguments/_parser.py +3 -0
  3. meerschaum/_internal/entry.py +3 -2
  4. meerschaum/actions/install.py +7 -3
  5. meerschaum/actions/show.py +128 -42
  6. meerschaum/actions/sync.py +7 -3
  7. meerschaum/api/__init__.py +24 -14
  8. meerschaum/api/_oauth2.py +4 -4
  9. meerschaum/api/dash/callbacks/dashboard.py +93 -23
  10. meerschaum/api/dash/callbacks/jobs.py +55 -3
  11. meerschaum/api/dash/jobs.py +34 -8
  12. meerschaum/api/dash/keys.py +1 -1
  13. meerschaum/api/dash/pages/dashboard.py +14 -4
  14. meerschaum/api/dash/pipes.py +137 -26
  15. meerschaum/api/dash/plugins.py +25 -9
  16. meerschaum/api/resources/static/js/xterm.js +1 -1
  17. meerschaum/api/resources/templates/termpage.html +3 -0
  18. meerschaum/api/routes/_login.py +5 -4
  19. meerschaum/api/routes/_plugins.py +6 -3
  20. meerschaum/config/_dash.py +11 -0
  21. meerschaum/config/_default.py +3 -1
  22. meerschaum/config/_jobs.py +13 -4
  23. meerschaum/config/_paths.py +2 -0
  24. meerschaum/config/_sync.py +2 -3
  25. meerschaum/config/_version.py +1 -1
  26. meerschaum/config/stack/__init__.py +6 -7
  27. meerschaum/config/stack/grafana/__init__.py +1 -1
  28. meerschaum/config/static/__init__.py +4 -1
  29. meerschaum/connectors/__init__.py +2 -0
  30. meerschaum/connectors/api/_plugins.py +2 -1
  31. meerschaum/connectors/sql/SQLConnector.py +4 -2
  32. meerschaum/connectors/sql/_create_engine.py +9 -9
  33. meerschaum/connectors/sql/_instance.py +3 -1
  34. meerschaum/connectors/sql/_pipes.py +54 -38
  35. meerschaum/connectors/sql/_plugins.py +0 -2
  36. meerschaum/connectors/sql/_sql.py +7 -9
  37. meerschaum/core/User/_User.py +158 -16
  38. meerschaum/core/User/__init__.py +1 -1
  39. meerschaum/plugins/_Plugin.py +12 -3
  40. meerschaum/plugins/__init__.py +23 -1
  41. meerschaum/utils/daemon/Daemon.py +89 -36
  42. meerschaum/utils/daemon/FileDescriptorInterceptor.py +140 -0
  43. meerschaum/utils/daemon/RotatingFile.py +130 -14
  44. meerschaum/utils/daemon/__init__.py +3 -0
  45. meerschaum/utils/dtypes/__init__.py +9 -5
  46. meerschaum/utils/packages/__init__.py +21 -5
  47. meerschaum/utils/packages/_packages.py +18 -20
  48. meerschaum/utils/process.py +13 -10
  49. meerschaum/utils/schedule.py +276 -30
  50. meerschaum/utils/threading.py +1 -0
  51. meerschaum/utils/typing.py +1 -1
  52. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/METADATA +59 -62
  53. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/RECORD +59 -57
  54. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/WHEEL +1 -1
  55. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/LICENSE +0 -0
  56. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/NOTICE +0 -0
  57. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/entry_points.txt +0 -0
  58. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/top_level.txt +0 -0
  59. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/zip-safe +0 -0
meerschaum/__main__.py CHANGED
@@ -4,7 +4,7 @@
4
4
  # vim:fenc=utf-8
5
5
 
6
6
  """
7
- Copyright 2021 Bennett Meares
7
+ Copyright 2024 Bennett Meares
8
8
 
9
9
  Licensed under the Apache License, Version 2.0 (the "License");
10
10
  you may not use this file except in compliance with the License.
@@ -346,6 +346,9 @@ groups['misc'].add_argument(
346
346
  groups['misc'].add_argument(
347
347
  '--nopretty', action="store_true", help="Print elements without 'pretty' formatting"
348
348
  )
349
+ groups['misc'].add_argument(
350
+ '--skip-deps', action="store_true", help="Skip dependencies when installing plugins.",
351
+ )
349
352
  groups['misc'].add_argument(
350
353
  '-P', '--params', type=string_to_dict, help=(
351
354
  "Parameters dictionary in JSON format or simple format. " +
@@ -1,6 +1,7 @@
1
1
  #! /usr/bin/env python
2
2
  # -*- coding: utf-8 -*-
3
3
  # vim:fenc=utf-8
4
+ # type: ignore
4
5
 
5
6
  """
6
7
  The entry point for launching Meerschaum actions.
@@ -48,7 +49,7 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
48
49
 
49
50
  if args.get('schedule', None):
50
51
  from meerschaum.utils.schedule import schedule_function
51
- return schedule_function(entry_with_args, args['schedule'], **args)
52
+ return schedule_function(entry_with_args, **args)
52
53
  return entry_with_args(**args)
53
54
 
54
55
 
@@ -61,7 +62,7 @@ def entry_with_args(
61
62
  """
62
63
  import sys
63
64
  from meerschaum.plugins import Plugin
64
- from meerschaum.actions import get_shell, get_action, get_main_action_name
65
+ from meerschaum.actions import get_action, get_main_action_name
65
66
  from meerschaum._internal.arguments import remove_leading_action
66
67
  from meerschaum.utils.venv import Venv, active_venvs, deactivate_venv
67
68
  if kw.get('trace', None):
@@ -54,6 +54,7 @@ def _complete_install(
54
54
  def _install_plugins(
55
55
  action: Optional[List[str]] = None,
56
56
  repository: Optional[str] = None,
57
+ skip_deps: bool = False,
57
58
  force: bool = False,
58
59
  debug: bool = False,
59
60
  **kw: Any
@@ -87,11 +88,14 @@ def _install_plugins(
87
88
 
88
89
  repo_connector = parse_repo_keys(repository)
89
90
 
90
- successes = {}
91
91
  for name in action:
92
92
  info(f"Installing plugin '{name}' from Meerschaum repository '{repo_connector}'...")
93
- success, msg = repo_connector.install_plugin(name, force=force, debug=debug)
94
- successes[name] = (success, msg)
93
+ success, msg = repo_connector.install_plugin(
94
+ name,
95
+ force = force,
96
+ skip_deps = skip_deps,
97
+ debug = debug,
98
+ )
95
99
  print_tuple((success, msg))
96
100
 
97
101
  reload_plugins(debug=debug)
@@ -41,6 +41,7 @@ def show(
41
41
  'jobs' : _show_jobs,
42
42
  'logs' : _show_logs,
43
43
  'tags' : _show_tags,
44
+ 'schedules' : _show_schedules,
44
45
  }
45
46
  return choose_subaction(action, show_options, **kw)
46
47
 
@@ -577,6 +578,7 @@ def _show_logs(
577
578
  `show logs myjob myotherjob`
578
579
  """
579
580
  import os, pathlib, random, asyncio
581
+ from datetime import datetime, timezone
580
582
  from meerschaum.utils.packages import attempt_import, import_rich
581
583
  from meerschaum.utils.daemon import get_filtered_daemons, Daemon
582
584
  from meerschaum.utils.warnings import warn, info
@@ -587,72 +589,106 @@ def _show_logs(
587
589
  if not ANSI:
588
590
  info = print
589
591
  colors = get_config('jobs', 'logs', 'colors')
592
+ timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
593
+ follow_timestamp_format = get_config('jobs', 'logs', 'timestamps', 'follow_format')
590
594
  daemons = get_filtered_daemons(action)
591
-
592
- def _build_buffer_spaces(daemons) -> Dict[str, str]:
593
- _max_len_id = max([len(d.daemon_id) for d in daemons]) if daemons else 0
594
- _buffer_len = max(get_config('jobs', 'logs', 'min_buffer_len'), _max_len_id + 2)
595
+ now = datetime.now(timezone.utc)
596
+ now_str = now.strftime(timestamp_format)
597
+ now_follow_str = now.strftime(follow_timestamp_format)
598
+
599
+ def build_buffer_spaces(daemons) -> Dict[str, str]:
600
+ max_len_id = max(len(d.daemon_id) for d in daemons) if daemons else 0
601
+ buffer_len = max(
602
+ get_config('jobs', 'logs', 'min_buffer_len'),
603
+ max_len_id
604
+ )
595
605
  return {
596
- d.daemon_id: ''.join([' ' for i in range(_buffer_len - len(d.daemon_id))])
606
+ d.daemon_id: ''.join([' ' for i in range(buffer_len - len(d.daemon_id))])
597
607
  for d in daemons
598
608
  }
599
609
 
600
- def _build_job_colors(daemons, _old_job_colors = None) -> Dict[str, str]:
610
+ def build_job_colors(daemons, _old_job_colors = None) -> Dict[str, str]:
601
611
  return {d.daemon_id: colors[i % len(colors)] for i, d in enumerate(daemons)}
602
612
 
603
- _buffer_spaces = _build_buffer_spaces(daemons)
604
- _job_colors = _build_job_colors(daemons)
613
+ buffer_spaces = build_buffer_spaces(daemons)
614
+ job_colors = build_job_colors(daemons)
605
615
 
606
- def _get_buffer_spaces(daemon_id):
607
- nonlocal _buffer_spaces, daemons
608
- if daemon_id not in _buffer_spaces:
616
+ def get_buffer_spaces(daemon_id):
617
+ nonlocal buffer_spaces, daemons
618
+ if daemon_id not in buffer_spaces:
609
619
  d = Daemon(daemon_id=daemon_id)
610
620
  if d not in daemons:
611
621
  daemons = get_filtered_daemons(action)
612
- _buffer_spaces = _build_buffer_spaces(daemons)
613
- return _buffer_spaces[daemon_id]
622
+ buffer_spaces = build_buffer_spaces(daemons)
623
+ return buffer_spaces[daemon_id] or ' '
614
624
 
615
- def _get_job_colors(daemon_id):
616
- nonlocal _job_colors, daemons
617
- if daemon_id not in _job_colors:
625
+ def get_job_colors(daemon_id):
626
+ nonlocal job_colors, daemons
627
+ if daemon_id not in job_colors:
618
628
  d = Daemon(daemon_id=daemon_id)
619
629
  if d not in daemons:
620
630
  daemons = get_filtered_daemons(action)
621
- _job_colors = _build_job_colors(daemons)
622
- return _job_colors[daemon_id]
631
+ job_colors = build_job_colors(daemons)
632
+ return job_colors[daemon_id]
623
633
 
624
- def _follow_pretty_print():
625
- watchgod = attempt_import('watchgod')
634
+ def follow_pretty_print():
635
+ watchfiles = attempt_import('watchfiles')
626
636
  rich = import_rich()
627
637
  rich_text = attempt_import('rich.text')
628
- _watch_daemon_ids = {d.daemon_id: d for d in daemons}
638
+ watch_daemon_ids = {d.daemon_id: d for d in daemons}
629
639
  info("Watching log files...")
630
640
 
631
- def _print_job_line(daemon, line):
641
+ previous_line_timestamp = None
642
+ def print_job_line(daemon, line):
643
+ nonlocal previous_line_timestamp
644
+ date_prefix_str = line[:len(now_str)]
645
+ try:
646
+ line_timestamp = datetime.strptime(date_prefix_str, timestamp_format)
647
+ previous_line_timestamp = line_timestamp
648
+ except Exception as e:
649
+ line_timestamp = None
650
+ if line_timestamp:
651
+ line = line[(len(now_str) + 3):]
652
+ else:
653
+ line_timestamp = previous_line_timestamp
654
+
655
+ if len(line) == 0 or line == '\n':
656
+ return
657
+
632
658
  text = rich_text.Text(daemon.daemon_id)
633
- text.append(
634
- _get_buffer_spaces(daemon.daemon_id) + '| '
635
- + (line[:-1] if line[-1] == '\n' else line)
659
+ line_prefix = (
660
+ get_buffer_spaces(daemon.daemon_id)
661
+ + (line_timestamp.strftime(follow_timestamp_format) if line_timestamp else '')
662
+ + ' | '
636
663
  )
664
+ text.append(line_prefix + (line[:-1] if line[-1] == '\n' else line))
637
665
  if ANSI:
638
666
  text.stylize(
639
- _get_job_colors(daemon.daemon_id),
667
+ get_job_colors(daemon.daemon_id),
640
668
  0,
641
- len(daemon.daemon_id) + len(_get_buffer_spaces(daemon.daemon_id)) + 1
669
+ len(daemon.daemon_id) + len(line_prefix)
642
670
  )
643
671
  get_console().print(text)
644
672
 
645
673
 
646
- def _print_log_lines(daemon):
674
+ def print_log_lines(daemon):
647
675
  for line in daemon.readlines():
648
- _print_job_line(daemon, line)
676
+ print_job_line(daemon, line)
649
677
 
650
- def _seek_back_offset(d) -> bool:
678
+ def seek_back_offset(d) -> bool:
651
679
  if d.log_offset_path.exists():
652
680
  d.log_offset_path.unlink()
653
681
 
654
682
  latest_subfile_path = d.rotating_log.get_latest_subfile_path()
655
683
  latest_subfile_index = d.rotating_log.get_latest_subfile_index()
684
+
685
+ ### Sometimes the latest file is empty.
686
+ if os.stat(latest_subfile_path).st_size == 0 and latest_subfile_index > 0:
687
+ latest_subfile_index -= 1
688
+ latest_subfile_path = d.rotating_log.get_subfile_path_from_index(
689
+ latest_subfile_index
690
+ )
691
+
656
692
  with open(latest_subfile_path, 'r', encoding='utf-8') as f:
657
693
  latest_lines = f.readlines()
658
694
 
@@ -672,12 +708,12 @@ def _show_logs(
672
708
  for d in daemons
673
709
  }
674
710
  for d in daemons:
675
- _seek_back_offset(d)
676
- _print_log_lines(d)
711
+ seek_back_offset(d)
712
+ print_log_lines(d)
677
713
 
678
714
  _quit = False
679
- async def _watch_logs():
680
- async for changes in watchgod.awatch(LOGS_RESOURCES_PATH):
715
+ def watch_logs():
716
+ for changes in watchfiles.watch(LOGS_RESOURCES_PATH):
681
717
  if _quit:
682
718
  return
683
719
  for change in changes:
@@ -688,7 +724,7 @@ def _show_logs(
688
724
  if not file_path.exists():
689
725
  continue
690
726
  daemon_id = file_path.name.split('.log')[0]
691
- if daemon_id not in _watch_daemon_ids and action:
727
+ if daemon_id not in watch_daemon_ids and action:
692
728
  continue
693
729
  try:
694
730
  daemon = Daemon(daemon_id=daemon_id)
@@ -697,22 +733,21 @@ def _show_logs(
697
733
  warn(f"Seeing new logs for non-existent job '{daemon_id}'.", stack=False)
698
734
 
699
735
  if daemon is not None:
700
- _print_log_lines(daemon)
736
+ print_log_lines(daemon)
701
737
 
702
- loop = asyncio.new_event_loop()
703
738
  try:
704
- loop.run_until_complete(_watch_logs())
705
- except KeyboardInterrupt:
739
+ watch_logs()
740
+ except KeyboardInterrupt as ki:
706
741
  _quit = True
707
742
 
708
- def _print_nopretty_log_text():
743
+ def print_nopretty_log_text():
709
744
  for d in daemons:
710
745
  log_text = d.log_text
711
746
  print(d.daemon_id)
712
747
  print(log_text)
713
748
 
714
- _print_log_text = _follow_pretty_print if not nopretty else _print_nopretty_log_text
715
- _print_log_text()
749
+ print_log_text = follow_pretty_print if not nopretty else print_nopretty_log_text
750
+ print_log_text()
716
751
 
717
752
  return True, "Success"
718
753
 
@@ -817,6 +852,57 @@ def _show_tags(
817
852
  return True, "Success"
818
853
 
819
854
 
855
+ def _show_schedules(
856
+ action: Optional[List[str]] = None,
857
+ nopretty: bool = False,
858
+ **kwargs: Any
859
+ ) -> SuccessTuple:
860
+ """
861
+ Print the upcoming timestamps according to the given schedule.
862
+
863
+ Examples:
864
+ show schedule 'daily starting 00:00'
865
+ show schedule 'every 12 hours and mon-fri starting 2024-01-01'
866
+ """
867
+ from meerschaum.utils.schedule import parse_schedule
868
+ from meerschaum.utils.misc import is_int
869
+ from meerschaum.utils.formatting import print_options
870
+ if not action:
871
+ return False, "Provide a schedule to be parsed."
872
+ schedule = action[0]
873
+ default_num_timestamps = 5
874
+ num_timestamps_str = action[1] if len(action) >= 2 else str(default_num_timestamps)
875
+ num_timestamps = (
876
+ int(num_timestamps_str)
877
+ if is_int(num_timestamps_str)
878
+ else default_num_timestamps
879
+ )
880
+ try:
881
+ trigger = parse_schedule(schedule)
882
+ except ValueError as e:
883
+ return False, str(e)
884
+
885
+ next_datetimes = []
886
+ for _ in range(num_timestamps):
887
+ try:
888
+ next_dt = trigger.next()
889
+ next_datetimes.append(next_dt)
890
+ except Exception as e:
891
+ break
892
+
893
+ print_options(
894
+ next_datetimes,
895
+ num_cols = 1,
896
+ nopretty = nopretty,
897
+ header = (
898
+ f"Next {min(num_timestamps, len(next_datetimes))} timestamps "
899
+ + f"for schedule '{schedule}':"
900
+ ),
901
+ )
902
+
903
+ return True, "Success"
904
+
905
+
820
906
 
821
907
  ### NOTE: This must be the final statement of the module.
822
908
  ### Any subactions added below these lines will not
@@ -453,16 +453,20 @@ def _wrap_pipe(
453
453
  return False, msg
454
454
  return True, "Success"
455
455
 
456
- hook_results = []
456
+ pre_hook_results, post_hook_results = [], []
457
457
  def apply_hooks(is_pre_sync: bool):
458
458
  _sync_hooks = (_pre_sync_hooks if is_pre_sync else _post_sync_hooks)
459
+ _hook_results = (pre_hook_results if is_pre_sync else post_hook_results)
459
460
  for module_name, sync_hooks in _sync_hooks.items():
460
461
  plugin_name = module_name.split('.')[-1] if module_name.startswith('plugins.') else None
461
462
  for sync_hook in sync_hooks:
462
463
  hook_result = pool.apply_async(call_sync_hook, (plugin_name, sync_hook))
463
- hook_results.append(hook_result)
464
+ _hook_results.append(hook_result)
464
465
 
465
466
  apply_hooks(True)
467
+ for hook_result in pre_hook_results:
468
+ hook_success, hook_msg = hook_result.get()
469
+ mrsm.pprint((hook_success, hook_msg))
466
470
 
467
471
  try:
468
472
  with Venv(get_connector_plugin(pipe.connector), debug=debug):
@@ -480,7 +484,7 @@ def _wrap_pipe(
480
484
  'sync_complete_timestamp': datetime.now(timezone.utc),
481
485
  })
482
486
  apply_hooks(False)
483
- for hook_result in hook_results:
487
+ for hook_result in post_hook_results:
484
488
  hook_success, hook_msg = hook_result.get()
485
489
  mrsm.pprint((hook_success, hook_msg))
486
490
 
@@ -30,25 +30,35 @@ _locks = {'pipes': RLock(), 'connector': RLock(), 'uvicorn_config': RLock()}
30
30
  CHECK_UPDATE = os.environ.get(STATIC_CONFIG['environment']['runtime'], None) != 'docker'
31
31
 
32
32
  endpoints = STATIC_CONFIG['api']['endpoints']
33
- aiofiles = attempt_import('aiofiles', lazy=False, check_update=CHECK_UPDATE)
34
- typing_extensions = attempt_import(
35
- 'typing_extensions', lazy=False, check_update=CHECK_UPDATE,
36
- venv = None,
37
- )
38
- pydantic_dataclasses = attempt_import(
39
- 'pydantic.dataclasses', lazy=False, check_update=CHECK_UPDATE,
33
+
34
+ (
35
+ fastapi,
36
+ aiofiles,
37
+ starlette_responses,
38
+ multipart,
39
+ packaging_version,
40
+ ) = attempt_import(
41
+ 'fastapi',
42
+ 'aiofiles',
43
+ 'starlette.responses',
44
+ 'multipart',
45
+ 'packaging.version',
46
+ lazy = False,
47
+ check_update = CHECK_UPDATE,
40
48
  )
41
- fastapi = attempt_import('fastapi', lazy=False, check_update=CHECK_UPDATE)
42
- starlette_reponses = attempt_import(
43
- 'starlette.responses', warn=False, lazy=False,
44
- check_update=CHECK_UPDATE,
49
+ (
50
+ typing_extensions,
51
+ uvicorn_workers,
52
+ ) = attempt_import(
53
+ 'typing_extensions',
54
+ 'uvicorn.workers',
55
+ lazy = False,
56
+ check_update = CHECK_UPDATE,
57
+ venv = None,
45
58
  )
46
- python_multipart = attempt_import('multipart', lazy=False, check_update=CHECK_UPDATE)
47
- packaging_version = attempt_import('packaging.version', check_update=CHECK_UPDATE)
48
59
  from meerschaum.api._chain import check_allow_chaining, DISALLOW_CHAINING_MESSAGE
49
60
  uvicorn_config_path = API_UVICORN_RESOURCES_PATH / SERVER_ID / 'config.json'
50
61
 
51
- uvicorn_workers = attempt_import('uvicorn.workers', venv=None, check_update=CHECK_UPDATE)
52
62
  uvicorn_config = None
53
63
  sys_config = get_config('system', 'api')
54
64
  permissions_config = get_config('system', 'api', 'permissions')
meerschaum/api/_oauth2.py CHANGED
@@ -7,11 +7,11 @@ Define JWT authorization here.
7
7
  """
8
8
 
9
9
  import os
10
- from meerschaum.api import app, endpoints
10
+ from meerschaum.api import app, endpoints, CHECK_UPDATE
11
11
  from meerschaum.utils.packages import attempt_import
12
- fastapi = attempt_import('fastapi', lazy=False)
13
- fastapi_responses = attempt_import('fastapi.responses', lazy=False)
14
- fastapi_login = attempt_import('fastapi_login')
12
+ fastapi = attempt_import('fastapi', lazy=False, check_update=CHECK_UPDATE)
13
+ fastapi_responses = attempt_import('fastapi.responses', lazy=False, check_update=CHECK_UPDATE)
14
+ fastapi_login = attempt_import('fastapi_login', check_update=CHECK_UPDATE)
15
15
 
16
16
  LoginManager = fastapi_login.LoginManager
17
17
  def generate_secret_key() -> str:
@@ -440,18 +440,13 @@ def update_flags(input_flags_dropdown_values, n_clicks, input_flags_texts):
440
440
  className = 'input-text',
441
441
  )
442
442
 
443
+ remove_index = trigger_dict['index'] if trigger_type == 'input-flags-remove-button' else None
443
444
  rows = [
444
445
  build_row(i, val, val_text)
445
446
  for i, (val, val_text) in enumerate(zip(input_flags_dropdown_values, input_flags_texts))
447
+ if i != remove_index
446
448
  ]
447
449
 
448
- if trigger_type == 'input-flags-remove-button':
449
- remove_index = trigger_dict['index']
450
- try:
451
- del rows[remove_index]
452
- except IndexError:
453
- pass
454
-
455
450
  if not rows or input_flags_dropdown_values[-1]:
456
451
  rows.append(build_row(len(rows), None, None))
457
452
 
@@ -488,11 +483,11 @@ def update_keys_options(
488
483
  """
489
484
  ctx = dash.callback_context
490
485
  trigger = ctx.triggered[0]['prop_id'].split('.')[0]
486
+ instance_click = trigger == 'instance-select'
491
487
 
492
488
  ### Update the instance first.
493
489
  update_instance_keys = False
494
490
  if not instance_keys:
495
- # instance_keys = get_config('meerschaum', 'web_instance')
496
491
  instance_keys = str(get_api_connector())
497
492
  update_instance_keys = True
498
493
  instance_alerts = []
@@ -516,20 +511,23 @@ def update_keys_options(
516
511
  if location_keys:
517
512
  num_filter += 1
518
513
 
519
- _ck_alone, _mk_alone, _lk_alone = False, False, False
520
- _ck_filter, _mk_filter, _lk_filter = connector_keys, metric_keys, location_keys
521
-
522
- _ck_alone = connector_keys and num_filter == 1
523
- _mk_alone = metric_keys and num_filter == 1
524
- _lk_alone = location_keys and num_filter == 1
514
+ _ck_filter = connector_keys
515
+ _mk_filter = metric_keys
516
+ _lk_filter = location_keys
517
+ _ck_alone = (connector_keys and num_filter == 1) or instance_click
518
+ _mk_alone = (metric_keys and num_filter == 1) or instance_click
519
+ _lk_alone = (location_keys and num_filter == 1) or instance_click
525
520
 
526
521
  from meerschaum.utils import fetch_pipes_keys
527
522
 
528
523
  try:
529
524
  _all_keys = fetch_pipes_keys('registered', get_web_connector(ctx.states))
530
525
  _keys = fetch_pipes_keys(
531
- 'registered', get_web_connector(ctx.states),
532
- connector_keys=_ck_filter, metric_keys=_mk_filter, location_keys=_lk_filter
526
+ 'registered',
527
+ get_web_connector(ctx.states),
528
+ connector_keys = _ck_filter,
529
+ metric_keys = _mk_filter,
530
+ location_keys = _lk_filter,
533
531
  )
534
532
  except Exception as e:
535
533
  instance_alerts += [alert_from_success_tuple((False, str(e)))]
@@ -545,15 +543,39 @@ def update_keys_options(
545
543
  k = locals()[key_type]
546
544
  if k not in _seen_keys[key_type]:
547
545
  _k = 'None' if k in (None, '[None]', 'None', 'null') else k
548
- options.append({'label' : _k, 'value' : _k})
546
+ options.append({'label': _k, 'value': _k})
549
547
  _seen_keys[key_type].add(k)
550
548
 
551
549
  add_options(_connectors_options, _all_keys if _ck_alone else _keys, 'ck')
552
550
  add_options(_metrics_options, _all_keys if _mk_alone else _keys, 'mk')
553
551
  add_options(_locations_options, _all_keys if _lk_alone else _keys, 'lk')
554
- connector_keys = [ck for ck in connector_keys if ck in [_ck['value'] for _ck in _connectors_options]]
555
- metric_keys = [mk for mk in metric_keys if mk in [_mk['value'] for _mk in _metrics_options]]
556
- location_keys = [lk for lk in location_keys if lk in [_lk['value'] for _lk in _locations_options]]
552
+ _connectors_options.sort(key=lambda x: str(x).lower())
553
+ _metrics_options.sort(key=lambda x: str(x).lower())
554
+ _locations_options.sort(key=lambda x: str(x).lower())
555
+ connector_keys = [
556
+ ck
557
+ for ck in connector_keys
558
+ if ck in [
559
+ _ck['value']
560
+ for _ck in _connectors_options
561
+ ]
562
+ ]
563
+ metric_keys = [
564
+ mk
565
+ for mk in metric_keys
566
+ if mk in [
567
+ _mk['value']
568
+ for _mk in _metrics_options
569
+ ]
570
+ ]
571
+ location_keys = [
572
+ lk
573
+ for lk in location_keys
574
+ if lk in [
575
+ _lk['value']
576
+ for _lk in _locations_options
577
+ ]
578
+ ]
557
579
  _connectors_datalist = [html.Option(value=o['value']) for o in _connectors_options]
558
580
  _metrics_datalist = [html.Option(value=o['value']) for o in _metrics_options]
559
581
  _locations_datalist = [html.Option(value=o['value']) for o in _locations_options]
@@ -680,6 +702,9 @@ dash_app.clientside_callback(
680
702
  Input({'type': 'pipe-download-csv-button', 'index': ALL}, 'n_clicks'),
681
703
  )
682
704
  def download_pipe_csv(n_clicks):
705
+ """
706
+ Download the most recent chunk as a CSV file.
707
+ """
683
708
  if not n_clicks:
684
709
  raise PreventUpdate
685
710
  ctx = dash.callback_context.triggered
@@ -688,11 +713,11 @@ def download_pipe_csv(n_clicks):
688
713
  pipe = pipe_from_ctx(ctx, 'n_clicks')
689
714
  if pipe is None:
690
715
  raise PreventUpdate
691
- filename = str(pipe.target) + '.csv'
692
716
  bounds = pipe.get_chunk_bounds(bounded=True, debug=debug)
693
- begin, _ = bounds[-1]
717
+ begin, end = bounds[-1]
718
+ filename = str(pipe.target) + f" {begin} - {end}.csv"
694
719
  try:
695
- df = pipe.get_data(begin=begin, end=None, debug=debug)
720
+ df = pipe.get_data(begin=begin, end=end, debug=debug)
696
721
  except Exception as e:
697
722
  df = None
698
723
  if df is not None:
@@ -818,6 +843,51 @@ def sync_documents_click(n_clicks, sync_editor_text):
818
843
  return alert_from_success_tuple((success, msg))
819
844
 
820
845
 
846
+ dash_app.clientside_callback(
847
+ """
848
+ function(n_clicks_arr, url){
849
+ display_block = {"display": "block"};
850
+
851
+ var clicked = false;
852
+ for (var i = 0; i < n_clicks_arr.length; i++){
853
+ if (n_clicks_arr[i]){
854
+ clicked = true;
855
+ break;
856
+ }
857
+ }
858
+ if (!clicked){ return dash_clientside.no_update; }
859
+
860
+ const triggered_id = dash_clientside.callback_context.triggered_id;
861
+ const action = triggered_id["action"];
862
+ const pipe_meta = JSON.parse(triggered_id["index"]);
863
+
864
+ iframe = document.getElementById('webterm-iframe');
865
+ if (!iframe){ return dash_clientside.no_update; }
866
+ var location = pipe_meta.location;
867
+ if (!pipe_meta.location){
868
+ location = "None";
869
+ }
870
+
871
+ iframe.contentWindow.postMessage(
872
+ {
873
+ action: action,
874
+ subaction: "pipes",
875
+ connector_keys: [pipe_meta.connector],
876
+ metric_keys: [pipe_meta.metric],
877
+ location_keys: [location],
878
+ instance: pipe_meta.instance,
879
+ },
880
+ url
881
+ );
882
+ dash_clientside.set_props("webterm-div", {"style": display_block});
883
+ return [];
884
+ }
885
+ """,
886
+ Output('content-div-right', 'children'),
887
+ Input({'type': 'manage-pipe-button', 'index': ALL, 'action': ALL}, 'n_clicks'),
888
+ State('location', 'href'),
889
+ )
890
+
821
891
  @dash_app.callback(
822
892
  Output("navbar-collapse", "is_open"),
823
893
  [Input("navbar-toggler", "n_clicks")],