meerschaum 2.9.5__py3-none-any.whl → 3.0.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.
- meerschaum/__init__.py +5 -2
- meerschaum/_internal/__init__.py +1 -0
- meerschaum/_internal/arguments/_parse_arguments.py +4 -4
- meerschaum/_internal/arguments/_parser.py +33 -4
- meerschaum/_internal/cli/__init__.py +6 -0
- meerschaum/_internal/cli/daemons.py +103 -0
- meerschaum/_internal/cli/entry.py +220 -0
- meerschaum/_internal/cli/workers.py +435 -0
- meerschaum/_internal/docs/index.py +48 -2
- meerschaum/_internal/entry.py +50 -14
- meerschaum/_internal/shell/Shell.py +121 -29
- meerschaum/_internal/shell/__init__.py +4 -1
- meerschaum/_internal/static.py +359 -0
- meerschaum/_internal/term/TermPageHandler.py +1 -2
- meerschaum/_internal/term/__init__.py +40 -6
- meerschaum/_internal/term/tools.py +33 -8
- meerschaum/actions/__init__.py +6 -4
- meerschaum/actions/api.py +53 -13
- meerschaum/actions/attach.py +1 -0
- meerschaum/actions/bootstrap.py +8 -8
- meerschaum/actions/delete.py +4 -2
- meerschaum/actions/edit.py +171 -25
- meerschaum/actions/login.py +8 -8
- meerschaum/actions/register.py +143 -6
- meerschaum/actions/reload.py +22 -5
- meerschaum/actions/restart.py +14 -0
- meerschaum/actions/show.py +184 -31
- meerschaum/actions/start.py +166 -17
- meerschaum/actions/stop.py +38 -2
- meerschaum/actions/sync.py +7 -2
- meerschaum/actions/tag.py +9 -8
- meerschaum/actions/verify.py +5 -8
- meerschaum/api/__init__.py +45 -15
- meerschaum/api/_events.py +46 -4
- meerschaum/api/_oauth2.py +162 -9
- meerschaum/api/_tokens.py +102 -0
- meerschaum/api/dash/__init__.py +0 -3
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/custom.py +4 -3
- meerschaum/api/dash/callbacks/dashboard.py +198 -118
- meerschaum/api/dash/callbacks/jobs.py +14 -7
- meerschaum/api/dash/callbacks/login.py +10 -1
- meerschaum/api/dash/callbacks/pipes.py +194 -14
- meerschaum/api/dash/callbacks/plugins.py +0 -1
- meerschaum/api/dash/callbacks/register.py +10 -3
- meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
- meerschaum/api/dash/callbacks/tokens.py +389 -0
- meerschaum/api/dash/components.py +36 -15
- meerschaum/api/dash/jobs.py +1 -1
- meerschaum/api/dash/keys.py +35 -93
- meerschaum/api/dash/pages/__init__.py +2 -1
- meerschaum/api/dash/pages/dashboard.py +1 -20
- meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
- meerschaum/api/dash/pages/login.py +2 -2
- meerschaum/api/dash/pages/pipes.py +16 -5
- meerschaum/api/dash/pages/settings/password_reset.py +1 -1
- meerschaum/api/dash/pages/tokens.py +53 -0
- meerschaum/api/dash/pipes.py +382 -95
- meerschaum/api/dash/sessions.py +12 -0
- meerschaum/api/dash/tokens.py +603 -0
- meerschaum/api/dash/websockets.py +1 -1
- meerschaum/api/dash/webterm.py +18 -6
- meerschaum/api/models/__init__.py +23 -3
- meerschaum/api/models/_actions.py +22 -0
- meerschaum/api/models/_pipes.py +91 -7
- meerschaum/api/models/_tokens.py +81 -0
- meerschaum/api/resources/static/js/terminado.js +3 -0
- meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
- meerschaum/api/resources/templates/termpage.html +13 -0
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +3 -4
- meerschaum/api/routes/_connectors.py +3 -7
- meerschaum/api/routes/_jobs.py +26 -35
- meerschaum/api/routes/_login.py +120 -15
- meerschaum/api/routes/_misc.py +5 -10
- meerschaum/api/routes/_pipes.py +178 -143
- meerschaum/api/routes/_plugins.py +38 -28
- meerschaum/api/routes/_tokens.py +236 -0
- meerschaum/api/routes/_users.py +47 -35
- meerschaum/api/routes/_version.py +3 -3
- meerschaum/api/routes/_webterm.py +3 -3
- meerschaum/config/__init__.py +100 -30
- meerschaum/config/_default.py +132 -64
- meerschaum/config/_edit.py +38 -32
- meerschaum/config/_formatting.py +2 -0
- meerschaum/config/_patch.py +10 -8
- meerschaum/config/_paths.py +133 -13
- meerschaum/config/_read_config.py +87 -36
- meerschaum/config/_sync.py +6 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/environment.py +262 -0
- meerschaum/config/stack/__init__.py +37 -15
- meerschaum/config/static.py +18 -0
- meerschaum/connectors/_Connector.py +11 -6
- meerschaum/connectors/__init__.py +41 -22
- meerschaum/connectors/api/_APIConnector.py +34 -6
- meerschaum/connectors/api/_actions.py +2 -2
- meerschaum/connectors/api/_jobs.py +12 -1
- meerschaum/connectors/api/_login.py +33 -7
- meerschaum/connectors/api/_misc.py +2 -2
- meerschaum/connectors/api/_pipes.py +23 -32
- meerschaum/connectors/api/_plugins.py +2 -2
- meerschaum/connectors/api/_request.py +1 -1
- meerschaum/connectors/api/_tokens.py +146 -0
- meerschaum/connectors/api/_users.py +70 -58
- meerschaum/connectors/instance/_InstanceConnector.py +83 -0
- meerschaum/connectors/instance/__init__.py +10 -0
- meerschaum/connectors/instance/_pipes.py +442 -0
- meerschaum/connectors/instance/_plugins.py +159 -0
- meerschaum/connectors/instance/_tokens.py +317 -0
- meerschaum/connectors/instance/_users.py +188 -0
- meerschaum/connectors/parse.py +5 -2
- meerschaum/connectors/sql/_SQLConnector.py +22 -5
- meerschaum/connectors/sql/_cli.py +12 -11
- meerschaum/connectors/sql/_create_engine.py +12 -168
- meerschaum/connectors/sql/_fetch.py +2 -18
- meerschaum/connectors/sql/_pipes.py +295 -278
- meerschaum/connectors/sql/_plugins.py +29 -0
- meerschaum/connectors/sql/_sql.py +46 -21
- meerschaum/connectors/sql/_users.py +36 -2
- meerschaum/connectors/sql/tables/__init__.py +254 -122
- meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
- meerschaum/connectors/valkey/_pipes.py +60 -31
- meerschaum/connectors/valkey/_plugins.py +2 -26
- meerschaum/core/Pipe/__init__.py +115 -85
- meerschaum/core/Pipe/_attributes.py +425 -124
- meerschaum/core/Pipe/_bootstrap.py +54 -24
- meerschaum/core/Pipe/_cache.py +555 -0
- meerschaum/core/Pipe/_clear.py +0 -11
- meerschaum/core/Pipe/_data.py +96 -68
- meerschaum/core/Pipe/_deduplicate.py +0 -13
- meerschaum/core/Pipe/_delete.py +12 -21
- meerschaum/core/Pipe/_drop.py +11 -23
- meerschaum/core/Pipe/_dtypes.py +49 -19
- meerschaum/core/Pipe/_edit.py +14 -4
- meerschaum/core/Pipe/_fetch.py +1 -1
- meerschaum/core/Pipe/_index.py +8 -14
- meerschaum/core/Pipe/_show.py +5 -5
- meerschaum/core/Pipe/_sync.py +123 -204
- meerschaum/core/Pipe/_verify.py +4 -4
- meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
- meerschaum/core/Plugin/__init__.py +1 -1
- meerschaum/core/Token/_Token.py +220 -0
- meerschaum/core/Token/__init__.py +12 -0
- meerschaum/core/User/_User.py +35 -10
- meerschaum/core/User/__init__.py +9 -1
- meerschaum/core/__init__.py +1 -0
- meerschaum/jobs/_Executor.py +88 -4
- meerschaum/jobs/_Job.py +149 -38
- meerschaum/jobs/__init__.py +3 -2
- meerschaum/jobs/systemd.py +8 -3
- meerschaum/models/__init__.py +35 -0
- meerschaum/models/pipes.py +247 -0
- meerschaum/models/tokens.py +38 -0
- meerschaum/models/users.py +26 -0
- meerschaum/plugins/__init__.py +301 -88
- meerschaum/plugins/bootstrap.py +510 -4
- meerschaum/utils/_get_pipes.py +97 -30
- meerschaum/utils/daemon/Daemon.py +199 -43
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
- meerschaum/utils/daemon/RotatingFile.py +63 -36
- meerschaum/utils/daemon/StdinFile.py +53 -13
- meerschaum/utils/daemon/__init__.py +47 -6
- meerschaum/utils/daemon/_names.py +6 -3
- meerschaum/utils/dataframe.py +479 -81
- meerschaum/utils/debug.py +49 -19
- meerschaum/utils/dtypes/__init__.py +476 -34
- meerschaum/utils/dtypes/sql.py +369 -29
- meerschaum/utils/formatting/__init__.py +5 -2
- meerschaum/utils/formatting/_jobs.py +1 -1
- meerschaum/utils/formatting/_pipes.py +52 -50
- meerschaum/utils/formatting/_pprint.py +1 -0
- meerschaum/utils/formatting/_shell.py +44 -18
- meerschaum/utils/misc.py +268 -186
- meerschaum/utils/packages/__init__.py +25 -40
- meerschaum/utils/packages/_packages.py +42 -34
- meerschaum/utils/pipes.py +213 -0
- meerschaum/utils/process.py +2 -2
- meerschaum/utils/prompt.py +175 -144
- meerschaum/utils/schedule.py +2 -1
- meerschaum/utils/sql.py +134 -47
- meerschaum/utils/threading.py +42 -0
- meerschaum/utils/typing.py +1 -4
- meerschaum/utils/venv/_Venv.py +2 -2
- meerschaum/utils/venv/__init__.py +7 -7
- meerschaum/utils/warnings.py +19 -13
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
- meerschaum-3.0.0.dist-info/RECORD +289 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
- meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
- meerschaum/api/models/_interfaces.py +0 -15
- meerschaum/api/models/_locations.py +0 -15
- meerschaum/api/models/_metrics.py +0 -15
- meerschaum/config/_environment.py +0 -145
- meerschaum/config/static/__init__.py +0 -186
- meerschaum-2.9.5.dist-info/RECORD +0 -263
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -5,22 +5,35 @@
|
|
5
5
|
Define callbacks for the `/dash/pipes/` page.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from urllib.parse import parse_qs
|
8
|
+
from urllib.parse import parse_qs, quote_plus
|
9
|
+
from typing import List, Optional, Dict, Any
|
9
10
|
|
11
|
+
import dash
|
10
12
|
from dash.dependencies import Input, Output, State
|
11
13
|
from dash import no_update
|
14
|
+
from dash.exceptions import PreventUpdate
|
15
|
+
import dash_bootstrap_components as dbc
|
12
16
|
|
13
17
|
import meerschaum as mrsm
|
14
18
|
from meerschaum.api.dash import dash_app
|
15
|
-
from meerschaum.api.dash.
|
16
|
-
|
19
|
+
from meerschaum.api.dash.components import (
|
20
|
+
alert_from_success_tuple,
|
21
|
+
build_cards_grid,
|
22
|
+
)
|
23
|
+
from meerschaum.api.dash.pipes import (
|
24
|
+
build_pipe_card,
|
25
|
+
build_pipes_dropdown_keys_row,
|
26
|
+
build_pipes_tags_dropdown,
|
27
|
+
build_pipes_navbar,
|
28
|
+
)
|
29
|
+
from meerschaum.api import CHECK_UPDATE, get_api_connector
|
17
30
|
from meerschaum.utils.packages import import_html, import_dcc
|
18
31
|
from meerschaum.api.dash.sessions import is_session_authenticated
|
19
|
-
from meerschaum.utils.typing import Optional, Dict, Any
|
20
32
|
html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
|
21
33
|
|
22
34
|
|
23
35
|
@dash_app.callback(
|
36
|
+
Output('pipes-navbar-div', 'children'),
|
24
37
|
Output('pipe-output-div', 'children'),
|
25
38
|
Input('pipes-location', 'pathname'),
|
26
39
|
State('pipes-location', 'search'),
|
@@ -32,24 +45,191 @@ def render_pipe_page_from_url(
|
|
32
45
|
session_data: Optional[Dict[str, Any]],
|
33
46
|
):
|
34
47
|
if not str(pathname).startswith('/dash/pipes'):
|
35
|
-
|
48
|
+
raise PreventUpdate
|
36
49
|
|
37
50
|
session_id = (session_data or {}).get('session-id', None)
|
38
51
|
authenticated = is_session_authenticated(str(session_id))
|
52
|
+
query_params = parse_qs(pipe_search.lstrip('?')) if pipe_search else {}
|
53
|
+
instance = query_params.get('instance', [None])[0] or str(get_api_connector())
|
54
|
+
tags = query_params.get('tags', [None])[0] or []
|
55
|
+
if isinstance(tags, str):
|
56
|
+
tags = tags.split(',')
|
57
|
+
|
58
|
+
connector_keys = query_params.get('connector_keys', [None])[0] or []
|
59
|
+
if isinstance(connector_keys, str):
|
60
|
+
connector_keys = connector_keys.split(',')
|
61
|
+
|
62
|
+
metric_keys = query_params.get('metric_keys', [None])[0] or []
|
63
|
+
if isinstance(metric_keys, str):
|
64
|
+
metric_keys = metric_keys.split(',')
|
65
|
+
|
66
|
+
location_keys = query_params.get('location_keys', [None])[0] or []
|
67
|
+
if isinstance(location_keys, str):
|
68
|
+
location_keys = location_keys.split(',')
|
39
69
|
|
40
70
|
keys = pathname.replace('/dash/pipes', '').lstrip('/').rstrip('/').split('/')
|
41
|
-
|
42
|
-
|
71
|
+
instance_connector = mrsm.get_connector(instance)
|
72
|
+
viewing_single_pipe = len(keys) in (2, 3)
|
73
|
+
if instance_connector is None:
|
74
|
+
return (
|
75
|
+
build_pipes_navbar(instance, with_instance_select=(not viewing_single_pipe)),
|
76
|
+
[
|
77
|
+
html.Br(),
|
78
|
+
alert_from_success_tuple((False, f"Invalid instance keys '{instance}'.")),
|
79
|
+
html.Br(),
|
80
|
+
]
|
81
|
+
)
|
82
|
+
|
83
|
+
if not viewing_single_pipe:
|
84
|
+
try:
|
85
|
+
pipes = mrsm.get_pipes(
|
86
|
+
as_list=True,
|
87
|
+
connector_keys=connector_keys,
|
88
|
+
metric_keys=metric_keys,
|
89
|
+
location_keys=location_keys,
|
90
|
+
tags=tags,
|
91
|
+
instance=instance_connector,
|
92
|
+
)
|
93
|
+
except Exception as e:
|
94
|
+
return (
|
95
|
+
build_pipes_navbar(instance, with_instance_select=False),
|
96
|
+
[
|
97
|
+
html.Br(),
|
98
|
+
alert_from_success_tuple(
|
99
|
+
(False, f"Failed to get pipes for instance '{instance}':\n{e}")
|
100
|
+
),
|
101
|
+
html.Br(),
|
102
|
+
dbc.Row(
|
103
|
+
[
|
104
|
+
dbc.Button(
|
105
|
+
"Reload",
|
106
|
+
id='pipes-reload-button',
|
107
|
+
size='lg',
|
108
|
+
href=(
|
109
|
+
"/dash/pipes"
|
110
|
+
if pathname.startswith('/dash/pipes/')
|
111
|
+
else "/dash/pipes/"
|
112
|
+
)
|
113
|
+
),
|
114
|
+
],
|
115
|
+
justify='center',
|
116
|
+
align='center',
|
117
|
+
className='h-50',
|
118
|
+
),
|
119
|
+
]
|
120
|
+
)
|
121
|
+
|
122
|
+
cards = [
|
123
|
+
build_pipe_card(pipe, authenticated=authenticated, include_manage=False)
|
124
|
+
for pipe in pipes
|
125
|
+
]
|
126
|
+
return (
|
127
|
+
build_pipes_navbar(instance, with_instance_select=True),
|
128
|
+
[
|
129
|
+
html.Div([
|
130
|
+
html.Br(),
|
131
|
+
build_pipes_dropdown_keys_row(
|
132
|
+
connector_keys,
|
133
|
+
metric_keys,
|
134
|
+
location_keys,
|
135
|
+
tags,
|
136
|
+
pipes,
|
137
|
+
instance_connector,
|
138
|
+
),
|
139
|
+
html.Br(),
|
140
|
+
build_pipes_tags_dropdown(
|
141
|
+
connector_keys,
|
142
|
+
metric_keys,
|
143
|
+
location_keys,
|
144
|
+
tags,
|
145
|
+
instance,
|
146
|
+
),
|
147
|
+
]),
|
148
|
+
html.Br(),
|
149
|
+
build_cards_grid(cards, 1),
|
150
|
+
html.Br(),
|
151
|
+
]
|
152
|
+
)
|
43
153
|
|
44
154
|
ck = keys[0]
|
45
155
|
mk = keys[1]
|
46
156
|
lk = keys[2] if len(keys) == 3 else None
|
47
|
-
query_params = parse_qs(pipe_search.lstrip('?')) if pipe_search else {}
|
48
|
-
instance = query_params.get('instance', [None])[0]
|
49
157
|
|
50
158
|
pipe = mrsm.Pipe(ck, mk, lk, instance=instance)
|
51
|
-
return
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
159
|
+
return (
|
160
|
+
build_pipes_navbar(instance, with_instance_select=False),
|
161
|
+
[
|
162
|
+
html.Br(),
|
163
|
+
build_pipe_card(pipe, authenticated=authenticated, include_manage=False),
|
164
|
+
html.Br(),
|
165
|
+
]
|
166
|
+
)
|
167
|
+
|
168
|
+
|
169
|
+
@dash_app.callback(
|
170
|
+
Output('pipes-location', 'search'),
|
171
|
+
Input('pipes-connector-keys-dropdown', 'value'),
|
172
|
+
Input('pipes-metric-keys-dropdown', 'value'),
|
173
|
+
Input('pipes-location-keys-dropdown', 'value'),
|
174
|
+
Input('pipes-tags-dropdown', 'value'),
|
175
|
+
Input('instance-select', 'value'),
|
176
|
+
Input('pipes-clear-all-button', 'n_clicks'),
|
177
|
+
)
|
178
|
+
def update_location_on_pipes_filter_change(
|
179
|
+
connector_keys: Optional[List[str]],
|
180
|
+
metric_keys: Optional[List[str]],
|
181
|
+
location_keys: Optional[List[str]],
|
182
|
+
tags: Optional[List[str]],
|
183
|
+
instance_keys: str,
|
184
|
+
clear_all_button_n_clicks: Optional[int],
|
185
|
+
):
|
186
|
+
"""
|
187
|
+
Update the URL parameters when clicking the dropdowns.
|
188
|
+
"""
|
189
|
+
ctx = dash.callback_context.triggered
|
190
|
+
if len(ctx) != 1:
|
191
|
+
raise PreventUpdate
|
192
|
+
|
193
|
+
if not any(
|
194
|
+
(connector_keys or [])
|
195
|
+
+ (metric_keys or [])
|
196
|
+
+ (location_keys or [])
|
197
|
+
+ (tags or [])
|
198
|
+
+ ([instance_keys] if instance_keys else [])
|
199
|
+
):
|
200
|
+
return ''
|
201
|
+
|
202
|
+
if ctx[0].get('prop_id', None) == 'pipes-clear-all-button.n_clicks':
|
203
|
+
connector_keys = []
|
204
|
+
metric_keys = []
|
205
|
+
location_keys = []
|
206
|
+
tags = []
|
207
|
+
|
208
|
+
include_instance_keys = instance_keys and instance_keys != str(get_api_connector())
|
209
|
+
search_str = ""
|
210
|
+
|
211
|
+
if connector_keys:
|
212
|
+
search_str += "connector_keys=" + ','.join((quote_plus(ck) for ck in connector_keys))
|
213
|
+
if metric_keys or location_keys or tags or include_instance_keys:
|
214
|
+
search_str += '&'
|
215
|
+
|
216
|
+
if metric_keys:
|
217
|
+
search_str += "metric_keys=" + ','.join((quote_plus(mk) for mk in metric_keys))
|
218
|
+
if location_keys or tags or include_instance_keys:
|
219
|
+
search_str += '&'
|
220
|
+
|
221
|
+
if location_keys:
|
222
|
+
search_str += "location_keys=" + ','.join((quote_plus(str(lk)) for lk in location_keys))
|
223
|
+
if tags or include_instance_keys:
|
224
|
+
search_str += '&'
|
225
|
+
|
226
|
+
if tags:
|
227
|
+
search_str += "tags=" + ','.join((quote_plus(tag) for tag in tags))
|
228
|
+
if include_instance_keys:
|
229
|
+
search_str += '&'
|
230
|
+
|
231
|
+
if instance_keys:
|
232
|
+
if include_instance_keys:
|
233
|
+
search_str += "instance=" + quote_plus(instance_keys)
|
234
|
+
|
235
|
+
return ('?' + search_str) if search_str else ''
|
@@ -13,8 +13,9 @@ from meerschaum.api.dash.sessions import set_session
|
|
13
13
|
from dash.dependencies import Input, Output, State, ALL, MATCH
|
14
14
|
from dash.exceptions import PreventUpdate
|
15
15
|
from meerschaum.core import User
|
16
|
-
from meerschaum.
|
16
|
+
from meerschaum._internal.static import STATIC_CONFIG
|
17
17
|
from meerschaum.utils.packages import attempt_import
|
18
|
+
from meerschaum.api._oauth2 import CustomOAuth2PasswordRequestForm
|
18
19
|
dash = attempt_import('dash', check_update=CHECK_UPDATE)
|
19
20
|
from fastapi.exceptions import HTTPException
|
20
21
|
|
@@ -97,10 +98,16 @@ def register_button_click(
|
|
97
98
|
form_class += ' is-invalid'
|
98
99
|
return {}, form_class, dash.no_update
|
99
100
|
try:
|
100
|
-
|
101
|
+
form = CustomOAuth2PasswordRequestForm(
|
102
|
+
grant_type='password',
|
103
|
+
username=username,
|
104
|
+
password=password,
|
105
|
+
scope=' '.join(STATIC_CONFIG['tokens']['scopes'])
|
106
|
+
)
|
107
|
+
_ = login(form)
|
101
108
|
session_data = {'session-id': str(uuid.uuid4())}
|
102
109
|
set_session(session_data['session-id'], {'username': username})
|
103
|
-
except HTTPException
|
110
|
+
except HTTPException:
|
104
111
|
form_class += ' is-invalid'
|
105
112
|
session_data = None
|
106
113
|
return session_data, form_class, (dash.no_update if not session_data else endpoints['dash'])
|
@@ -11,7 +11,7 @@ from dash.dependencies import Input, Output, State
|
|
11
11
|
import dash_bootstrap_components as dbc
|
12
12
|
|
13
13
|
from meerschaum.core.User import User
|
14
|
-
from meerschaum.
|
14
|
+
from meerschaum._internal.static import STATIC_CONFIG
|
15
15
|
from meerschaum.api import get_api_connector, debug
|
16
16
|
from meerschaum.api.dash import dash_app
|
17
17
|
from meerschaum.api.dash.components import alert_from_success_tuple
|
@@ -39,7 +39,7 @@ def password_reset_button_click(n_clicks, new_password_value, session_store_data
|
|
39
39
|
)
|
40
40
|
|
41
41
|
instance_connector = get_api_connector()
|
42
|
-
user = User(username, new_password_value)
|
42
|
+
user = User(username, new_password_value, instance=instance_connector)
|
43
43
|
success, msg = instance_connector.edit_user(user, debug=debug)
|
44
44
|
return alert_from_success_tuple((success, msg))
|
45
45
|
|
@@ -0,0 +1,389 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# vim:fenc=utf-8
|
3
|
+
|
4
|
+
"""
|
5
|
+
Define the callbacks for the tokens page.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
from typing import Optional, List, Dict, Any, Tuple
|
10
|
+
from datetime import datetime, timezone
|
11
|
+
|
12
|
+
import dash
|
13
|
+
from dash.dependencies import Input, Output, State, MATCH, ALL
|
14
|
+
from dash.exceptions import PreventUpdate
|
15
|
+
import dash_bootstrap_components as dbc
|
16
|
+
import dash.html as html
|
17
|
+
import dash.dcc as dcc
|
18
|
+
|
19
|
+
import meerschaum as mrsm
|
20
|
+
from meerschaum.api import get_api_connector, debug
|
21
|
+
from meerschaum.api.dash import dash_app
|
22
|
+
from meerschaum.api.dash.sessions import get_user_from_session
|
23
|
+
from meerschaum.api.dash.components import alert_from_success_tuple, build_cards_grid
|
24
|
+
from meerschaum.api.dash.tokens import (
|
25
|
+
get_tokens_cards,
|
26
|
+
get_tokens_table,
|
27
|
+
build_tokens_register_input_modal,
|
28
|
+
build_tokens_register_output_modal,
|
29
|
+
)
|
30
|
+
from meerschaum._internal.static import STATIC_CONFIG
|
31
|
+
from meerschaum.utils.daemon import get_new_daemon_name
|
32
|
+
from meerschaum.core import Token
|
33
|
+
|
34
|
+
|
35
|
+
@dash_app.callback(
|
36
|
+
Output('tokens-output-div', 'children'),
|
37
|
+
Output('tokens-register-input-modal', 'children'),
|
38
|
+
Output('tokens-alert-div', 'children'),
|
39
|
+
Input('tokens-refresh-button', 'n_clicks'),
|
40
|
+
State('session-store', 'data'),
|
41
|
+
)
|
42
|
+
def refresh_tokens_button_click(
|
43
|
+
n_clicks: Optional[int],
|
44
|
+
session_data: Optional[Dict[str, Any]] = None,
|
45
|
+
):
|
46
|
+
"""
|
47
|
+
Build the tokens cards on load or refresh.
|
48
|
+
"""
|
49
|
+
session_id = (session_data or {}).get('session-id', None)
|
50
|
+
tokens_table, alerts = get_tokens_table(session_id)
|
51
|
+
if not tokens_table:
|
52
|
+
return (
|
53
|
+
[
|
54
|
+
html.H4('No tokens registered.'),
|
55
|
+
html.P('Click the `+` button to register a new token.'),
|
56
|
+
],
|
57
|
+
build_tokens_register_input_modal(),
|
58
|
+
alerts,
|
59
|
+
)
|
60
|
+
|
61
|
+
return tokens_table, build_tokens_register_input_modal(), alerts
|
62
|
+
|
63
|
+
|
64
|
+
@dash_app.callback(
|
65
|
+
Output('tokens-register-input-modal', 'is_open'),
|
66
|
+
Input('tokens-create-button', 'n_clicks'),
|
67
|
+
prevent_initial_call=True,
|
68
|
+
)
|
69
|
+
def create_tokens_button_click(n_clicks: Optional[int]):
|
70
|
+
"""
|
71
|
+
Open the tokens registration modal when the plus button is clicked.
|
72
|
+
"""
|
73
|
+
if not n_clicks:
|
74
|
+
raise PreventUpdate
|
75
|
+
|
76
|
+
return True
|
77
|
+
|
78
|
+
|
79
|
+
@dash_app.callback(
|
80
|
+
Output("tokens-scopes-checklist-div", 'style'),
|
81
|
+
Input("tokens-toggle-scopes-switch", 'value'),
|
82
|
+
prevent_initial_call=True,
|
83
|
+
)
|
84
|
+
def toggle_token_scopes_checklist(value: bool):
|
85
|
+
"""
|
86
|
+
Toggle the scopes checklist.
|
87
|
+
"""
|
88
|
+
return {'display': 'none'} if value else {}
|
89
|
+
|
90
|
+
|
91
|
+
@dash_app.callback(
|
92
|
+
Output('tokens-scopes-checklist', 'value'),
|
93
|
+
Output('tokens-deselect-scopes-button', 'children'),
|
94
|
+
Input('tokens-deselect-scopes-button', 'n_clicks'),
|
95
|
+
State('tokens-deselect-scopes-button', 'children'),
|
96
|
+
prevent_initial_call=True,
|
97
|
+
)
|
98
|
+
def deselect_scopes_click(n_clicks: Optional[int], name: str):
|
99
|
+
"""
|
100
|
+
Set the value of the scopes checklist to an empty list.
|
101
|
+
"""
|
102
|
+
if not n_clicks:
|
103
|
+
raise PreventUpdate
|
104
|
+
|
105
|
+
new_name = 'Select all' if name == 'Deselect all' else 'Deselect all'
|
106
|
+
value = (
|
107
|
+
[]
|
108
|
+
if name == 'Deselect all'
|
109
|
+
else list(STATIC_CONFIG['tokens']['scopes'])
|
110
|
+
)
|
111
|
+
|
112
|
+
return value, new_name
|
113
|
+
|
114
|
+
|
115
|
+
@dash_app.callback(
|
116
|
+
Output({'type': 'tokens-scopes-checklist', 'index': MATCH}, 'value'),
|
117
|
+
Output({'type': 'tokens-deselect-scopes-button', 'index': MATCH}, 'children'),
|
118
|
+
Input({'type': 'tokens-deselect-scopes-button', 'index': MATCH}, 'n_clicks'),
|
119
|
+
State({'type': 'tokens-deselect-scopes-button', 'index': MATCH}, 'children'),
|
120
|
+
prevent_initial_call=True,
|
121
|
+
)
|
122
|
+
def edit_token_deselect_scopes_click(n_clicks: Optional[int], name: str):
|
123
|
+
"""
|
124
|
+
Set the value of the scopes checklist to an empty list.
|
125
|
+
"""
|
126
|
+
if not n_clicks:
|
127
|
+
raise PreventUpdate
|
128
|
+
|
129
|
+
new_name = 'Select all' if name == 'Deselect all' else 'Deselect all'
|
130
|
+
value = (
|
131
|
+
[]
|
132
|
+
if name == 'Deselect all'
|
133
|
+
else list(STATIC_CONFIG['tokens']['scopes'])
|
134
|
+
)
|
135
|
+
|
136
|
+
return value, new_name
|
137
|
+
|
138
|
+
|
139
|
+
@dash_app.callback(
|
140
|
+
Output('tokens-register-input-modal', 'is_open'),
|
141
|
+
Output('tokens-register-output-modal', 'is_open'),
|
142
|
+
Output('tokens-register-output-modal', 'children'),
|
143
|
+
Input('tokens-register-button', 'n_clicks'),
|
144
|
+
State('tokens-name-input', 'value'),
|
145
|
+
State('tokens-scopes-checklist', 'value'),
|
146
|
+
State('tokens-expiration-datepickersingle', 'date'),
|
147
|
+
State('session-store', 'data'),
|
148
|
+
prevent_initial_call=True,
|
149
|
+
)
|
150
|
+
def register_token_click(
|
151
|
+
n_clicks: Optional[int],
|
152
|
+
name: str,
|
153
|
+
scopes: List[str],
|
154
|
+
expiration: Optional[datetime] = None,
|
155
|
+
session_data: Optional[Dict[str, Any]] = None,
|
156
|
+
):
|
157
|
+
"""
|
158
|
+
Register the token.
|
159
|
+
"""
|
160
|
+
if not n_clicks:
|
161
|
+
raise PreventUpdate
|
162
|
+
|
163
|
+
session_id = (session_data or {}).get('session-id', None)
|
164
|
+
token = Token(
|
165
|
+
label=(name or None),
|
166
|
+
user=get_user_from_session(session_id),
|
167
|
+
expiration=(datetime.fromisoformat(f"{expiration}") if expiration is not None else None),
|
168
|
+
)
|
169
|
+
return False, True, build_tokens_register_output_modal(token)
|
170
|
+
|
171
|
+
|
172
|
+
@dash_app.callback(
|
173
|
+
Output("tokens-refresh-button", "n_clicks"),
|
174
|
+
Input("tokens-register-output-modal", "is_open"),
|
175
|
+
Input({'type': 'tokens-edit-modal', 'index': ALL}, 'is_open'),
|
176
|
+
Input({'type': 'tokens-invalidate-modal', 'index': ALL}, 'is_open'),
|
177
|
+
Input({'type': 'tokens-delete-modal', 'index': ALL}, 'is_open'),
|
178
|
+
State("tokens-refresh-button", "n_clicks"),
|
179
|
+
prevent_initial_call=True,
|
180
|
+
)
|
181
|
+
def register_token_modal_close_refresh(
|
182
|
+
register_is_open: bool,
|
183
|
+
edit_is_open_list,
|
184
|
+
invalidate_is_open_list,
|
185
|
+
delete_is_open_list,
|
186
|
+
n_clicks: int,
|
187
|
+
):
|
188
|
+
"""
|
189
|
+
Refresh the cards when the registration, edit, invalidate, or delete modals changes visibility.
|
190
|
+
"""
|
191
|
+
if any(
|
192
|
+
edit_is_open_list
|
193
|
+
+ invalidate_is_open_list
|
194
|
+
+ delete_is_open_list
|
195
|
+
):
|
196
|
+
raise PreventUpdate
|
197
|
+
|
198
|
+
return (n_clicks or 0) + 1
|
199
|
+
|
200
|
+
|
201
|
+
@dash_app.callback(
|
202
|
+
Output('tokens-register-clipboard', 'content'),
|
203
|
+
Output('tokens-register-clipboard', 'n_clicks'),
|
204
|
+
Output('tokens-register-copy-button', 'children'),
|
205
|
+
Input('tokens-register-copy-button', 'n_clicks'),
|
206
|
+
State('tokens-register-clipboard', 'n_clicks'),
|
207
|
+
State('token-id-pre', 'children'),
|
208
|
+
State('token-secret-pre', 'children'),
|
209
|
+
prevent_initial_call=True,
|
210
|
+
)
|
211
|
+
def copy_token_button_click(
|
212
|
+
n_clicks: int,
|
213
|
+
clipboard_n_clicks: Optional[int],
|
214
|
+
token_id: str,
|
215
|
+
token_secret: str,
|
216
|
+
) -> Tuple[str, int, str]:
|
217
|
+
"""
|
218
|
+
Copy the token's ID and secret to the clipboard.
|
219
|
+
"""
|
220
|
+
if not n_clicks:
|
221
|
+
raise PreventUpdate
|
222
|
+
return (
|
223
|
+
f"Client ID: {token_id}\nClient Secret: {token_secret}",
|
224
|
+
(clipboard_n_clicks or 0) + 1,
|
225
|
+
"Copied!",
|
226
|
+
)
|
227
|
+
|
228
|
+
|
229
|
+
@dash_app.callback(
|
230
|
+
Output('tokens-close-register-output-modal-button', 'disabled'),
|
231
|
+
Output('tokens-register-output-modal', 'backdrop'),
|
232
|
+
Input('tokens-register-clipboard', 'n_clicks'),
|
233
|
+
prevent_initial_call=True,
|
234
|
+
)
|
235
|
+
def enable_close_button(n_clicks):
|
236
|
+
"""
|
237
|
+
Enable the close button once the token has been copied.
|
238
|
+
"""
|
239
|
+
if not n_clicks:
|
240
|
+
raise PreventUpdate
|
241
|
+
return False, True
|
242
|
+
|
243
|
+
|
244
|
+
@dash_app.callback(
|
245
|
+
Output('tokens-register-output-modal', 'is_open'),
|
246
|
+
Input('tokens-close-register-output-modal-button', 'n_clicks'),
|
247
|
+
prevent_initial_call=True,
|
248
|
+
)
|
249
|
+
def close_register_output_modal(n_clicks: int) -> bool:
|
250
|
+
"""
|
251
|
+
Close the register output modal when the Close button is clicked.
|
252
|
+
"""
|
253
|
+
if not n_clicks:
|
254
|
+
raise PreventUpdate
|
255
|
+
return False
|
256
|
+
|
257
|
+
|
258
|
+
@dash_app.callback(
|
259
|
+
Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
|
260
|
+
Input({'type': 'tokens-edit-button', 'index': MATCH}, 'n_clicks'),
|
261
|
+
prevent_initial_call=True,
|
262
|
+
)
|
263
|
+
def edit_token_button_click(n_clicks: int):
|
264
|
+
if not n_clicks:
|
265
|
+
raise PreventUpdate
|
266
|
+
return True
|
267
|
+
|
268
|
+
|
269
|
+
@dash_app.callback(
|
270
|
+
Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
|
271
|
+
Output({'type': 'tokens-edit-alerts-div', 'index': MATCH}, 'children'),
|
272
|
+
Input({'type': 'tokens-edit-submit-button', 'index': MATCH}, 'n_clicks'),
|
273
|
+
State({'type': 'tokens-expiration-datepickersingle', 'index': MATCH}, 'date'),
|
274
|
+
State({'type': 'tokens-scopes-checklist', 'index': MATCH}, 'value'),
|
275
|
+
State({'type': 'tokens-name-input', 'index': MATCH}, 'value'),
|
276
|
+
prevent_initial_call=True,
|
277
|
+
)
|
278
|
+
def edit_token_submit_button_click(
|
279
|
+
n_clicks: int,
|
280
|
+
expiration: Optional[datetime],
|
281
|
+
scopes: List[str],
|
282
|
+
label: str,
|
283
|
+
):
|
284
|
+
if not n_clicks:
|
285
|
+
raise PreventUpdate
|
286
|
+
|
287
|
+
ctx = dash.callback_context.triggered
|
288
|
+
if ctx[0]['value'] is None:
|
289
|
+
raise PreventUpdate
|
290
|
+
|
291
|
+
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
292
|
+
token_id = component_dict['index']
|
293
|
+
|
294
|
+
token = Token(
|
295
|
+
id=token_id,
|
296
|
+
label=label,
|
297
|
+
expiration=(datetime.fromisoformat(f"{expiration}T00:00:00Z") if expiration is not None else None),
|
298
|
+
scopes=scopes,
|
299
|
+
instance=get_api_connector(),
|
300
|
+
)
|
301
|
+
|
302
|
+
success, msg = token.edit(debug=debug)
|
303
|
+
if not success:
|
304
|
+
return dash.no_update, alert_from_success_tuple((success, msg))
|
305
|
+
|
306
|
+
return False, dash.no_update
|
307
|
+
|
308
|
+
|
309
|
+
@dash_app.callback(
|
310
|
+
Output({'type': 'tokens-invalidate-modal', 'index': MATCH}, 'is_open'),
|
311
|
+
Input({'type': 'tokens-invalidate-button', 'index': MATCH}, 'n_clicks'),
|
312
|
+
prevent_initial_call=True,
|
313
|
+
)
|
314
|
+
def invalidate_token_click(n_clicks: int):
|
315
|
+
if not n_clicks:
|
316
|
+
raise PreventUpdate
|
317
|
+
return True
|
318
|
+
|
319
|
+
|
320
|
+
@dash_app.callback(
|
321
|
+
Output({'type': 'tokens-delete-modal', 'index': MATCH}, 'is_open'),
|
322
|
+
Input({'type': 'tokens-delete-button', 'index': MATCH}, 'n_clicks'),
|
323
|
+
prevent_initial_call=True,
|
324
|
+
)
|
325
|
+
def invalidate_token_click(n_clicks: int):
|
326
|
+
if not n_clicks:
|
327
|
+
raise PreventUpdate
|
328
|
+
return True
|
329
|
+
|
330
|
+
|
331
|
+
|
332
|
+
@dash_app.callback(
|
333
|
+
Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
|
334
|
+
Output({'type': 'tokens-invalidate-modal', 'index': MATCH}, 'is_open'),
|
335
|
+
Output({'type': 'tokens-invalidate-alerts-div', 'index': MATCH}, 'children'),
|
336
|
+
Input({'type': 'tokens-invalidate-confirm-button', 'index': MATCH}, 'n_clicks'),
|
337
|
+
prevent_initial_call=True,
|
338
|
+
)
|
339
|
+
def invalidate_token_confirm_click(n_clicks: int):
|
340
|
+
if not n_clicks:
|
341
|
+
raise PreventUpdate
|
342
|
+
|
343
|
+
ctx = dash.callback_context.triggered
|
344
|
+
if ctx[0]['value'] is None:
|
345
|
+
raise PreventUpdate
|
346
|
+
|
347
|
+
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
348
|
+
token_id = component_dict['index']
|
349
|
+
|
350
|
+
token = Token(
|
351
|
+
id=token_id,
|
352
|
+
instance=get_api_connector(),
|
353
|
+
)
|
354
|
+
|
355
|
+
success, msg = token.invalidate(debug=debug)
|
356
|
+
if not success:
|
357
|
+
return dash.no_update, dash.no_update, alert_from_success_tuple((success, msg))
|
358
|
+
|
359
|
+
return False, False, dash.no_update
|
360
|
+
|
361
|
+
|
362
|
+
@dash_app.callback(
|
363
|
+
Output({'type': 'tokens-edit-modal', 'index': MATCH}, 'is_open'),
|
364
|
+
Output({'type': 'tokens-delete-modal', 'index': MATCH}, 'is_open'),
|
365
|
+
Output({'type': 'tokens-delete-alerts-div', 'index': MATCH}, 'children'),
|
366
|
+
Input({'type': 'tokens-delete-confirm-button', 'index': MATCH}, 'n_clicks'),
|
367
|
+
prevent_initial_call=True,
|
368
|
+
)
|
369
|
+
def delete_token_confirm_click(n_clicks: int):
|
370
|
+
if not n_clicks:
|
371
|
+
raise PreventUpdate
|
372
|
+
|
373
|
+
ctx = dash.callback_context.triggered
|
374
|
+
if ctx[0]['value'] is None:
|
375
|
+
raise PreventUpdate
|
376
|
+
|
377
|
+
component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
|
378
|
+
token_id = component_dict['index']
|
379
|
+
|
380
|
+
token = Token(
|
381
|
+
id=token_id,
|
382
|
+
instance=get_api_connector(),
|
383
|
+
)
|
384
|
+
|
385
|
+
success, msg = token.delete(debug=debug)
|
386
|
+
if not success:
|
387
|
+
return dash.no_update, dash.no_update, alert_from_success_tuple((success, msg))
|
388
|
+
|
389
|
+
return False, False, []
|