meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc1__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 (153) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +17 -1
  5. meerschaum/_internal/entry.py +6 -6
  6. meerschaum/_internal/shell/Shell.py +1 -1
  7. meerschaum/_internal/static.py +372 -0
  8. meerschaum/actions/api.py +12 -2
  9. meerschaum/actions/bootstrap.py +7 -7
  10. meerschaum/actions/edit.py +142 -18
  11. meerschaum/actions/register.py +137 -6
  12. meerschaum/actions/show.py +117 -29
  13. meerschaum/actions/stop.py +4 -1
  14. meerschaum/actions/sync.py +1 -1
  15. meerschaum/actions/tag.py +9 -8
  16. meerschaum/api/__init__.py +9 -2
  17. meerschaum/api/_events.py +39 -2
  18. meerschaum/api/_oauth2.py +118 -8
  19. meerschaum/api/_tokens.py +102 -0
  20. meerschaum/api/dash/__init__.py +0 -1
  21. meerschaum/api/dash/callbacks/custom.py +2 -2
  22. meerschaum/api/dash/callbacks/dashboard.py +102 -18
  23. meerschaum/api/dash/callbacks/plugins.py +0 -1
  24. meerschaum/api/dash/callbacks/register.py +1 -1
  25. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  26. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  27. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  28. meerschaum/api/dash/components.py +30 -8
  29. meerschaum/api/dash/keys.py +19 -93
  30. meerschaum/api/dash/pages/dashboard.py +1 -20
  31. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  32. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  33. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  34. meerschaum/api/dash/pipes.py +94 -59
  35. meerschaum/api/dash/sessions.py +12 -0
  36. meerschaum/api/dash/tokens.py +606 -0
  37. meerschaum/api/dash/websockets.py +1 -1
  38. meerschaum/api/dash/webterm.py +4 -0
  39. meerschaum/api/models/__init__.py +23 -3
  40. meerschaum/api/models/_actions.py +22 -0
  41. meerschaum/api/models/_pipes.py +85 -7
  42. meerschaum/api/models/_tokens.py +81 -0
  43. meerschaum/api/resources/templates/termpage.html +12 -0
  44. meerschaum/api/routes/__init__.py +1 -0
  45. meerschaum/api/routes/_actions.py +3 -4
  46. meerschaum/api/routes/_connectors.py +3 -7
  47. meerschaum/api/routes/_jobs.py +14 -35
  48. meerschaum/api/routes/_login.py +49 -12
  49. meerschaum/api/routes/_misc.py +5 -10
  50. meerschaum/api/routes/_pipes.py +134 -111
  51. meerschaum/api/routes/_plugins.py +38 -28
  52. meerschaum/api/routes/_tokens.py +236 -0
  53. meerschaum/api/routes/_users.py +47 -35
  54. meerschaum/api/routes/_version.py +3 -3
  55. meerschaum/config/__init__.py +43 -20
  56. meerschaum/config/_default.py +32 -5
  57. meerschaum/config/_edit.py +28 -24
  58. meerschaum/config/_environment.py +1 -1
  59. meerschaum/config/_patch.py +6 -6
  60. meerschaum/config/_paths.py +5 -1
  61. meerschaum/config/_read_config.py +65 -34
  62. meerschaum/config/_sync.py +6 -3
  63. meerschaum/config/_version.py +1 -1
  64. meerschaum/config/stack/__init__.py +24 -5
  65. meerschaum/config/static.py +18 -0
  66. meerschaum/connectors/_Connector.py +10 -4
  67. meerschaum/connectors/__init__.py +4 -20
  68. meerschaum/connectors/api/_APIConnector.py +34 -6
  69. meerschaum/connectors/api/_actions.py +2 -2
  70. meerschaum/connectors/api/_jobs.py +1 -1
  71. meerschaum/connectors/api/_login.py +33 -7
  72. meerschaum/connectors/api/_misc.py +2 -2
  73. meerschaum/connectors/api/_pipes.py +15 -14
  74. meerschaum/connectors/api/_plugins.py +2 -2
  75. meerschaum/connectors/api/_request.py +1 -1
  76. meerschaum/connectors/api/_tokens.py +146 -0
  77. meerschaum/connectors/api/_users.py +70 -58
  78. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  79. meerschaum/connectors/instance/__init__.py +10 -0
  80. meerschaum/connectors/instance/_pipes.py +442 -0
  81. meerschaum/connectors/instance/_plugins.py +151 -0
  82. meerschaum/connectors/instance/_tokens.py +296 -0
  83. meerschaum/connectors/instance/_users.py +181 -0
  84. meerschaum/connectors/parse.py +4 -1
  85. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  86. meerschaum/connectors/sql/_cli.py +12 -11
  87. meerschaum/connectors/sql/_create_engine.py +6 -154
  88. meerschaum/connectors/sql/_fetch.py +2 -18
  89. meerschaum/connectors/sql/_pipes.py +42 -31
  90. meerschaum/connectors/sql/_plugins.py +29 -0
  91. meerschaum/connectors/sql/_sql.py +8 -1
  92. meerschaum/connectors/sql/_users.py +29 -2
  93. meerschaum/connectors/sql/tables/__init__.py +1 -1
  94. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  95. meerschaum/connectors/valkey/_pipes.py +9 -10
  96. meerschaum/connectors/valkey/_plugins.py +2 -26
  97. meerschaum/core/Pipe/__init__.py +31 -14
  98. meerschaum/core/Pipe/_attributes.py +156 -58
  99. meerschaum/core/Pipe/_bootstrap.py +54 -24
  100. meerschaum/core/Pipe/_data.py +41 -1
  101. meerschaum/core/Pipe/_dtypes.py +29 -14
  102. meerschaum/core/Pipe/_edit.py +12 -4
  103. meerschaum/core/Pipe/_show.py +5 -5
  104. meerschaum/core/Pipe/_sync.py +48 -53
  105. meerschaum/core/Pipe/_verify.py +1 -1
  106. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  107. meerschaum/core/Plugin/__init__.py +1 -1
  108. meerschaum/core/Token/_Token.py +221 -0
  109. meerschaum/core/Token/__init__.py +12 -0
  110. meerschaum/core/User/_User.py +34 -8
  111. meerschaum/core/User/__init__.py +9 -1
  112. meerschaum/core/__init__.py +1 -0
  113. meerschaum/jobs/_Job.py +3 -2
  114. meerschaum/jobs/__init__.py +3 -2
  115. meerschaum/jobs/systemd.py +1 -1
  116. meerschaum/models/__init__.py +35 -0
  117. meerschaum/models/pipes.py +247 -0
  118. meerschaum/models/tokens.py +38 -0
  119. meerschaum/models/users.py +26 -0
  120. meerschaum/plugins/__init__.py +22 -7
  121. meerschaum/plugins/bootstrap.py +2 -1
  122. meerschaum/utils/_get_pipes.py +68 -27
  123. meerschaum/utils/daemon/Daemon.py +2 -1
  124. meerschaum/utils/daemon/__init__.py +30 -2
  125. meerschaum/utils/dataframe.py +95 -14
  126. meerschaum/utils/dtypes/__init__.py +91 -18
  127. meerschaum/utils/dtypes/sql.py +44 -0
  128. meerschaum/utils/formatting/__init__.py +1 -1
  129. meerschaum/utils/formatting/_pipes.py +5 -4
  130. meerschaum/utils/formatting/_shell.py +11 -9
  131. meerschaum/utils/misc.py +237 -80
  132. meerschaum/utils/packages/__init__.py +3 -6
  133. meerschaum/utils/packages/_packages.py +34 -32
  134. meerschaum/utils/pipes.py +181 -0
  135. meerschaum/utils/process.py +1 -1
  136. meerschaum/utils/prompt.py +3 -1
  137. meerschaum/utils/schedule.py +1 -0
  138. meerschaum/utils/sql.py +114 -37
  139. meerschaum/utils/typing.py +1 -4
  140. meerschaum/utils/venv/_Venv.py +2 -2
  141. meerschaum/utils/venv/__init__.py +5 -7
  142. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/METADATA +88 -80
  143. meerschaum-3.0.0rc1.dist-info/RECORD +282 -0
  144. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/WHEEL +1 -1
  145. meerschaum/api/models/_interfaces.py +0 -15
  146. meerschaum/api/models/_locations.py +0 -15
  147. meerschaum/api/models/_metrics.py +0 -15
  148. meerschaum/config/static/__init__.py +0 -186
  149. meerschaum-2.9.5.dist-info/RECORD +0 -263
  150. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  151. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
  152. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/top_level.txt +0 -0
  153. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/zip-safe +0 -0
