meerschaum 2.9.4__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 (201) 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 +228 -117
  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 +438 -88
  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/css/dash.css +16 -0
  68. meerschaum/api/resources/static/js/terminado.js +3 -0
  69. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  70. meerschaum/api/resources/templates/termpage.html +13 -0
  71. meerschaum/api/routes/__init__.py +1 -0
  72. meerschaum/api/routes/_actions.py +3 -4
  73. meerschaum/api/routes/_connectors.py +3 -7
  74. meerschaum/api/routes/_jobs.py +26 -35
  75. meerschaum/api/routes/_login.py +120 -15
  76. meerschaum/api/routes/_misc.py +5 -10
  77. meerschaum/api/routes/_pipes.py +178 -143
  78. meerschaum/api/routes/_plugins.py +38 -28
  79. meerschaum/api/routes/_tokens.py +236 -0
  80. meerschaum/api/routes/_users.py +47 -35
  81. meerschaum/api/routes/_version.py +3 -3
  82. meerschaum/api/routes/_webterm.py +3 -3
  83. meerschaum/config/__init__.py +100 -30
  84. meerschaum/config/_default.py +132 -64
  85. meerschaum/config/_edit.py +38 -32
  86. meerschaum/config/_formatting.py +2 -0
  87. meerschaum/config/_patch.py +10 -8
  88. meerschaum/config/_paths.py +133 -13
  89. meerschaum/config/_read_config.py +87 -36
  90. meerschaum/config/_sync.py +6 -3
  91. meerschaum/config/_version.py +1 -1
  92. meerschaum/config/environment.py +262 -0
  93. meerschaum/config/stack/__init__.py +37 -15
  94. meerschaum/config/static.py +18 -0
  95. meerschaum/connectors/_Connector.py +11 -6
  96. meerschaum/connectors/__init__.py +41 -22
  97. meerschaum/connectors/api/_APIConnector.py +34 -6
  98. meerschaum/connectors/api/_actions.py +2 -2
  99. meerschaum/connectors/api/_jobs.py +12 -1
  100. meerschaum/connectors/api/_login.py +33 -7
  101. meerschaum/connectors/api/_misc.py +2 -2
  102. meerschaum/connectors/api/_pipes.py +23 -32
  103. meerschaum/connectors/api/_plugins.py +2 -2
  104. meerschaum/connectors/api/_request.py +1 -1
  105. meerschaum/connectors/api/_tokens.py +146 -0
  106. meerschaum/connectors/api/_users.py +70 -58
  107. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  108. meerschaum/connectors/instance/__init__.py +10 -0
  109. meerschaum/connectors/instance/_pipes.py +442 -0
  110. meerschaum/connectors/instance/_plugins.py +159 -0
  111. meerschaum/connectors/instance/_tokens.py +317 -0
  112. meerschaum/connectors/instance/_users.py +188 -0
  113. meerschaum/connectors/parse.py +5 -2
  114. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  115. meerschaum/connectors/sql/_cli.py +12 -11
  116. meerschaum/connectors/sql/_create_engine.py +12 -168
  117. meerschaum/connectors/sql/_fetch.py +2 -18
  118. meerschaum/connectors/sql/_pipes.py +295 -278
  119. meerschaum/connectors/sql/_plugins.py +29 -0
  120. meerschaum/connectors/sql/_sql.py +47 -22
  121. meerschaum/connectors/sql/_users.py +36 -2
  122. meerschaum/connectors/sql/tables/__init__.py +254 -122
  123. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  124. meerschaum/connectors/valkey/_pipes.py +60 -31
  125. meerschaum/connectors/valkey/_plugins.py +2 -26
  126. meerschaum/core/Pipe/__init__.py +115 -85
  127. meerschaum/core/Pipe/_attributes.py +425 -124
  128. meerschaum/core/Pipe/_bootstrap.py +54 -24
  129. meerschaum/core/Pipe/_cache.py +555 -0
  130. meerschaum/core/Pipe/_clear.py +0 -11
  131. meerschaum/core/Pipe/_data.py +96 -68
  132. meerschaum/core/Pipe/_deduplicate.py +0 -13
  133. meerschaum/core/Pipe/_delete.py +12 -21
  134. meerschaum/core/Pipe/_drop.py +11 -23
  135. meerschaum/core/Pipe/_dtypes.py +49 -19
  136. meerschaum/core/Pipe/_edit.py +14 -4
  137. meerschaum/core/Pipe/_fetch.py +1 -1
  138. meerschaum/core/Pipe/_index.py +8 -14
  139. meerschaum/core/Pipe/_show.py +5 -5
  140. meerschaum/core/Pipe/_sync.py +123 -204
  141. meerschaum/core/Pipe/_verify.py +4 -4
  142. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  143. meerschaum/core/Plugin/__init__.py +1 -1
  144. meerschaum/core/Token/_Token.py +220 -0
  145. meerschaum/core/Token/__init__.py +12 -0
  146. meerschaum/core/User/_User.py +35 -10
  147. meerschaum/core/User/__init__.py +9 -1
  148. meerschaum/core/__init__.py +1 -0
  149. meerschaum/jobs/_Executor.py +88 -4
  150. meerschaum/jobs/_Job.py +149 -38
  151. meerschaum/jobs/__init__.py +3 -2
  152. meerschaum/jobs/systemd.py +8 -3
  153. meerschaum/models/__init__.py +35 -0
  154. meerschaum/models/pipes.py +247 -0
  155. meerschaum/models/tokens.py +38 -0
  156. meerschaum/models/users.py +26 -0
  157. meerschaum/plugins/__init__.py +301 -88
  158. meerschaum/plugins/bootstrap.py +510 -4
  159. meerschaum/utils/_get_pipes.py +97 -30
  160. meerschaum/utils/daemon/Daemon.py +199 -43
  161. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  162. meerschaum/utils/daemon/RotatingFile.py +63 -36
  163. meerschaum/utils/daemon/StdinFile.py +53 -13
  164. meerschaum/utils/daemon/__init__.py +47 -6
  165. meerschaum/utils/daemon/_names.py +6 -3
  166. meerschaum/utils/dataframe.py +480 -82
  167. meerschaum/utils/debug.py +49 -19
  168. meerschaum/utils/dtypes/__init__.py +478 -37
  169. meerschaum/utils/dtypes/sql.py +369 -29
  170. meerschaum/utils/formatting/__init__.py +5 -2
  171. meerschaum/utils/formatting/_jobs.py +1 -1
  172. meerschaum/utils/formatting/_pipes.py +52 -50
  173. meerschaum/utils/formatting/_pprint.py +1 -0
  174. meerschaum/utils/formatting/_shell.py +44 -18
  175. meerschaum/utils/misc.py +268 -186
  176. meerschaum/utils/packages/__init__.py +25 -40
  177. meerschaum/utils/packages/_packages.py +42 -34
  178. meerschaum/utils/pipes.py +213 -0
  179. meerschaum/utils/process.py +2 -2
  180. meerschaum/utils/prompt.py +175 -144
  181. meerschaum/utils/schedule.py +2 -1
  182. meerschaum/utils/sql.py +135 -49
  183. meerschaum/utils/threading.py +42 -0
  184. meerschaum/utils/typing.py +1 -4
  185. meerschaum/utils/venv/_Venv.py +2 -2
  186. meerschaum/utils/venv/__init__.py +7 -7
  187. meerschaum/utils/warnings.py +19 -13
  188. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  189. meerschaum-3.0.0.dist-info/RECORD +289 -0
  190. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  191. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  192. meerschaum/api/models/_interfaces.py +0 -15
  193. meerschaum/api/models/_locations.py +0 -15
  194. meerschaum/api/models/_metrics.py +0 -15
  195. meerschaum/config/_environment.py +0 -145
  196. meerschaum/config/static/__init__.py +0 -186
  197. meerschaum-2.9.4.dist-info/RECORD +0 -263
  198. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  199. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  200. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  201. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -86,6 +86,18 @@ def get_username_from_session(session_id: Optional[str]) -> Union[str, None]:
