meerschaum 3.0.0rc3__py3-none-any.whl → 3.0.0rc7__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 (126) hide show
  1. meerschaum/_internal/arguments/_parser.py +14 -2
  2. meerschaum/_internal/cli/__init__.py +6 -0
  3. meerschaum/_internal/cli/daemons.py +103 -0
  4. meerschaum/_internal/cli/entry.py +220 -0
  5. meerschaum/_internal/cli/workers.py +434 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +113 -19
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +3 -1
  11. meerschaum/_internal/term/TermPageHandler.py +1 -2
  12. meerschaum/_internal/term/__init__.py +40 -6
  13. meerschaum/_internal/term/tools.py +33 -8
  14. meerschaum/actions/__init__.py +6 -4
  15. meerschaum/actions/api.py +39 -11
  16. meerschaum/actions/attach.py +1 -0
  17. meerschaum/actions/delete.py +4 -2
  18. meerschaum/actions/edit.py +27 -8
  19. meerschaum/actions/login.py +8 -8
  20. meerschaum/actions/register.py +13 -7
  21. meerschaum/actions/reload.py +22 -5
  22. meerschaum/actions/restart.py +14 -0
  23. meerschaum/actions/show.py +69 -4
  24. meerschaum/actions/start.py +135 -14
  25. meerschaum/actions/stop.py +36 -3
  26. meerschaum/actions/sync.py +6 -1
  27. meerschaum/api/__init__.py +35 -13
  28. meerschaum/api/_events.py +7 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +103 -97
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/pipes.py +136 -57
  34. meerschaum/api/dash/callbacks/register.py +9 -2
  35. meerschaum/api/dash/callbacks/tokens.py +2 -1
  36. meerschaum/api/dash/components.py +6 -7
  37. meerschaum/api/dash/keys.py +17 -1
  38. meerschaum/api/dash/pages/login.py +2 -2
  39. meerschaum/api/dash/pages/pipes.py +14 -4
  40. meerschaum/api/dash/pipes.py +186 -65
  41. meerschaum/api/dash/tokens.py +1 -1
  42. meerschaum/api/dash/webterm.py +14 -6
  43. meerschaum/api/models/_pipes.py +7 -1
  44. meerschaum/api/resources/static/js/terminado.js +3 -0
  45. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  46. meerschaum/api/resources/templates/termpage.html +1 -0
  47. meerschaum/api/routes/_jobs.py +23 -11
  48. meerschaum/api/routes/_login.py +73 -5
  49. meerschaum/api/routes/_pipes.py +6 -4
  50. meerschaum/api/routes/_webterm.py +3 -3
  51. meerschaum/config/__init__.py +60 -13
  52. meerschaum/config/_default.py +89 -61
  53. meerschaum/config/_edit.py +10 -8
  54. meerschaum/config/_formatting.py +2 -0
  55. meerschaum/config/_patch.py +4 -2
  56. meerschaum/config/_paths.py +127 -12
  57. meerschaum/config/_read_config.py +20 -10
  58. meerschaum/config/_version.py +1 -1
  59. meerschaum/config/environment.py +262 -0
  60. meerschaum/config/stack/__init__.py +7 -5
  61. meerschaum/connectors/_Connector.py +1 -2
  62. meerschaum/connectors/__init__.py +37 -2
  63. meerschaum/connectors/api/_APIConnector.py +1 -1
  64. meerschaum/connectors/api/_jobs.py +11 -0
  65. meerschaum/connectors/api/_pipes.py +7 -1
  66. meerschaum/connectors/instance/_plugins.py +9 -1
  67. meerschaum/connectors/instance/_tokens.py +20 -3
  68. meerschaum/connectors/instance/_users.py +8 -1
  69. meerschaum/connectors/parse.py +1 -1
  70. meerschaum/connectors/sql/_create_engine.py +3 -0
  71. meerschaum/connectors/sql/_pipes.py +98 -79
  72. meerschaum/connectors/sql/_users.py +8 -1
  73. meerschaum/connectors/sql/tables/__init__.py +20 -3
  74. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  75. meerschaum/connectors/valkey/_pipes.py +7 -5
  76. meerschaum/core/Pipe/__init__.py +62 -72
  77. meerschaum/core/Pipe/_attributes.py +66 -90
  78. meerschaum/core/Pipe/_cache.py +555 -0
  79. meerschaum/core/Pipe/_clear.py +0 -11
  80. meerschaum/core/Pipe/_data.py +0 -50
  81. meerschaum/core/Pipe/_deduplicate.py +0 -13
  82. meerschaum/core/Pipe/_delete.py +12 -21
  83. meerschaum/core/Pipe/_drop.py +11 -23
  84. meerschaum/core/Pipe/_dtypes.py +1 -1
  85. meerschaum/core/Pipe/_index.py +8 -14
  86. meerschaum/core/Pipe/_sync.py +12 -18
  87. meerschaum/core/Plugin/_Plugin.py +7 -1
  88. meerschaum/core/Token/_Token.py +1 -1
  89. meerschaum/core/User/_User.py +1 -2
  90. meerschaum/jobs/_Executor.py +88 -4
  91. meerschaum/jobs/_Job.py +135 -35
  92. meerschaum/jobs/systemd.py +7 -2
  93. meerschaum/plugins/__init__.py +277 -81
  94. meerschaum/utils/_get_pipes.py +30 -4
  95. meerschaum/utils/daemon/Daemon.py +195 -41
  96. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  97. meerschaum/utils/daemon/RotatingFile.py +63 -36
  98. meerschaum/utils/daemon/StdinFile.py +53 -13
  99. meerschaum/utils/daemon/__init__.py +18 -5
  100. meerschaum/utils/daemon/_names.py +6 -3
  101. meerschaum/utils/debug.py +34 -4
  102. meerschaum/utils/dtypes/__init__.py +5 -1
  103. meerschaum/utils/formatting/__init__.py +4 -1
  104. meerschaum/utils/formatting/_jobs.py +1 -1
  105. meerschaum/utils/formatting/_pipes.py +47 -46
  106. meerschaum/utils/formatting/_pprint.py +1 -0
  107. meerschaum/utils/formatting/_shell.py +16 -6
  108. meerschaum/utils/misc.py +18 -38
  109. meerschaum/utils/packages/__init__.py +15 -13
  110. meerschaum/utils/packages/_packages.py +1 -0
  111. meerschaum/utils/pipes.py +39 -7
  112. meerschaum/utils/process.py +1 -1
  113. meerschaum/utils/prompt.py +171 -144
  114. meerschaum/utils/sql.py +12 -2
  115. meerschaum/utils/threading.py +42 -0
  116. meerschaum/utils/venv/__init__.py +2 -0
  117. meerschaum/utils/warnings.py +19 -13
  118. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/METADATA +3 -1
  119. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/RECORD +125 -119
  120. meerschaum/config/_environment.py +0 -145
  121. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/WHEEL +0 -0
  122. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/entry_points.txt +0 -0
  123. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  124. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/NOTICE +0 -0
  125. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/top_level.txt +0 -0
  126. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/zip-safe +0 -0