@@ -14,7 +14,7 @@ from fnmatch import fnmatch
14
14
  import meerschaum as mrsm
15
15
  from meerschaum.utils.typing import Dict, Any, Optional, PipesDict
16
16
  from meerschaum.config import get_config
17
- from meerschaum.config.static import STATIC_CONFIG, SERVER_ID
17
+ from meerschaum._internal.static import STATIC_CONFIG, SERVER_ID
18
18
  from meerschaum.utils.packages import attempt_import
19
19
  from meerschaum.utils import get_pipes as _get_pipes
20
20
  from meerschaum.config._paths import API_UVICORN_CONFIG_PATH, API_UVICORN_RESOURCES_PATH
@@ -95,10 +95,12 @@ def get_uvicorn_config() -> Dict[str, Any]:
95
95
 
96
96
  debug = get_uvicorn_config().get('debug', False)
97
97
  no_dash = get_uvicorn_config().get('no_dash', False)
98
+ no_webterm = get_uvicorn_config().get('no_webterm', False)
98
99
  no_auth = get_uvicorn_config().get('no_auth', False)
99
100
  private = get_uvicorn_config().get('private', False)
100
101
  production = get_uvicorn_config().get('production', False)
101
102
  _include_dash = (not no_dash)
103
+ _include_webterm = (not no_webterm) and _include_dash
102
104
  docs_enabled = not production or sys_config.get('endpoints', {}).get('docs_in_production', True)
103
105
 
104
106
  default_instance_keys = None
