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.
Files changed (200) 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 +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +198 -118
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +382 -95
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/js/terminado.js +3 -0
  68. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  69. meerschaum/api/resources/templates/termpage.html +13 -0
  70. meerschaum/api/routes/__init__.py +1 -0
  71. meerschaum/api/routes/_actions.py +3 -4
  72. meerschaum/api/routes/_connectors.py +3 -7
  73. meerschaum/api/routes/_jobs.py +26 -35
  74. meerschaum/api/routes/_login.py +120 -15
  75. meerschaum/api/routes/_misc.py +5 -10
  76. meerschaum/api/routes/_pipes.py +178 -143
  77. meerschaum/api/routes/_plugins.py +38 -28
  78. meerschaum/api/routes/_tokens.py +236 -0
  79. meerschaum/api/routes/_users.py +47 -35
  80. meerschaum/api/routes/_version.py +3 -3
  81. meerschaum/api/routes/_webterm.py +3 -3
  82. meerschaum/config/__init__.py +100 -30
  83. meerschaum/config/_default.py +132 -64
  84. meerschaum/config/_edit.py +38 -32
  85. meerschaum/config/_formatting.py +2 -0
  86. meerschaum/config/_patch.py +10 -8
  87. meerschaum/config/_paths.py +133 -13
  88. meerschaum/config/_read_config.py +87 -36
  89. meerschaum/config/_sync.py +6 -3
  90. meerschaum/config/_version.py +1 -1
  91. meerschaum/config/environment.py +262 -0
  92. meerschaum/config/stack/__init__.py +37 -15
  93. meerschaum/config/static.py +18 -0
  94. meerschaum/connectors/_Connector.py +11 -6
  95. meerschaum/connectors/__init__.py +41 -22
  96. meerschaum/connectors/api/_APIConnector.py +34 -6
  97. meerschaum/connectors/api/_actions.py +2 -2
  98. meerschaum/connectors/api/_jobs.py +12 -1
  99. meerschaum/connectors/api/_login.py +33 -7
  100. meerschaum/connectors/api/_misc.py +2 -2
  101. meerschaum/connectors/api/_pipes.py +23 -32
  102. meerschaum/connectors/api/_plugins.py +2 -2
  103. meerschaum/connectors/api/_request.py +1 -1
  104. meerschaum/connectors/api/_tokens.py +146 -0
  105. meerschaum/connectors/api/_users.py +70 -58
  106. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  107. meerschaum/connectors/instance/__init__.py +10 -0
  108. meerschaum/connectors/instance/_pipes.py +442 -0
  109. meerschaum/connectors/instance/_plugins.py +159 -0
  110. meerschaum/connectors/instance/_tokens.py +317 -0
  111. meerschaum/connectors/instance/_users.py +188 -0
  112. meerschaum/connectors/parse.py +5 -2
  113. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  114. meerschaum/connectors/sql/_cli.py +12 -11
  115. meerschaum/connectors/sql/_create_engine.py +12 -168
  116. meerschaum/connectors/sql/_fetch.py +2 -18
  117. meerschaum/connectors/sql/_pipes.py +295 -278
  118. meerschaum/connectors/sql/_plugins.py +29 -0
  119. meerschaum/connectors/sql/_sql.py +46 -21
  120. meerschaum/connectors/sql/_users.py +36 -2
  121. meerschaum/connectors/sql/tables/__init__.py +254 -122
  122. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  123. meerschaum/connectors/valkey/_pipes.py +60 -31
  124. meerschaum/connectors/valkey/_plugins.py +2 -26
  125. meerschaum/core/Pipe/__init__.py +115 -85
  126. meerschaum/core/Pipe/_attributes.py +425 -124
  127. meerschaum/core/Pipe/_bootstrap.py +54 -24
  128. meerschaum/core/Pipe/_cache.py +555 -0
  129. meerschaum/core/Pipe/_clear.py +0 -11
  130. meerschaum/core/Pipe/_data.py +96 -68
  131. meerschaum/core/Pipe/_deduplicate.py +0 -13
  132. meerschaum/core/Pipe/_delete.py +12 -21
  133. meerschaum/core/Pipe/_drop.py +11 -23
  134. meerschaum/core/Pipe/_dtypes.py +49 -19
  135. meerschaum/core/Pipe/_edit.py +14 -4
  136. meerschaum/core/Pipe/_fetch.py +1 -1
  137. meerschaum/core/Pipe/_index.py +8 -14
  138. meerschaum/core/Pipe/_show.py +5 -5
  139. meerschaum/core/Pipe/_sync.py +123 -204
  140. meerschaum/core/Pipe/_verify.py +4 -4
  141. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  142. meerschaum/core/Plugin/__init__.py +1 -1
  143. meerschaum/core/Token/_Token.py +220 -0
  144. meerschaum/core/Token/__init__.py +12 -0
  145. meerschaum/core/User/_User.py +35 -10
  146. meerschaum/core/User/__init__.py +9 -1
  147. meerschaum/core/__init__.py +1 -0
  148. meerschaum/jobs/_Executor.py +88 -4
  149. meerschaum/jobs/_Job.py +149 -38
  150. meerschaum/jobs/__init__.py +3 -2
  151. meerschaum/jobs/systemd.py +8 -3
  152. meerschaum/models/__init__.py +35 -0
  153. meerschaum/models/pipes.py +247 -0
  154. meerschaum/models/tokens.py +38 -0
  155. meerschaum/models/users.py +26 -0
  156. meerschaum/plugins/__init__.py +301 -88
  157. meerschaum/plugins/bootstrap.py +510 -4
  158. meerschaum/utils/_get_pipes.py +97 -30
  159. meerschaum/utils/daemon/Daemon.py +199 -43
  160. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  161. meerschaum/utils/daemon/RotatingFile.py +63 -36
  162. meerschaum/utils/daemon/StdinFile.py +53 -13
  163. meerschaum/utils/daemon/__init__.py +47 -6
  164. meerschaum/utils/daemon/_names.py +6 -3
  165. meerschaum/utils/dataframe.py +479 -81
  166. meerschaum/utils/debug.py +49 -19
  167. meerschaum/utils/dtypes/__init__.py +476 -34
  168. meerschaum/utils/dtypes/sql.py +369 -29
  169. meerschaum/utils/formatting/__init__.py +5 -2
  170. meerschaum/utils/formatting/_jobs.py +1 -1
  171. meerschaum/utils/formatting/_pipes.py +52 -50
  172. meerschaum/utils/formatting/_pprint.py +1 -0
  173. meerschaum/utils/formatting/_shell.py +44 -18
  174. meerschaum/utils/misc.py +268 -186
  175. meerschaum/utils/packages/__init__.py +25 -40
  176. meerschaum/utils/packages/_packages.py +42 -34
  177. meerschaum/utils/pipes.py +213 -0
  178. meerschaum/utils/process.py +2 -2
  179. meerschaum/utils/prompt.py +175 -144
  180. meerschaum/utils/schedule.py +2 -1
  181. meerschaum/utils/sql.py +134 -47
  182. meerschaum/utils/threading.py +42 -0
  183. meerschaum/utils/typing.py +1 -4
  184. meerschaum/utils/venv/_Venv.py +2 -2
  185. meerschaum/utils/venv/__init__.py +7 -7
  186. meerschaum/utils/warnings.py +19 -13
  187. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  188. meerschaum-3.0.0.dist-info/RECORD +289 -0
  189. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  190. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  191. meerschaum/api/models/_interfaces.py +0 -15
  192. meerschaum/api/models/_locations.py +0 -15
  193. meerschaum/api/models/_metrics.py +0 -15
  194. meerschaum/config/_environment.py +0 -145
  195. meerschaum/config/static/__init__.py +0 -186
  196. meerschaum-2.9.5.dist-info/RECORD +0 -263
  197. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  198. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  199. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  200. {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.pipes import build_pipe_card
16
- from meerschaum.api import CHECK_UPDATE
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
- return no_update
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
- if len(keys) not in (2, 3):
42
- return no_update
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
- html.Br(),
53
- build_pipe_card(pipe, authenticated=authenticated, include_manage=False),
54
- html.Br(),
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 ''
@@ -66,4 +66,3 @@ def edit_plugin_description(
66
66
  success, _msg = get_api_connector().register_plugin(plugin, debug=debug, force=True)
67
67
  msg = _msg if not success else "Successfully updated description."
68
68
  return [alert_from_success_tuple((success, msg))]
69
-
@@ -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.config.static import STATIC_CONFIG
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
- _ = login({'username': username, 'password': password})
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 as e:
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.config.static import STATIC_CONFIG
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, []