@@ -21,7 +21,6 @@ from meerschaum.config._paths import API_UVICORN_CONFIG_PATH, API_UVICORN_RESOUR
21
21
  from meerschaum.plugins import _api_plugins
22
22
  from meerschaum.utils.warnings import warn, dprint
23
23
  from meerschaum.utils.threading import RLock
24
- from meerschaum.utils.misc import is_pipe_registered
25
24
  from meerschaum.connectors.parse import parse_instance_keys
26
25
 
27
26
  from meerschaum import __version__ as version
@@ -67,16 +66,13 @@ from meerschaum.api._exceptions import APIPermissionError
67
66
  uvicorn_config_path = API_UVICORN_RESOURCES_PATH / SERVER_ID / 'config.json'
68
67
 
69
68
  uvicorn_config = None
70
- sys_config = get_config('system', 'api')
71
- permissions_config = get_config('system', 'api', 'permissions')
69
+ sys_config = get_config('api')
70
+ permissions_config = get_config('api', 'permissions')
72
71
 
73
72
  def get_uvicorn_config() -> Dict[str, Any]:
74
73
  """Read the Uvicorn configuration JSON and return a dictionary."""
75
74
  global uvicorn_config
76
75
  import json
77
- runtime = os.environ.get(STATIC_CONFIG['environment']['runtime'], None)
78
- if runtime == 'api':
79
- return get_config('system', 'api', 'uvicorn')
80
76
  _uvicorn_config = uvicorn_config