@@ -210,6 +212,11 @@ def get_pipe(
210
212
  if location_key in ('[None]', 'None', 'null'):
211
213
  location_key = None
212
214
  instance_keys = str(get_api_connector(instance_keys))
215
+ if connector_keys == 'mrsm':
216
+ raise fastapi.HTTPException(
217
+ status_code=403,
218
+ detail="Unable to serve any pipes with connector keys `mrsm` over the API.",
219
+ )
213
220
  pipe = mrsm.Pipe(connector_keys, metric_key, location_key, mrsm_instance=instance_keys)
214
221
  if is_pipe_registered(pipe, pipes(instance_keys)):
215
222
  return pipes(instance_keys, refresh=refresh)[connector_keys][metric_key][location_key]
@@ -291,7 +298,7 @@ def __getattr__(name: str):
291
298
  raise AttributeError(f"Could not import '{name}'.")
292
299
 
293
300
  ### Import everything else within the API.
294
- from meerschaum.api._oauth2 import manager
301
+ from meerschaum.api._oauth2 import manager, ScopedAuth
295
302
  import meerschaum.api.routes as routes
296
303
  import meerschaum.api._events
297
304
  import meerschaum.api._websockets
meerschaum/api/_events.py CHANGED
@@ -16,6 +16,8 @@ from meerschaum.api import (
16
16
  get_uvicorn_config,
17
17
  debug,
18
18
  no_dash,
19
+ _include_dash,
20
+ _include_webterm,
19
21
  )
20
22
  from meerschaum.utils.debug import dprint
21
23
  from meerschaum.connectors.poll import retry_connect
@@ -25,7 +27,7 @@ from meerschaum.jobs import (
25
27
  start_check_jobs_thread,
26
28
  stop_check_jobs_thread,
27
29
  )
28
- from meerschaum.config.static import STATIC_CONFIG
30
+ from meerschaum._internal.static import STATIC_CONFIG
29
31
 
30
32
  TEMP_PREFIX: str = STATIC_CONFIG['api']['jobs']['temp_prefix']
31
33
 
@@ -35,8 +37,43 @@ async def startup():
35
37
  """
36
38
  Connect to the instance database and begin monitoring jobs.
37
39
  """
40
+ app.openapi_schema = app.openapi()
41
+
42
+ ### Remove the implicitly added HTTPBearer scheme if it exists.
43
+ if 'BearerAuth' in app.openapi_schema['components']['securitySchemes']:
44
+ del app.openapi_schema['components']['securitySchemes']['BearerAuth']
45
+ elif 'HTTPBearer' in app.openapi_schema['components']['securitySchemes']:
46
+ del app.openapi_schema['components']['securitySchemes']['HTTPBearer']
47
+ if 'LoginManager' in app.openapi_schema['components']['securitySchemes']:
48
+ del app.openapi_schema['components']['securitySchemes']['LoginManager']
49
+
50
+ scopes = STATIC_CONFIG['tokens']['scopes']
51
+ app.openapi_schema['components']['securitySchemes']['OAuth2PasswordBearer'] = {
52
+ 'type': 'oauth2',
53
+ 'flows': {
54
+ 'password': {
55
+ 'tokenUrl': STATIC_CONFIG['api']['endpoints']['login'],
56
+ 'scopes': scopes,
57
+ },
58
+ },
59
+ }
60
+ app.openapi_schema['components']['securitySchemes']['APIKey'] = {
61
+ 'type': 'http',
62
+ 'scheme': 'bearer',
63
+ 'bearerFormat': 'mrsm-key:{client_id}:{client_secret}',
64
+ 'description': 'Authentication using a Meerschaum API Key.',
65
+ }
66
+ app.openapi_schema['security'] = [
67
+ {
68
+ 'OAuth2PasswordBearer': [],
69
+ },
70
+ {
71
+ 'APIKey': [],
72
+ }
73
+ ]
74
+
38
75
  try:
39
- if not no_dash:
76
+ if _include_webterm:
40
77
  from meerschaum.api.dash.webterm import start_webterm
41
78
  start_webterm()
42
79
 
meerschaum/api/_oauth2.py CHANGED
@@ -7,21 +7,34 @@ Define JWT authorization here.
7
7
  """
8
8
 
9
9
  import os
10
- from meerschaum.api import app, endpoints, CHECK_UPDATE
10
+ import base64
11
+ import functools
12
+ import inspect
13
+ from typing import List, Optional, Union
14
+
15
+ from meerschaum.api import endpoints, CHECK_UPDATE, no_auth, debug
16
+ from meerschaum.api._tokens import optional_token, get_token_from_authorization
17
+ from meerschaum._internal.static import STATIC_CONFIG
11
18
  from meerschaum.utils.packages import attempt_import
12
- fastapi = attempt_import('fastapi', lazy=False, check_update=CHECK_UPDATE)
19
+ from meerschaum.core import User, Token
20
+
21
+ fastapi, starlette = attempt_import('fastapi', 'starlette', lazy=False, check_update=CHECK_UPDATE)
13
22
  fastapi_responses = attempt_import('fastapi.responses', lazy=False, check_update=CHECK_UPDATE)
14
23
  fastapi_login = attempt_import('fastapi_login', check_update=CHECK_UPDATE)
24
+ from fastapi import Depends, HTTPException, Request
25
+ from starlette import status
26
+
15
27
 
16
28
  class CustomOAuth2PasswordRequestForm:
17
29
  def __init__(
18
30
  self,
19
31
  grant_type: str = fastapi.Form(None, regex="password|client_credentials"),
20
- username: str = fastapi.Form(...),
21
- password: str = fastapi.Form(...),
22
- scope: str = fastapi.Form(""),
23
- client_id: str = fastapi.Form(None),
24
- client_secret: str = fastapi.Form(None),
32
+ username: Optional[str] = fastapi.Form(None),
33
+ password: Optional[str] = fastapi.Form(None),
34
+ scope: str = fastapi.Form(" ".join(STATIC_CONFIG['tokens']['scopes'])),
35
+ client_id: Optional[str] = fastapi.Form(None),
36
+ client_secret: Optional[str] = fastapi.Form(None),
37
+ authorization: Optional[str] = fastapi.Header(None),
25
38
  ):
26
39
  self.grant_type = grant_type
27
40
  self.username = username
@@ -30,9 +43,106 @@ class CustomOAuth2PasswordRequestForm:
30
43
  self.client_id = client_id
31
44
  self.client_secret = client_secret
32
45
 
46
+ if (
47
+ not username
48
+ and not password
49
+ and not self.client_id
50
+ and not self.client_secret
51
+ and authorization
52
+ ):
53
+ try:
54
+ scheme, credentials = authorization.split()
55
+ if credentials.startswith('mrsm-key:'):
56
+ credentials = credentials[len('mrsm-key:'):]
57
+ if scheme.lower() in ('basic', 'bearer'):
58
+ decoded_credentials = base64.b64decode(credentials).decode('utf-8')
59
+ _client_id, _client_secret = decoded_credentials.split(':', 1)
60
+ self.client_id = _client_id
61
+ self.client_secret = _client_secret
62
+ self.grant_type = 'client_credentials'
63
+ except ValueError:
64
+ pass
65
+
66
+
67
+ async def optional_user(request: Request) -> Optional[User]:
68
+ """
69
+ FastAPI dependency that returns a User if logged in, otherwise None.
70
+ """
71
+ if no_auth:
72
+ return None
73
+ return await manager(request)
74
+ try:
75
+ return await manager(request)
76
+ except HTTPException:
77
+ return None
78
+
79
+
80
+ async def load_user_or_token(
81
+ request: Request,
82
+ users: bool = True,
83
+ tokens: bool = True,
84
+ ) -> Union[User, Token, None]:
85
+ """
86
+ Load the current user or token.
87
+ """
88
+ authorization = request.headers.get('authorization', request.headers.get('Authorization', None))
89
+ if not authorization:
90
+ raise HTTPException(
91
+ status_code=status.HTTP_401_UNAUTHORIZED,
92
+ detail="Not authenticated.",
93
+ )
94
+ authorization = authorization.replace('Basic ', '').replace('Bearer ', '')
95
+ if not authorization.startswith('mrsm-key:'):
96
+ if not users:
97
+ raise HTTPException(
98
+ status=status.HTTP_401_UNAUTHORIZED,
99
+ detail="Users not authenticated for this endpoint.",
100
+ )
101
+ return await manager(request)
102
+ if not tokens:
103
+ raise HTTPException(
104
+ status_code=status.HTTP_401_UNAUTHORIZED,
105
+ detail="Tokens not authenticated for this endpoint.",
106
+ )
107
+ return get_token_from_authorization(authorization)
108
+
109
+
110
+ def ScopedAuth(scopes: List[str]):
111
+ """
112
+ Dependency factory for authenticating with either a user session or a scoped token.
113
+ """
114
+ async def _authenticate(
115
+ user_or_token: Union[User, Token, None] = Depends(
116
+ load_user_or_token,
117
+ ),
118
+ ) -> Union[User, Token, None]:
119
+ if no_auth:
120
+ return None
121
+
122
+ if not user_or_token:
123
+ raise HTTPException(
124
+ status_code=status.HTTP_401_UNAUTHORIZED,
125
+ detail="Not authenticated.",
126
+ headers={"WWW-Authenticate": "Basic"},
127
+ )
128
+
129
+ fresh_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
130
+ if '*' in fresh_scopes:
131
+ return user_or_token
132
+
133
+ for scope in scopes:
134
+ if scope not in fresh_scopes:
135
+ raise HTTPException(
136
+ status_code=status.HTTP_403_FORBIDDEN,
137
+ detail=f"Missing required scope: '{scope}'",
138
+ )
139
+
140
+ return user_or_token
141
+ return _authenticate
142
+
33
143
 
34
144
  LoginManager = fastapi_login.LoginManager
35
- def generate_secret_key() -> str:
145
+ def generate_secret_key() -> bytes:
36
146
  """
37
147
  Read or generate the secret keyfile.
38
148
  """
@@ -0,0 +1,102 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the authentication logic for Meerschaum Tokens.
6
+ """
7
+
8
+ import base64
9
+ import uuid
10
+ from typing import Optional, Union, List
11
+ from datetime import datetime, timezone
12
+
13
+ from fastapi import Depends, HTTPException, Request
14
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
+ from starlette import status
16
+
17
+ import meerschaum as mrsm
18
+ from meerschaum.api import (
19
+ get_api_connector,
20
+ debug,
21
+ )
22
+ from meerschaum.core import Token, User
23
+ from meerschaum.core.User import verify_password
24
+
25
+
26
+ http_bearer = HTTPBearer(auto_error=False, scheme_name="APIKey")
27
+
28
+
29
+ def get_token_from_authorization(authorization: str) -> Token:
30
+ """
31
+ Helper function to decode and verify a token from credentials.
32
+ Raises HTTPException on failure.
33
+ """
34
+ if authorization.startswith('mrsm-key:'):
35
+ authorization = authorization[len('mrsm-key:'):]
36
+ try:
37
+ credential_string = base64.b64decode(authorization).decode('utf-8')
38
+ token_id_str, secret = credential_string.split(':', 1)
39
+ token_id = uuid.UUID(token_id_str)
40
+ except Exception:
41
+ raise HTTPException(
42
+ status_code=status.HTTP_401_UNAUTHORIZED,
43
+ detail="Invalid token format. Expected Base64-encoded 'token_id:secret'.",
44
+ headers={"WWW-Authenticate": "Bearer"},
45
+ )
46
+
47
+ conn = get_api_connector()
48
+ token = conn.get_token(token_id)
49
+
50
+ if not token or not token.is_valid:
51
+ raise HTTPException(
52
+ status_code=status.HTTP_401_UNAUTHORIZED,
53
+ detail="Invalid or revoked token.",
54
+ headers={"WWW-Authenticate": "Bearer"},
55
+ )
56
+
57
+ if token.get_expiration_status(debug=debug):
58
+ raise HTTPException(
59
+ status_code=status.HTTP_401_UNAUTHORIZED,
60
+ detail="Token has expired.",
61
+ headers={"WWW-Authenticate": "Bearer"},
62
+ )
63
+
64
+ if not verify_password(secret, token.secret_hash):
65
+ raise HTTPException(
66
+ status_code=status.HTTP_401_UNAUTHORIZED,
67
+ detail="Invalid secret.",
68
+ headers={"WWW-Authenticate": "Bearer"},
69
+ )
70
+
71
+ return token
72
+
73
+
74
+ def get_current_token(
75
+ auth_creds: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer),
76
+ ) -> Token:
77
+ """
78
+ FastAPI dependency to authenticate a request with a Meerschaum Token.
79
+ This dependency will fail if no token is provided.
80
+ """
81
+ if auth_creds is None:
82
+ raise HTTPException(
83
+ status_code=status.HTTP_401_UNAUTHORIZED,
84
+ detail="Not authenticated.",
85
+ headers={"WWW-Authenticate": "Bearer"},
86
+ )
87
+ return get_token_from_authorization(auth_creds.credentials)
88
+
89
+
90
+ async def optional_token(
91
+ auth_creds: Optional[HTTPAuthorizationCredentials] = Depends(http_bearer),
92
+ ) -> Optional[Token]:
93
+ """
94
+ FastAPI dependency that returns a Token if provided, otherwise None.
95
+ """
96
+ if not auth_creds:
97
+ return None
98
+
99
+ try:
100
+ return get_token_from_authorization(auth_creds.credentials)
101
+ except HTTPException as e:
102
+ return None
@@ -19,7 +19,6 @@ _monkey_patch_get_distribution('flask-compress', flask_compress.__version__)
19
19
  dash = attempt_import('dash', lazy=False)