86
86
  return session_data.get('username', None)
87
87
 
88
88
 
89
+ def get_user_from_session(session_id: Optional[str]) -> Union[User, None]:
90
+ """
91
+ Return a `User` from the current session.
92
+ """
93
+ username = get_username_from_session(session_id)
94
+ if username is None:
95
+ return None
96
+
97
+ conn = get_api_connector()
98
+ return User(username, instance=conn)
99
+
100
+
89
101
  def is_session_active(session_id: Union[str, None]) -> bool:
90
102
  """
91
103
  Return whether a given `session_id` has been set.
@@ -0,0 +1,603 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Dash utility functions for constructing tokens components.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from datetime import datetime, timezone, timedelta
12
+ from typing import Any, List, Optional
13
+
14
+ import meerschaum as mrsm
15
+ from meerschaum.api import debug, CHECK_UPDATE, get_api_connector
16
+ from meerschaum.api.dash.connectors import get_web_connector
17
+ from meerschaum.api.dash.components import alert_from_success_tuple
18
+ from meerschaum.api.dash.sessions import get_user_from_session
19
+ from meerschaum.utils.typing import WebState, SuccessTuple, List, Tuple
20
+ from meerschaum.utils.packages import attempt_import, import_html, import_dcc
21
+ from meerschaum.utils.misc import interval_str
22
+ from meerschaum.utils.dtypes import value_is_null, round_time
23
+ from meerschaum._internal.static import STATIC_CONFIG
24
+ from meerschaum.core import Token
25
+ from meerschaum.utils.daemon import get_new_daemon_name
26
+ dcc, html = import_dcc(check_update=CHECK_UPDATE), import_html(check_update=CHECK_UPDATE)
27
+ dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
28
+
29
+
30
+ def get_tokens_table(session_id: Optional[str] = None) -> Tuple[dbc.Table, List[dbc.Alert]]:
31
+ """
32
+ Return the main tokens table.
33
+ """
34
+ conn = get_api_connector()
35
+ user = get_user_from_session(session_id) if session_id is not None else None
36
+ alerts = []
37
+ try:
38
+ tokens = conn.get_tokens(user=user, debug=debug)
39
+ except Exception as e:
40
+ tokens = []
41
+ alerts = [alert_from_success_tuple((False, f"Failed to fetch tokens from '{conn}':\n{e}"))]
42
+
43
+ if not tokens:
44
+ return tokens, alerts
45
+
46
+ table_header = [
47
+ html.Thead(
48
+ html.Tr([
49
+ html.Th("Label"),
50
+ html.Th("Client ID"),
51
+ html.Th("Created"),
52
+ html.Th("Expires in"),
53
+ html.Th("Is Valid"),
54
+ html.Th(""),
55
+ ]),
56
+ ),
57
+ ]
58
+
59
+ rows = [
60
+ html.Tr([
61
+ html.Td(str(token.label)),
62
+ html.Td(html.Pre(str(token.id))),
63
+ html.Td(get_creation_string(token).replace('Created ', '')),
64
+ html.Td(get_expiration_string(token).replace('Expires in ', '')),
65
+ html.Td("✅" if token.is_valid else "❌"),
66
+ html.Td([
67
+ dbc.Button(
68
+ html.B("⋮"),
69
+ color='link',
70
+ size='sm',
71
+ id={'type': 'tokens-context-button', 'index': str(token.id)},
72
+ style={'text-decoration': 'none'},
73
+ ),
74
+ build_manage_token_popover(token),
75
+ build_edit_token_modal(token),
76
+ build_invalidate_token_modal(token),
77
+ build_delete_token_modal(token),
78
+ ]),
79
+ ])
80
+ for token in tokens
81
+ ]
82
+
83
+ table_body = [html.Tbody(rows)]
84
+ table = dbc.Table(table_header + table_body)
85
+ return table, alerts
86
+
87
+
88
+ def build_manage_token_popover(token: Token) -> dbc.Popover:
89
+ """
90
+ Return the "Manage token" popover.
91
+ """
92
+ return dbc.Popover(
93
+ [
94
+ dbc.ButtonGroup(
95
+ ([
96
+ dbc.Button(
97
+ "Edit",
98
+ outline=True,
99
+ color='light',
100
+ id={
101
+ 'type': 'tokens-edit-button',
102
+ 'index': str(token.id),
103
+ },
104
+ ),
105
+ dbc.Button(
106
+ "Invalidate",
107
+ outline=True,
108
+ color='warning',
109
+ id={
110
+ 'type': 'tokens-invalidate-button',
111
+ 'index': str(token.id),
112
+ },
113
+ ),
114
+ ] if token.is_valid else []) + [
115
+ dbc.Button(
116
+ "Delete",
117
+ color='danger',
118
+ outline=True,
119
+ id={
120
+ 'type': 'tokens-delete-button',
121
+ 'index': str(token.id),
122
+ },
123
+ ),
124
+ ]),
125
+ ],
126
+ body=True,
127
+ trigger='legacy',
128
+ autohide=True,
129
+ id={'type': 'tokens-context-popover', 'index': str(token.id)},
130
+ target={'type': 'tokens-context-button', 'index': str(token.id)},
131
+ )
132
+
133
+
134
+ def build_edit_token_modal(token: Token) -> dbc.Modal:
135
+ """
136
+ Return the Modal for editing the token.
137
+ """
138
+ return dbc.Modal(
139
+ [
140
+ dbc.ModalHeader([
141
+ html.H4([
142
+ "Edit token ",
143
+ html.B(str(token.label))
144
+ ]),
145
+ ]),
146
+ dbc.ModalBody([
147
+ html.Div(id={'type': 'tokens-edit-alerts-div', 'index': str(token.id)}),
148
+
149
+ dbc.Form([
150
+ dbc.Row(
151
+ ([
152
+ dbc.Label("Name", width='auto'),
153
+ dbc.Col(
154
+ [
155
+ dbc.Input(
156
+ placeholder="Enter token's label",
157
+ value=str(token.label),
158
+ id={
159
+ 'type': 'tokens-name-input',
160
+ 'index': str(token.id),
161
+ },
162
+ ),
163
+ ],
164
+ className="me-3",
165
+ ),
166
+ dbc.Label("Expiration", width='auto'),
167
+ dbc.Col(
168
+ dcc.DatePickerSingle(
169
+ date=(
170
+ token.expiration.to_pydatetime()
171
+ if hasattr(token.expiration, 'to_pydatetime')
172
+ else token.expiration
173
+ ),
174
+ clearable=True,
175
+ min_date_allowed=datetime.today().date(),
176
+ display_format="YYYY-MM-DD",
177
+ id={
178
+ 'type': 'tokens-expiration-datepickersingle',
179
+ 'index': str(token.id),
180
+ },
181
+ )
182
+ ),
183
+ ] if token.is_valid else []) + [
184
+ dbc.Col([
185
+ ]),
186
+ ],
187
+ className='g-2',
188
+ ),
189
+ html.Br(),
190
+ dbc.Row([
191
+ ]),
192
+ html.Div([
193
+ dbc.Button(
194
+ "Deselect all",
195
+ size='sm',
196
+ color='link',
197
+ id={
198
+ 'type': "tokens-deselect-scopes-button",
199
+ 'index': str(token.id),
200
+ },
201
+ style={'text-decoration': 'none'},
202
+ ),
203
+ html.Br(),
204
+ dbc.Row([
205
+ dbc.Label("Scopes", width='auto'),
206
+ dbc.Col([
207
+ dbc.Checklist(
208
+ options=[
209
+ {"label": scope, "value": scope}
210
+ for scope in STATIC_CONFIG['tokens']['scopes']
211
+ ],
212
+ value=token.scopes,
213
+ id={
214
+ 'type': "tokens-scopes-checklist",
215
+ 'index': str(token.id),
216
+ },
217
+ style={'columnCount': 3},
218
+ ),
219
+ ]),
220
+ ]),
221
+ ] if token.is_valid else [], id={
222
+ 'type': 'tokens-scopes-checklist-div',
223
+ 'index': str(token.id),
224
+ }),
225
+ ]),
226
+
227
+ ]),
228
+ dbc.ModalFooter(
229
+ [
230
+ html.Small(str(token.id)),
231
+ dbc.Button(
232
+ "Submit",
233
+ id={'type': 'tokens-edit-submit-button', 'index': str(token.id)},
234
+ ),
235
+ ],
236
+ className='d-flex justify-content-between',
237
+ ),
238
+ ],
239
+ size='lg',
240
+ is_open=False,
241
+ id={'type': 'tokens-edit-modal', 'index': str(token.id)},
242
+ )
243
+
244
+
245
+ def build_invalidate_token_modal(token: Token) -> dbc.Modal:
246
+ """
247
+ Return the Invalidate token modal.
248
+ """
249
+ return dbc.Modal(
250
+ [
251
+ dbc.ModalHeader([
252
+ html.H4([
253
+ "Invalidate token ",
254
+ html.B(token.label),
255
+ "?"
256
+ ]),
257
+ ]),
258
+ dbc.ModalBody([
259
+ html.Div(
260
+ id={
261
+ 'type': 'tokens-invalidate-alerts-div',
262
+ 'index': str(token.id)
263
+ },
264
+ ),
265
+ html.P(
266
+ [
267
+ "Are you sure you want to invalidate token ",
268
+ html.B(token.label),
269
+ " (",
270
+ html.I(str(token.id)),
271
+ ")?",
272
+ ],
273
+ ),
274
+ html.P([html.B("This action cannot be undone!")]),
275
+ ]),
276
+ dbc.ModalFooter([
277
+ dbc.Button(
278
+ "Invalidate",
279
+ color='danger',
280
+ id={
281
+ 'type': 'tokens-invalidate-confirm-button',
282
+ 'index': str(token.id),
283
+ },
284
+ ),
285
+ ]),
286
+ ],
287
+ id={
288
+ 'type': 'tokens-invalidate-modal',
289
+ 'index': str(token.id),
290
+ },
291
+ )
292
+
293
+
294
+ def build_delete_token_modal(token: Token) -> dbc.Modal:
295
+ """
296
+ Return the delete token modal.
297
+ """
298
+ return dbc.Modal(
299
+ [
300
+ dbc.ModalHeader([
301
+ html.H4([
302
+ "Delete token ",
303
+ html.B(token.label),
304
+ "?"
305
+ ]),
306
+ ]),
307
+ dbc.ModalBody([
308
+ html.Div(
309
+ id={
310
+ 'type': 'tokens-delete-alerts-div',
311
+ 'index': str(token.id)
312
+ },
313
+ ),
314
+ html.P(
315
+ [
316
+ "Are you sure you want to delete token ",
317
+ html.B(token.label),
318
+ " (",
319
+ html.I(str(token.id)),
320
+ ")?",
321
+ ],
322
+ ),
323
+ html.P([html.B("This action cannot be undone!")]),
324
+ ]),
325
+ dbc.ModalFooter([
326
+ dbc.Button(
327
+ "Delete",
328
+ color='danger',
329
+ id={
330
+ 'type': 'tokens-delete-confirm-button',
331
+ 'index': str(token.id),
332
+ },
333
+ ),
334
+ ]),
335
+ ],
336
+ id={
337
+ 'type': 'tokens-delete-modal',
338
+ 'index': str(token.id),
339
+ },
340
+ )
341
+
342
+
343
+ def get_tokens_cards(session_id: Optional[str] = None) -> Tuple[List[dbc.Card], List[dbc.Alert]]:
344
+ """
345
+ Return the cards and alerts for tokens.
346
+ """
347
+ cards, alerts = [], []
348
+ conn = get_api_connector()
349
+ user = get_user_from_session(session_id) if session_id is not None else None
350
+ try:
351
+ tokens = conn.get_tokens(user=user, debug=debug)
352
+ except Exception as e:
353
+ tokens = []
354
+ alerts = [alert_from_success_tuple((False, f"Failed to fetch tokens from '{conn}':\n{e}"))]
355
+
356
+ for token in tokens:
357
+ try:
358
+ cards.append(
359
+ dbc.Card([
360
+ dbc.CardHeader(
361
+ [
362
+ html.H5(token.label),
363
+ ]
364
+ ),
365
+ dbc.CardBody(
366
+ [
367
+ html.Code(str(token.id), style={'color': '#999999'}),
368
+ ]
369
+ ),
370
+ dbc.CardFooter(
371
+ [
372
+ html.P(
373
+ get_creation_string(token),
374
+ style={'color': '#999999'},
375
+ ),
376
+ html.P(
377
+ get_expiration_string(token),
378
+ style={'color': '#999999'},
379
+ ),
380
+ ]
381
+ ),
382
+ ])
383
+ )
384
+ except Exception as e:
385
+ alerts.append(
386
+ alert_from_success_tuple((False, f"Failed to load metadata for token:\n{e}"))
387
+ )
388
+
389
+ return cards, alerts
390
+
391
+
392
+ def get_creation_string(token: mrsm.core.Token) -> str:
393
+ """
394
+ Return the formatted string to represent the token's creation timestamp.
395
+ """
396
+ creation = token.creation
397
+ if value_is_null(str(creation)):
398
+ return ''
399
+ now = datetime.now(timezone.utc)
400
+ return 'Created ' + interval_str(creation - now, round_unit=True)
401
+
402
+
403
+ def get_expiration_string(token: mrsm.core.Token) -> str:
404
+ """
405
+ Return the formatted string to represent the token's expiration timestamp.
406
+ """
407
+ expiration = token.expiration
408
+ if value_is_null(str(expiration)):
409
+ return 'Does not expire'
410
+ now = datetime.now(timezone.utc)
411
+ return 'Expires in ' + interval_str(expiration - now, round_unit=True)
412
+
413
+
414
+ def build_tokens_register_input_modal() -> dbc.Modal:
415
+ """
416
+ Return the layout for the tokens register input modal.
417
+ """
418
+ now = datetime.now(timezone.utc)
419
+ default_expiration_days = mrsm.get_config(
420
+ 'system', 'api', 'tokens', 'default_expiration_days',
421
+ ) or 366
422
+ default_expiration = round_time(
423
+ now + timedelta(days=default_expiration_days),
424
+ timedelta(days=1),
425
+ )
426
+ min_date_allowed = round_time(now + timedelta(days=1), timedelta(days=1))
427
+
428
+ return [
429
+ dbc.ModalHeader(html.H4("Register Token")),
430
+ dbc.ModalBody([
431
+ dbc.Form([
432
+ dbc.Row(
433
+ [
434
+ dbc.Label("Name", width='auto'),
435
+ dbc.Col(
436
+ [
437
+ dbc.Input(
438
+ placeholder="Enter token's label",
439
+ value=get_new_daemon_name(),
440
+ id='tokens-name-input'
441
+ ),
442
+ ],
443
+ className="me-3",
444
+ ),
445
+ dbc.Label("Expiration", width='auto'),
446
+ dbc.Col(
447
+ dcc.DatePickerSingle(
448
+ date=default_expiration,
449
+ clearable=True,
450
+ min_date_allowed=min_date_allowed,
451
+ display_format="YYYY-MM-DD",
452
+ id='tokens-expiration-datepickersingle',
453
+ )
454
+ ),
455
+ dbc.Col(
456
+ dbc.Switch(
457
+ id="tokens-toggle-scopes-switch",
458
+ label="Grant all scopes",
459
+ value=True,
460
+ ),
461
+ className="me-3",
462
+ ),
463
+ ],
464
+ className='g-2',
465
+ ),
466
+ html.Br(),
467
+ dbc.Row([
468
+ ]),
469
+ html.Div([
470
+ dbc.Button(
471
+ "Deselect all",
472
+ size='sm',
473
+ color='link',
474
+ id="tokens-deselect-scopes-button",
475
+ style={'text-decoration': 'none'},
476
+ ),
477
+ html.Br(),
478
+ dbc.Row([
479
+ dbc.Label("Scopes", width='auto'),
480
+ dbc.Col([
481
+ dbc.Checklist(
482
+ options=[
483
+ {"label": scope, "value": scope}
484
+ for scope in STATIC_CONFIG['tokens']['scopes']
485
+ ],
486
+ value=list(STATIC_CONFIG['tokens']['scopes']),
487
+ id="tokens-scopes-checklist",
488
+ style={'columnCount': 3},
489
+ ),
490
+ ]),
491
+ ]),
492
+ ], id='tokens-scopes-checklist-div', style={'display': 'none'}),
493
+ ]),
494
+ ]),
495
+ dbc.ModalFooter([
496
+ dbc.Button('Register', id='tokens-register-button'),
497
+ ]),
498
+ ]
499
+
500
+
501
+ def build_register_table_from_token(token: Token) -> dbc.Table:
502
+ """
503
+ Return a table with the token's metadata.
504
+ """
505
+ table_header = [html.Thead(html.Tr([html.Th("Attribute"), html.Th("Value")]))]
506
+ table_header = []
507
+ pre_style = {'white-space': 'pre-wrap', 'word-break': 'break-all'}
508
+ rows = [
509
+ html.Tr([
510
+ html.Td(html.B("Client ID")),
511
+ html.Td(html.Pre(
512
+ str(token.id),
513
+ style=pre_style,
514
+ id='token-id-pre',
515
+ )),
516
+ ]),
517
+ html.Tr([
518
+ html.Td(html.B("Client Secret")),
519
+ html.Td(html.Pre(
520
+ str(token.secret),
521
+ style=pre_style,
522
+ id='token-secret-pre',
523
+ )),
524
+ ]),
525
+ html.Tr([
526
+ html.Td(html.B("API Key")),
527
+ html.Td(html.Pre(token.get_api_key(), style=pre_style)),
528
+ ]),
529
+ html.Tr([
530
+ html.Td(html.B("Expiration")),
531
+ html.Td(
532
+ html.Pre(token.expiration.isoformat(), style=pre_style)
533
+ if token.expiration is not None
534
+ else "Does not expire"
535
+ ),
536
+ ]),
537
+ html.Tr([
538
+ html.Td(html.B("Scopes")),
539
+ html.Td(html.P(' '.join(token.scopes), style={'word-break': 'normal'})),
540
+ ]),
541
+ html.Tr([
542
+ html.Td(html.B("User")),
543
+ html.Td(token.user.username if token.user is not None else ""),
544
+ ]),
545
+ ]
546
+ table_body = [html.Tbody(rows)]
547
+ table = dbc.Table(
548
+ table_header + table_body,
549
+ id='tokens-register-table',
550
+ )
551
+ return table
552
+
553
+
554
+ def build_tokens_register_output_modal(token: Token) -> List[Any]:
555
+ """
556
+ Return the layout for the tokens register output modal.
557
+ """
558
+ success, msg = token.register(debug=debug)
559
+ header_text = (
560
+ "Registered token "
561
+ if success
562
+ else "Failed to register token "
563
+ )
564
+ body_children = (
565
+ [
566
+ dbc.Stack(
567
+ [
568
+ html.Div([
569
+ html.P(html.B("Copy the token's details to dismiss.")),
570
+ html.P(html.I("The secret and API key will not be shown again.")),
571
+ ]),
572
+ html.Div([
573
+ dbc.Button(
574
+ "Copy token details",
575
+ id='tokens-register-copy-button',
576
+ ),
577
+ ], className="ms-auto"),
578
+ html.Div([
579
+ dcc.Clipboard(id="tokens-register-clipboard"),
580
+ ]),
581
+ ],
582
+ direction='horizontal',
583
+ gap=2,
584
+ ),
585
+ build_register_table_from_token(token),
586
+ ]
587
+ if success
588
+ else alert_from_success_tuple((False, msg))
589
+ )
590
+ return [
591
+ dbc.ModalHeader(
592
+ html.H4([header_text, html.B(token.label)]),
593
+ close_button=False,
594
+ ),
595
+ dbc.ModalBody(body_children),
596
+ dbc.ModalFooter([
597
+ dbc.Button(
598
+ "Close",
599
+ id='tokens-close-register-output-modal-button',
600
+ disabled=success,
601
+ ),
602
+ ]),
603
+ ]
@@ -8,7 +8,7 @@ Functions for interacting via WebSockets.
8
8
 
9
9
  import asyncio, sys
10
10
  from meerschaum.api._websockets import websockets
11
- from meerschaum.config.static import STATIC_CONFIG
11
+ from meerschaum._internal.static import STATIC_CONFIG
12
12
 
13
13
  def ws_url_from_href(href: str) -> str:
14
14
  """