81
77
  with _locks['uvicorn_config']:
82
78
  if uvicorn_config is None:
@@ -85,6 +81,8 @@ def get_uvicorn_config() -> Dict[str, Any]:
85
81
  uvicorn_config = json.load(f)
86
82
  _uvicorn_config = uvicorn_config
87
83
  except Exception:
84
+ import traceback
85
+ traceback.print_exc()
88
86
  _uvicorn_config = sys_config.get('uvicorn', None)
89
87
 
90
88
  if _uvicorn_config is None:
@@ -102,6 +100,10 @@ production = get_uvicorn_config().get('production', False)
102
100
  _include_dash = (not no_dash)
103
101
  _include_webterm = (not no_webterm) and _include_dash
104
102
  docs_enabled = not production or sys_config.get('endpoints', {}).get('docs_in_production', True)
103
+ webterm_port = (
104
+ get_uvicorn_config().get('webterm_port', None)
105
+ or mrsm.get_config('api', 'webterm', 'port')
106
+ )
105
107
 
106
108
  default_instance_keys = None
107
109
  _instance_connectors = defaultdict(lambda: None)
@@ -129,7 +131,7 @@ def get_api_connector(instance_keys: Optional[str] = None):
129
131
  )
130
132
  found_match: bool = False
131
133
  for allowed_keys_pattern in allowed_instance_keys:
132
- if fnmatch(instance_keys, allowed_keys_pattern):
134
+ if fnmatch(str(instance_keys), allowed_keys_pattern):
133
135
  found_match = True
134
136
  break
135
137
  if not found_match:
@@ -141,7 +143,9 @@ def get_api_connector(instance_keys: Optional[str] = None):
141
143
  if _instance_connectors[instance_keys] is None:
142
144
  try:
143
145
  is_valid_connector = True
144
- _instance_connectors[instance_keys] = parse_instance_keys(instance_keys, debug=debug)
146
+ instance_connector = parse_instance_keys(instance_keys, debug=debug)
147
+ instance_connector._cache_connector = get_cache_connector()
148
+ _instance_connectors[instance_keys] = instance_connector
145
149
  except Exception:
146
150
  is_valid_connector = False
147
151
 
@@ -168,7 +172,7 @@ def get_cache_connector(connector_keys: Optional[str] = None):
168
172
  return None
169
173
 
170
174
  connector_keys = connector_keys or get_config(
171
- 'system', 'api', 'cache', 'connector',
175
+ 'api', 'cache', 'connector',
172
176
  warn=False,
173
177
  )
174
178
  if connector_keys is None:
@@ -196,7 +200,11 @@ def pipes(instance_keys: Optional[str] = None, refresh: bool = False) -> PipesDi
196
200
  with _locks['pipes-' + instance_keys]:
197
201
  pipes = _instance_pipes[instance_keys]
198
202
  if pipes is None or refresh:
199
- pipes = _get_pipes(mrsm_instance=instance_keys)
203
+ pipes = _get_pipes(
204
+ mrsm_instance=instance_keys,
205
+ cache=True,
206
+ cache_connector_keys=get_cache_connector(),
207
+ )
200
208
  _instance_pipes[instance_keys] = pipes