20
20
 
21
21
  from meerschaum.utils.typing import List, Optional
22
- from meerschaum.config.static import _static_config
23
22
  from meerschaum.api import (
24
23
  app as fastapi_app,
25
24
  debug,
@@ -36,9 +36,9 @@ def add_plugin_pages(debug: bool = False):
36
36
  """
37
37
  Allow users to add pages via the `@web_page` decorator.
38
38
  """
39
- for plugin_name, pages_dicts in _plugin_endpoints_to_pages.items():
39
+ for page_group, pages_dicts in _plugin_endpoints_to_pages.items():
40
40
  if debug:
41
- dprint(f"Adding pages from plugin '{plugin_name}'...")
41
+ dprint(f"Adding pages for group '{page_group}'...")
42
42
  for _endpoint, _page_dict in pages_dicts.items():
43
43
  page_layout = _page_dict['function']()
44
44
  if not _page_dict['skip_navbar']:
@@ -11,10 +11,11 @@ from __future__ import annotations
11
11
  import textwrap
12
12
  import json
13
13
  import uuid
14
- from datetime import datetime, timezone
15
14
 
16
15
  from dash.dependencies import Input, Output, State, ALL, MATCH
17
16
  from dash.exceptions import PreventUpdate
17
+
18
+ import meerschaum as mrsm
18
19
  from meerschaum.utils.typing import List, Optional, Any, Tuple
19
20
  from meerschaum.api import get_api_connector, endpoints, no_auth, CHECK_UPDATE
20
21
  from meerschaum.api.dash import dash_app, debug
@@ -26,7 +27,12 @@ from meerschaum.api.dash.sessions import (
26
27
  from meerschaum.api.dash.sessions import is_session_authenticated
27
28
  from meerschaum.api.dash.connectors import get_web_connector
28
29
  from meerschaum.connectors.parse import parse_instance_keys
29
- from meerschaum.api.dash.pipes import get_pipes_cards, pipe_from_ctx, accordion_items_from_pipe
30
+ from meerschaum.api.dash.pipes import (
31
+ get_pipes_cards,
32
+ pipe_from_ctx,
33
+ accordion_items_from_pipe,
34
+ get_backtrack_text,
35
+ )
30
36
  from meerschaum.api.dash.jobs import get_jobs_cards
31
37
  from meerschaum.api.dash.plugins import get_plugins_cards
32
38
  from meerschaum.api.dash.users import get_users_cards
@@ -41,7 +47,7 @@ from meerschaum.api.dash.components import (
41
47
  from meerschaum.api.dash import pages
42
48
  from meerschaum.utils.typing import Dict
43
49
  from meerschaum.utils.packages import attempt_import, import_html, import_dcc
44
- from meerschaum.utils.misc import filter_keywords, flatten_list
50
+ from meerschaum.utils.misc import filter_keywords, flatten_list, string_to_dict
45
51
  from meerschaum.utils.yaml import yaml
46
52
  from meerschaum.actions import get_subactions, actions
47
53
  from meerschaum._internal.arguments._parser import parser
@@ -55,11 +61,8 @@ keys_state = (
55
61
  State('connector-keys-dropdown', 'value'),
56
62
  State('metric-keys-dropdown', 'value'),
57
63
  State('location-keys-dropdown', 'value'),
58
- State('connector-keys-input', 'value'),
59
- State('metric-keys-input', 'value'),
60
- State('location-keys-input', 'value'),
61
- State('search-parameters-editor', 'value'),
62
- State('pipes-filter-tabs', 'active_tab'),
64
+ State('tags-dropdown', 'value'),
65
+ State('tags-dropdown-div', 'style'),
63
66
  State('action-dropdown', 'value'),
64
67
  State('subaction-dropdown', 'value'),
65
68
  State('subaction-dropdown', 'options'),
@@ -295,6 +298,7 @@ dash_app.clientside_callback(
295
298
  connector_keys,
296
299
  metric_keys,
297
300
  location_keys,
301
+ tags,
298
302
  flags,
299
303
  input_flags,
300
304
  input_flags_texts,
@@ -317,6 +321,7 @@ dash_app.clientside_callback(
317
321
  connector_keys: connector_keys,
318
322
  metric_keys: metric_keys,
319
323
  location_keys: location_keys,
324
+ tags: tags,
320
325
  flags: flags,
321
326
  input_flags: input_flags,
322
327
  input_flags_texts: input_flags_texts,
@@ -333,6 +338,7 @@ dash_app.clientside_callback(
333
338
  State('connector-keys-dropdown', 'value'),
334
339
  State('metric-keys-dropdown', 'value'),
335
340
  State('location-keys-dropdown', 'value'),
341
+ State('tags-dropdown', 'value'),
336
342
  State('flags-dropdown', 'value'),
337
343
  State({'type': 'input-flags-dropdown', 'index': ALL}, 'value'),
338
344
  State({'type': 'input-flags-dropdown-text', 'index': ALL}, 'value'),
@@ -358,7 +364,7 @@ def update_actions(action: str, subaction: str):
358
364
  _actions_options = sorted([
359
365
  {
360
366
  'label': a.replace('_', ' '),
361
- 'value': a,
367
+ 'value': a.replace('_', ' '),
362
368
  'title': (textwrap.dedent(f.__doc__).lstrip() if f.__doc__ else 'No help available.'),
363
369
  }
364
370
  for a, f in actions.items() if a not in omit_actions
@@ -366,7 +372,7 @@ def update_actions(action: str, subaction: str):
366
372
  _subactions_options = sorted([
367
373
  {
368
374
  'label': sa.replace('_', ' '),
369
- 'value': sa,
375
+ 'value': sa.replace('_', ' '),
370
376
  'title': (textwrap.dedent(f.__doc__).lstrip() if f.__doc__ else 'No help available.'),
371
377
  }
372
378
  for sa, f in get_subactions(action).items()
@@ -497,11 +503,15 @@ def update_flags(input_flags_dropdown_values, n_clicks, input_flags_texts):
497
503
  Output('location-keys-dropdown', 'options'),
498
504
  Output('location-keys-list', 'children'),
499
505
  Output('location-keys-dropdown', 'value'),
506
+ Output('tags-dropdown', 'options'),
507
+ Output('tags-list', 'children'),
508
+ Output('tags-dropdown', 'value'),
500
509
  Output('instance-select', 'value'),
501
510
  Output('instance-alert-div', 'children'),
502
511
  Input('connector-keys-dropdown', 'value'),
503
512
  Input('metric-keys-dropdown', 'value'),
504
513
  Input('location-keys-dropdown', 'value'),
514
+ Input('tags-dropdown', 'value'),
505
515
  Input('instance-select', 'value'),
506
516
  *keys_state ### NOTE: Necessary for `ctx.states`.
507
517
  )
@@ -509,6 +519,7 @@ def update_keys_options(
509
519
  connector_keys: Optional[List[str]],
510
520
  metric_keys: Optional[List[str]],
511
521
  location_keys: Optional[List[str]],
522
+ tags: Optional[List[str]],
512
523
  instance_keys: Optional[str],
513
524
  *keys
514
525
  ):
@@ -543,6 +554,8 @@ def update_keys_options(
543
554
  metric_keys = []
544
555
  if location_keys is None:
545
556
  location_keys = []
557
+ if tags is None:
558
+ tags = []
546
559
  num_filter = 0
547
560
  if connector_keys:
548
561
  num_filter += 1
@@ -550,13 +563,17 @@ def update_keys_options(
550
563
  num_filter += 1
551
564
  if location_keys:
552
565
  num_filter += 1
566
+ if tags:
567
+ num_filter += 1
553
568
 
554
569
  _ck_filter = connector_keys
555
570
  _mk_filter = metric_keys
556
571
  _lk_filter = location_keys
572
+ _tags_filter = tags
557
573
  _ck_alone = (connector_keys and num_filter == 1) or instance_click
558
574
  _mk_alone = (metric_keys and num_filter == 1) or instance_click
559
575
  _lk_alone = (location_keys and num_filter == 1) or instance_click
576
+ _tags_alone = (tags and num_filter == 1) or instance_click
560
577
 
561
578
  from meerschaum.utils import fetch_pipes_keys
562
579
 
@@ -568,15 +585,31 @@ def update_keys_options(
568
585
  connector_keys=_ck_filter,
569
586
  metric_keys=_mk_filter,
570
587
  location_keys=_lk_filter,
588
+ tags=_tags_filter,
589
+ )
590
+ _tags_pipes = mrsm.get_pipes(
591
+ connector_keys=_ck_filter,
592
+ metric_keys=_mk_filter,
593
+ location_keys=_lk_filter,
594
+ tags=_tags_filter,
595
+ instance=get_web_connector(ctx.states),
596
+ as_tags_dict=True,
571
597
  )
598
+ _all_tags = list(
599
+ mrsm.get_pipes(
600
+ instance=get_web_connector(ctx.states),
601
+ as_tags_dict=True,
602
+ )
603
+ ) if _tags_alone else []
572
604
  except Exception as e:
573
605
  instance_alerts += [alert_from_success_tuple((False, str(e)))]
574
606
  _all_keys, _keys = [], []
575
607
  _connectors_options = []
576
608
  _metrics_options = []
577
609
  _locations_options = []
610
+ _tags_options = []
578
611
 
579
- _seen_keys = {'ck' : set(), 'mk' : set(), 'lk' : set()}
612
+ _seen_keys = {'ck' : set(), 'mk' : set(), 'lk' : set(), 'tags': set()}
580
613
 
581
614
  def add_options(options, keys, key_type):
582
615
  for ck, mk, lk in keys:
@@ -589,9 +622,16 @@ def update_keys_options(
589
622
  add_options(_connectors_options, _all_keys if _ck_alone else _keys, 'ck')
590
623
  add_options(_metrics_options, _all_keys if _mk_alone else _keys, 'mk')
591
624
  add_options(_locations_options, _all_keys if _lk_alone else _keys, 'lk')
625
+
626
+ _tags_options = [
627
+ {'label': tag, 'value': tag}
628
+ for tag in (_all_tags if _tags_alone else _tags_pipes)
629
+ ]
630
+
592
631
  _connectors_options.sort(key=lambda x: str(x).lower())
593
632
  _metrics_options.sort(key=lambda x: str(x).lower())
594
633
  _locations_options.sort(key=lambda x: str(x).lower())
634
+ _tags_options.sort(key=lambda x: str(x).lower())
595
635
  connector_keys = [
596
636
  ck
597
637
  for ck in connector_keys
@@ -616,9 +656,18 @@ def update_keys_options(
616
656
  for _lk in _locations_options
617
657
  ]
618
658
  ]
659
+ tags = [
660
+ tag
661
+ for tag in tags
662
+ if tag in [
663
+ _tag['value']
664
+ for _tag in _tags_options
665
+ ]
666
+ ]
619
667
  _connectors_datalist = [html.Option(value=o['value']) for o in _connectors_options]
620
668
  _metrics_datalist = [html.Option(value=o['value']) for o in _metrics_options]
621
669
  _locations_datalist = [html.Option(value=o['value']) for o in _locations_options]
670
+ _tags_datalist = [html.Option(value=o['value']) for o in _tags_options]
622
671
  return (
623
672
  _connectors_options,
624
673
  _connectors_datalist,
@@ -629,6 +678,9 @@ def update_keys_options(
629
678
  _locations_options,
630
679
  _locations_datalist,
631
680
  location_keys,
681
+ _tags_options,
682
+ _tags_datalist,
683
+ tags,
632
684
  (instance_keys if update_instance_keys else dash.no_update),
633
685
  instance_alerts,
634
686
  )
@@ -861,7 +913,6 @@ def update_pipe_accordion(item, session_store_data):
861
913
  def update_pipe_parameters_click(n_clicks, parameters_editor_text):
862
914
  if not n_clicks:
863
915
  raise PreventUpdate
864
- ctx = dash.callback_context
865
916
  triggered = dash.callback_context.triggered
866
917
  if triggered[0]['value'] is None:
867
918
  raise PreventUpdate
@@ -890,12 +941,11 @@ def update_pipe_parameters_click(n_clicks, parameters_editor_text):
890
941
  @dash_app.callback(
891
942
  Output({'type': 'update-sql-success-div', 'index': MATCH}, 'children'),
892
943
  Input({'type': 'update-sql-button', 'index': MATCH}, 'n_clicks'),
893
- State({'type': 'sql-editor', 'index': MATCH}, 'value')
944
+ State({'type': 'sql-editor', 'index': MATCH}, 'value'),
894
945
  )
895
946
  def update_pipe_sql_click(n_clicks, sql_editor_text):
896
947
  if not n_clicks:
897
948
  raise PreventUpdate
898
- ctx = dash.callback_context
899
949
  triggered = dash.callback_context.triggered
900
950
  if triggered[0]['value'] is None:
901
951
  raise PreventUpdate
@@ -923,7 +973,6 @@ def update_pipe_sql_click(n_clicks, sql_editor_text):
923
973
  def sync_documents_click(n_clicks, sync_editor_text):
924
974
  if not n_clicks:
925
975
  raise PreventUpdate
926
- ctx = dash.callback_context
927
976
  triggered = dash.callback_context.triggered
928
977
  if triggered[0]['value'] is None:
929
978
  raise PreventUpdate
@@ -937,6 +986,15 @@ def sync_documents_click(n_clicks, sync_editor_text):
937
986
  except Exception as e:
938
987
  docs = None
939
988
  msg = str(e)
989
+
990
+ if docs is None:
991
+ try:
992
+ lines = sync_editor_text.splitlines()
993
+ docs = [string_to_dict(line) for line in lines]
994
+ msg = '... '
995
+ except Exception:
996
+ docs = None
997
+
940
998
  if docs is None:
941
999
  success, msg = False, (msg + f"Unable to sync documents to {pipe}.")
942
1000
  else:
@@ -1043,8 +1101,8 @@ dash_app.clientside_callback(
1043
1101
 
1044
1102
  @dash_app.callback(
1045
1103
  Output("navbar-collapse", "is_open"),
1046
- [Input("navbar-toggler", "n_clicks")],
1047
- [State("navbar-collapse", "is_open")],
1104
+ Input("navbar-toggler", "n_clicks"),
1105
+ State("navbar-collapse", "is_open"),
1048
1106
  )
1049
1107
  def toggle_navbar_collapse(n_clicks: Optional[int], is_open: bool) -> bool:
1050
1108
  """
@@ -1103,7 +1161,6 @@ def parameters_as_yaml_or_json_click(
1103
1161
  if not yaml_n_clicks and not json_n_clicks:
1104
1162
  raise PreventUpdate
1105
1163
 
1106
- ctx = dash.callback_context
1107
1164
  triggered = dash.callback_context.triggered
1108
1165
  if triggered[0]['value'] is None:
1109
1166
  raise PreventUpdate
@@ -1117,6 +1174,33 @@ def parameters_as_yaml_or_json_click(
1117
1174
  return json.dumps(pipe.parameters, indent=4, separators=(',', ': '), sort_keys=True)
1118
1175
 
1119
1176
 
1177
+ @dash_app.callback(
1178
+ Output({'type': 'sync-editor', 'index': MATCH}, 'value'),
1179
+ Input({'type': 'sync-as-json-button', 'index': MATCH}, 'n_clicks'),
1180
+ Input({'type': 'sync-as-lines-button', 'index': MATCH}, 'n_clicks'),
1181
+ )
1182
+ def sync_as_json_or_lines_click(
1183
+ json_n_clicks: Optional[int],
1184
+ lines_n_clicks: Optional[int],
1185
+ ):
1186
+ """
1187
+ When the `YAML` button is clicked under the parameters editor, switch the content to YAML.
1188
+ """
1189
+ if not json_n_clicks and not lines_n_clicks:
1190
+ raise PreventUpdate
1191
+
1192
+ triggered = dash.callback_context.triggered
1193
+ if triggered[0]['value'] is None:
1194
+ raise PreventUpdate
1195
+
1196
+ as_lines = 'lines' in triggered[0]['prop_id']
1197
+ pipe = pipe_from_ctx(triggered, 'n_clicks')
1198
+ if pipe is None:
1199
+ raise PreventUpdate
1200
+
1201
+ return get_backtrack_text(pipe, lines=as_lines)
1202
+
1203
+
1120
1204
  @dash_app.callback(
1121
1205
  Output('pages-offcanvas', 'is_open'),
1122
1206
  Output('pages-offcanvas', 'children'),