@@ -7,12 +7,13 @@ Functions for interacting with the Webterm via the dashboard.
7
7
  """
8
8
 
9
9
  import time
10
+ from typing import Optional, Tuple, Any
10
11
 
11
12
  import meerschaum as mrsm
12
- from meerschaum.api import CHECK_UPDATE, get_api_connector
13
+ from meerschaum.api import CHECK_UPDATE, get_api_connector, webterm_port
13
14
  from meerschaum.api.dash.sessions import is_session_authenticated, get_username_from_session
14
15
  from meerschaum.api.dash.components import alert_from_success_tuple, console_div
15
- from meerschaum.utils.typing import WebState, Tuple, Any
16
+ from meerschaum.utils.typing import WebState
16
17
  from meerschaum.utils.packages import attempt_import, import_html, import_dcc
17
18
  from meerschaum._internal.term.tools import is_webterm_running
18
19
  from meerschaum.utils.threading import Thread, RLock
@@ -22,7 +23,7 @@ dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK
22
23
 
23
24
  MAX_WEBTERM_ATTEMPTS: int = 10
24
25
  TMUX_IS_ENABLED: bool = (
25
- is_tmux_available() and mrsm.get_config('system', 'webterm', 'tmux', 'enabled')
26
+ is_tmux_available() and mrsm.get_config('api', 'webterm', 'tmux', 'enabled')
26
27
  )
27
28
 
28
29
  _locks = {'webterm_thread': RLock()}
@@ -31,6 +32,10 @@ def get_webterm(state: WebState) -> Tuple[Any, Any]:
31
32
  """