201
209
  return pipes
202
210
 
@@ -218,9 +226,23 @@ def get_pipe(
218
226
  detail="Unable to serve any pipes with connector keys `mrsm` over the API.",
219
227
  )
220
228
 
221
- pipe = mrsm.Pipe(connector_keys, metric_key, location_key, mrsm_instance=instance_keys)
222
- if is_pipe_registered(pipe, pipes(instance_keys, refresh=False)):
223
- return pipes(instance_keys, refresh=refresh)[connector_keys][metric_key][location_key]
229
+ pipes_dict = pipes(instance_keys)
230
+ if (
231
+ not refresh
232
+ and connector_keys in pipes_dict
233
+ and metric_key in pipes_dict[connector_keys]
234
+ and location_key in pipes_dict[connector_keys][metric_key]
235
+ ):
236
+ return pipes_dict[connector_keys][metric_key][location_key]
237
+
238
+ pipe = mrsm.Pipe(
239
+ connector_keys,
240
+ metric_key,
241
+ location_key,
242
+ mrsm_instance=instance_keys,
243
+ cache=True,
244
+ cache_connector_keys=get_cache_connector(),
245
+ )
224
246
  return pipe
225
247
 
226
248
 
meerschaum/api/_events.py CHANGED
@@ -8,13 +8,13 @@ Declare FastAPI events in this module (startup, shutdown, etc.).
8
8
 
9
9
  import sys
10
10
  import os
11
- import time
12
11
  from meerschaum.api import (
13
12
  app,
14
13
  get_api_connector,
15
14
  get_cache_connector,
16
15
  get_uvicorn_config,
17
16
  debug,
17
+ webterm_port,
18
18
  no_dash,
19
19
  _include_dash,
20
20
  _include_webterm,
@@ -75,7 +75,7 @@ async def startup():
75
75
  try:
76
76
  if _include_webterm:
77
77
  from meerschaum.api.dash.webterm import start_webterm
78
- start_webterm()
78
+ start_webterm(webterm_port=webterm_port)
79
79
 
80
80
  connected = retry_connect(
81
81
  get_api_connector(),
@@ -98,6 +98,11 @@ async def startup():
98
98
  await shutdown()
99
99
  os._exit(1)
100
100
 
101
+ conn = get_api_connector()
102
+ if conn.type == 'sql':
103
+ from meerschaum.connectors.sql.tables import get_tables
104
+ _ = get_tables(conn, refresh=True, create=True, debug=debug)
105
+
101
106
  start_check_jobs_thread()
102
107
 
103
108
 
meerschaum/api/_oauth2.py CHANGED
@@ -21,6 +21,7 @@ from meerschaum.core import User, Token
21
21
  fastapi, starlette = attempt_import('fastapi', 'starlette', lazy=False, check_update=CHECK_UPDATE)
22
22
  fastapi_responses = attempt_import('fastapi.responses', lazy=False, check_update=CHECK_UPDATE)
23
23
  fastapi_login = attempt_import('fastapi_login', check_update=CHECK_UPDATE)
24
+ jose_jwt, jose_exceptions = attempt_import('jose.jwt', 'jose.exceptions', lazy=False, check_update=CHECK_UPDATE)
24
25
  from fastapi import Depends, HTTPException, Request
25
26
  from starlette import status
26
27
 
@@ -91,6 +92,7 @@ async def load_user_or_token(
91
92
  status_code=status.HTTP_401_UNAUTHORIZED,
92
93
  detail="Not authenticated.",
93
94
  )
95
+
94
96
  authorization = authorization.replace('Basic ', '').replace('Bearer ', '')
95
97
  if not authorization.startswith('mrsm-key:'):
96
98
  if not users:
@@ -98,12 +100,15 @@ async def load_user_or_token(
98
100
  status=status.HTTP_401_UNAUTHORIZED,
99
101
  detail="Users not authenticated for this endpoint.",
100
102
  )
103
+
101
104
  return await manager(request)
105
+
102
106
  if not tokens:
103
107
  raise HTTPException(
104
108
  status_code=status.HTTP_401_UNAUTHORIZED,
105
109
  detail="Tokens not authenticated for this endpoint.",
106
110
  )
111
+
107
112
  return get_token_from_authorization(authorization)
108
113
 
109
114
 
@@ -112,6 +117,7 @@ def ScopedAuth(scopes: List[str]):
112
117
  Dependency factory for authenticating with either a user session or a scoped token.
113
118
  """
114
119
  async def _authenticate(
120
+ request: Request,
115
121
  user_or_token: Union[User, Token, None] = Depends(
116
122
  load_user_or_token,
117
123
  ),
@@ -126,12 +132,45 @@ def ScopedAuth(scopes: List[str]):
126
132
  headers={"WWW-Authenticate": "Basic"},
127
133
  )
128
134
 
129
- fresh_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
130
- if '*' in fresh_scopes:
135
+ authorization = request.headers.get('authorization', request.headers.get('Authorization', None))
136
+ is_long_lived = authorization and 'mrsm-key:' in authorization
137
+
138
+ current_scopes = []
139
+ ### For long-lived API tokens, always hit the database.
140
+ if is_long_lived:
141
+ current_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
142
+
143
+ ### For JWTs, trust the scopes in the token.
144
+ else:
145
+ if not authorization:
146
+ # This should be caught by `load_user_or_token` but we can be safe.
147
+ raise HTTPException(
148
+ status_code=status.HTTP_401_UNAUTHORIZED,
149
+ detail="Not authenticated.",
150
+ )
151
+
152
+ scheme, _, token_str = authorization.partition(' ')
153
+ if not token_str or scheme.lower() != 'bearer':
154
+ raise HTTPException(
155
+ status_code=status.HTTP_401_UNAUTHORIZED,
156
+ detail="Unsupported authentication scheme.",
157
+ )
158
+
159
+ try:
160
+ payload = jose_jwt.decode(token_str, SECRET, algorithms=['HS256'])
161
+ current_scopes = payload.get('scopes', [])
162
+ except jose_exceptions.JWTError:
163
+ raise HTTPException(
164
+ status_code=status.HTTP_401_UNAUTHORIZED,
165
+ detail="Invalid access token.",
166
+ headers={"WWW-Authenticate": "Bearer"},
167
+ )
168
+
169
+ if '*' in current_scopes:
131
170
  return user_or_token
132
171
 
133
172
  for scope in scopes:
134
- if scope not in fresh_scopes:
173
+ if scope not in current_scopes:
135
174
  raise HTTPException(
136
175
  status_code=status.HTTP_403_FORBIDDEN,
137
176
  detail=f"Missing required scope: '{scope}'",
@@ -159,4 +198,8 @@ def generate_secret_key() -> bytes:
159
198
 
160
199
 
161
200
  SECRET = generate_secret_key()
162
- manager = LoginManager(SECRET, token_url=endpoints['login'])
201
+ manager = LoginManager(
202
+ SECRET,
203
+ token_url=endpoints['login'],
204
+ scopes=STATIC_CONFIG['tokens']['scopes'],
205
+ )
@@ -498,13 +498,9 @@ def update_flags(input_flags_dropdown_values, n_clicks, input_flags_texts):
498
498
 
499
499
  @dash_app.callback(
500
500
  Output('connector-keys-dropdown', 'options'),
501
- Output('connector-keys-dropdown', 'value'),
502
501
  Output('metric-keys-dropdown', 'options'),
503
- Output('metric-keys-dropdown', 'value'),
504
502
  Output('location-keys-dropdown', 'options'),
505
- Output('location-keys-dropdown', 'value'),
506
503
  Output('tags-dropdown', 'options'),
507
- Output('tags-dropdown', 'value'),
508
504
  Output('instance-select', 'value'),
509
505
  Output('instance-alert-div', 'children'),
510
506
  Input('connector-keys-dropdown', 'value'),
@@ -546,15 +542,6 @@ def update_keys_options(
546
542
  except Exception as e:
547
543
  instance_alerts += [alert_from_success_tuple((False, str(e)))]
548
544
 
549
- ### Update the keys filters.
550
- if connector_keys is None:
551
- connector_keys = []
552
- if metric_keys is None:
553
- metric_keys = []
554
- if location_keys is None:
555
- location_keys = []
556
- if tags is None:
557
- tags = []
558
545
  num_filter = 0
559
546
  if connector_keys:
560
547
  num_filter += 1
@@ -565,10 +552,6 @@ def update_keys_options(
565
552
  if tags:
566
553
  num_filter += 1
567
554
 
568
- _ck_filter = connector_keys
569
- _mk_filter = metric_keys
570
- _lk_filter = location_keys
571
- _tags_filter = tags
572
555
  _ck_alone = (connector_keys and num_filter == 1) or instance_click
573
556
  _mk_alone = (metric_keys and num_filter == 1) or instance_click
574
557
  _lk_alone = (location_keys and num_filter == 1) or instance_click
@@ -581,101 +564,95 @@ def update_keys_options(
581
564
  _keys = fetch_pipes_keys(
582
565
  'registered',
583
566
  get_web_connector(ctx.states),
584
- connector_keys=_ck_filter,
585
- metric_keys=_mk_filter,
586
- location_keys=_lk_filter,
587
- tags=_tags_filter,
567
+ connector_keys=connector_keys,
568
+ metric_keys=metric_keys,
569
+ location_keys=location_keys,
570
+ tags=tags,
588
571
  )
589
572
  _tags_pipes = mrsm.get_pipes(
590
- connector_keys=_ck_filter,
591
- metric_keys=_mk_filter,
592
- location_keys=_lk_filter,
593
- tags=_tags_filter,
573
+ connector_keys=connector_keys,
574
+ metric_keys=metric_keys,
575
+ location_keys=location_keys,
576
+ tags=tags,
594
577
  instance=get_web_connector(ctx.states),
595
578
  as_tags_dict=True,
596
579
  )
597
580
  _all_tags = list(
598
- mrsm.get_pipes(
599
- instance=get_web_connector(ctx.states),
600
- as_tags_dict=True,
601
- )
581
+ set(
582
+ mrsm.get_pipes(
583
+ instance=get_web_connector(ctx.states),
584
+ as_tags_dict=True,
585
+ )
586
+ ).union(tags or [])
602
587
  ) if _tags_alone else []
603
588
  except Exception as e:
604
589
  instance_alerts += [alert_from_success_tuple((False, str(e)))]
605
- _all_keys, _keys = [], []
606
- _connectors_options = []
607
- _metrics_options = []
608
- _locations_options = []
609
- _tags_options = []
610
-
611
- _seen_keys = {'ck' : set(), 'mk' : set(), 'lk' : set(), 'tags': set()}
612
-
613
- def add_options(options, keys, key_type):
614
- for ck, mk, lk in keys:
615
- k = locals()[key_type]
616
- if k not in _seen_keys[key_type]:
617
- _k = 'None' if k in (None, '[None]', 'None', 'null') else k
618
- options.append({'label': _k, 'value': _k})
619
- _seen_keys[key_type].add(k)
620
-
621
- add_options(_connectors_options, _all_keys if _ck_alone else _keys, 'ck')
622
- add_options(_metrics_options, _all_keys if _mk_alone else _keys, 'mk')
623
- add_options(_locations_options, _all_keys if _lk_alone else _keys, 'lk')
624
-
625
- _tags_options = [
626
- {'label': tag, 'value': tag}
627
- for tag in (_all_tags if _tags_alone else _tags_pipes)
628
- ]
590
+ _all_keys, _all_tags, _keys = [], [], []
591
+
592
+ connectors_options = sorted(
593
+ list(
594
+ set(
595
+ keys_tuple[0] for keys_tuple in (_all_keys if _ck_alone else _keys)
596
+ ).union(set(connector_keys or []))
597
+ ),
598
+ key=(lambda x: str(x).lower()),
599
+ )
600
+ metrics_options = sorted(
601
+ list(
602
+ set(
603
+ keys_tuple[1] for keys_tuple in (_all_keys if _mk_alone else _keys)
604
+ ).union(set(metric_keys or []))
605
+ ),
606
+ key=(lambda x: str(x).lower()),
607
+ )
608
+ locations_options = sorted(
609
+ list(
610
+ set(
611
+ (
612
+ str(keys_tuple[2])
613
+ for keys_tuple in (_all_keys if _lk_alone else _keys)
614
+ )
615
+ ).union(set((str(_lk) for _lk in (location_keys or []))))
616
+ ),
617
+ key=(lambda x: str(x).lower()),
618
+ )
619
+
620
+ tags_options = sorted(
621
+ list(
622
+ set(
623
+ (_all_tags if _tags_alone else _tags_pipes)
624
+ ).union(set(tags or []))
625
+ ),
626
+ key=(lambda x: str(x).lower()),
627
+ )
629
628
 
630
- _connectors_options.sort(key=lambda x: str(x).lower())
631
- _metrics_options.sort(key=lambda x: str(x).lower())
632
- _locations_options.sort(key=lambda x: str(x).lower())
633
- _tags_options.sort(key=lambda x: str(x).lower())
634
- connector_keys = [
635
- ck
636
- for ck in connector_keys
637
- if ck in [
638
- _ck['value']
639
- for _ck in _connectors_options
640
- ]
641
- ]
642
- metric_keys = [
643
- mk
644
- for mk in metric_keys
645
- if mk in [
646
- _mk['value']
647
- for _mk in _metrics_options
648
- ]
649
- ]
650
- location_keys = [
651
- lk
652
- for lk in location_keys
653
- if lk in [
654
- _lk['value']
655
- for _lk in _locations_options
656
- ]
657
- ]
658
- tags = [
659
- tag
660
- for tag in tags
661
- if tag in [
662
- _tag['value']
663
- for _tag in _tags_options
664
- ]
665
- ]
666
629
  return (
667
- _connectors_options,
668
- connector_keys,
669
- _metrics_options,
670
- metric_keys,
671
- _locations_options,
672
- location_keys,
673
- _tags_options,
674
- tags,
630
+ connectors_options,
631
+ metrics_options,
632
+ locations_options,
633
+ tags_options,
675
634
  (instance_keys if update_instance_keys else dash.no_update),
676
635
  instance_alerts,
677
636
  )
678
637
 
638
+
639
+ @dash_app.callback(
640
+ Output('connector-keys-dropdown', 'value'),
641
+ Output('metric-keys-dropdown', 'value'),
642
+ Output('location-keys-dropdown', 'value'),
643
+ Output('tags-dropdown', 'value'),
644
+ Input('clear-all-keys-button', 'n_clicks'),
645
+ prevent_initial_call=True,
646
+ )
647
+ def clear_all_keys_button_click(n_clicks):
648
+ """
649
+ Clear the keys dropdowns when the `Clear all` button is clicked.
650
+ """
651
+ if not n_clicks:
652
+ raise PreventUpdate
653
+
654
+ return [], [], [], []
655
+
679
656
  dash_app.clientside_callback(
680
657
  """
681
658
  function(
@@ -1216,3 +1193,32 @@ def toggle_pages_offcanvas(n_clicks: Optional[int], is_open: bool):
1216
1193
  if n_clicks:
1217
1194
  return not is_open, pages_children
1218
1195
  return is_open, pages_children
1196
+
1197
+
1198
+ @dash_app.callback(
1199
+ Output({'type': 'calculate-rowcount-div', 'index': MATCH}, 'children'),
1200
+ Input({'type': 'calculate-rowcount-button', 'index': MATCH}, 'n_clicks'),
1201
+ prevent_initial_call=True,
1202
+ )
1203
+ def calculate_rowcount_button_click(n_clicks: int):
1204
+ """
1205
+ Calculate the rowcount for the pipe.
1206
+ """
1207
+ if not n_clicks:
1208
+ raise PreventUpdate
1209
+
1210
+ triggered = dash.callback_context.triggered
1211
+ if triggered[0]['value'] is None:
1212
+ raise PreventUpdate
1213
+
1214
+ pipe = pipe_from_ctx(triggered, 'n_clicks')
1215
+ if pipe is None:
1216
+ raise PreventUpdate
1217
+
1218
+ try:
1219
+ rowcount = pipe.get_rowcount(debug=debug)
1220
+ return f"{rowcount:,}"
1221
+ except Exception as e:
1222
+ return (
1223
+ alert_from_success_tuple((False, f"Failed to calculate row count: {e}"))
1224
+ )
@@ -13,6 +13,7 @@ import time
13
13
  import traceback
14
14
  from datetime import datetime, timezone
15
15
 
16
+ import meerschaum as mrsm
16
17
  from meerschaum.jobs import get_jobs
17
18
  from meerschaum.utils.typing import Optional, Dict, Any
18
19
  from meerschaum.api import CHECK_UPDATE
@@ -136,12 +137,12 @@ def manage_job_button_click(
136
137
  old_status = job.status
137
138
  try:
138
139
  success, msg = manage_functions[manage_job_action]()
139
- except Exception as e:
140
+ except Exception:
140
141
  success, msg = False, traceback.format_exc()
141
142
 
142
143
  ### Wait for a status change before building the elements.
143
144
  timeout_seconds = 1.0
144
- check_interval_seconds = 0.01
145
+ check_interval_seconds = mrsm.get_config('system', 'cli', 'refresh_seconds')
145
146
  begin = time.perf_counter()
146
147
  while (time.perf_counter() - begin) < timeout_seconds:
147
148
  if job.status != old_status:
@@ -16,11 +16,14 @@ from meerschaum.utils.typing import Optional
16
16
  dash = attempt_import('dash', lazy=False, check_update=CHECK_UPDATE)
17
17
  from dash.exceptions import PreventUpdate
18
18
  from dash.dependencies import Input, Output, State
19
+
19
20
  from meerschaum.api.dash import dash_app, debug, pipes, _get_pipes
20
21
  from meerschaum.api.dash.sessions import set_session
21
22
  from meerschaum.api.dash.connectors import get_web_connector
22
23
  from meerschaum.api.routes._login import login
23
24
  from meerschaum.api.dash.components import alert_from_success_tuple
25
+ from meerschaum.api._oauth2 import CustomOAuth2PasswordRequestForm
26
+ from meerschaum._internal.static import STATIC_CONFIG
24
27
  from fastapi_login.exceptions import InvalidCredentialsException
25
28
  from fastapi.exceptions import HTTPException
26
29
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
@@ -73,7 +76,13 @@ def login_button_click(
73
76
  raise PreventUpdate
74
77
 
75
78
  try:
76
- _ = login({'username': username, 'password': password})
79
+ form = CustomOAuth2PasswordRequestForm(
80
+ grant_type='password',
81
+ username=username,
82
+ password=password,
83
+ scope=' '.join(STATIC_CONFIG['tokens']['scopes'])
84
+ )
85
+ _ = login(form)
77
86
  session_id = str(uuid.uuid4())
78
87
  session_data = {
79
88
  'session-id': session_id,