32
33
  Start the webterm and return its iframe.
33
34
  """
35
+ from meerschaum.api import _include_webterm
36
+ if not _include_webterm:
37
+ return console_div, []
38
+
34
39
  session_id = state['session-store.data'].get('session-id', None)
35
40
  username = get_username_from_session(session_id)
36
41
  if not is_session_authenticated(session_id):
@@ -47,7 +52,7 @@ def get_webterm(state: WebState) -> Tuple[Any, Any]:
47
52
  )
48
53
 
49
54
  for i in range(MAX_WEBTERM_ATTEMPTS):
50
- if is_webterm_running('localhost', 8765, session_id=(username or session_id)):
55
+ if is_webterm_running('localhost', webterm_port, session_id=(username or session_id)):
51
56
  return (
52
57
  [
53
58
  html.Div(
@@ -87,7 +92,7 @@ def get_webterm(state: WebState) -> Tuple[Any, Any]:
87
92
 
88
93
 
89
94
  webterm_procs = {}
90
- def start_webterm() -> None:
95
+ def start_webterm(webterm_port: Optional[int] = None) -> None:
91
96
  """
92
97
  Start the webterm thread.
93
98
  """
@@ -97,7 +102,14 @@ def start_webterm() -> None:
97
102
  conn = get_api_connector()
98
103
  _ = run_python_package(
99
104
  'meerschaum',
100
- ['start', 'webterm', '-i', str(conn)],
105
+ (
106
+ ['start', 'webterm', '-i', str(conn)]
107
+ + (
108
+ []
109
+ if not webterm_port
110
+ else ['-p', str(webterm_port)]
111
+ )
112
+ ),
101
113
  capture_output=True,
102
114
  as_proc=True,
103
115
  store_proc_dict=webterm_procs,