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
@@ -0,0 +1,317 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the high level tokens instance methods.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import List, Union, Optional, Dict
11
+ import uuid
12
+ from datetime import datetime, timezone
13
+
14
+ import meerschaum as mrsm
15
+ from meerschaum.core import Token, User
16
+ from meerschaum.core.User import hash_password
17
+ from meerschaum._internal.static import STATIC_CONFIG
18
+ from meerschaum.utils.warnings import dprint
19
+
20
+
21
+ def get_tokens_pipe(self) -> mrsm.Pipe:
22
+ """
23
+ Return the internal pipe for tokens management.
24
+ """
25
+ if '_tokens_pipe' in self.__dict__:
26
+ return self._tokens_pipe
27
+
28
+ users_pipe = self.get_users_pipe()
29
+ user_id_dtype = (
30
+ users_pipe._attributes.get('parameters', {}).get('dtypes', {}).get('user_id', 'uuid')
31
+ )
32
+
33
+ cache_connector = self.__dict__.get('_cache_connector', None)
34
+
35
+ self._tokens_pipe = mrsm.Pipe(
36
+ 'mrsm', 'tokens',
37
+ instance=self,
38
+ target='mrsm_tokens',
39
+ temporary=True,
40
+ cache=True,
41
+ cache_connector_keys=cache_connector,
42
+ static=True,
43
+ autotime=True,
44
+ null_indices=False,
45
+ columns={
46
+ 'datetime': 'creation',
47
+ 'primary': 'id',
48
+ },
49
+ indices={
50
+ 'unique': 'label',
51
+ 'user_id': 'user_id',
52
+ },
53
+ dtypes={
54
+ 'id': 'uuid',
55
+ 'creation': 'datetime',
56
+ 'expiration': 'datetime',
57
+ 'is_valid': 'bool',
58
+ 'label': 'string',
59
+ 'user_id': user_id_dtype,
60
+ 'scopes': 'json',
61
+ 'secret_hash': 'string',
62
+ },
63
+ )
64
+ return self._tokens_pipe
65
+
66
+
67
+ def register_token(
68
+ self,
69
+ token: Token,
70
+ debug: bool = False,
71
+ ) -> mrsm.SuccessTuple:
72
+ """
73
+ Register the new token to the tokens table.
74
+ """
75
+ token_id, token_secret = token.generate_credentials()
76
+ tokens_pipe = self.get_tokens_pipe()
77
+ user_id = self.get_user_id(token.user) if token.user is not None else None
78
+ if user_id is None:
79
+ return False, "Cannot register a token without a user."
80
+
81
+ doc = {
82
+ 'id': token_id,
83
+ 'user_id': user_id,
84
+ 'creation': datetime.now(timezone.utc),
85
+ 'expiration': token.expiration,
86
+ 'label': token.label,
87
+ 'is_valid': token.is_valid,
88
+ 'scopes': list(token.scopes) if token.scopes else [],
89
+ 'secret_hash': hash_password(
90
+ str(token_secret),
91
+ rounds=STATIC_CONFIG['tokens']['hash_rounds']
92
+ ),
93
+ }
94
+ sync_success, sync_msg = tokens_pipe.sync([doc], check_existing=False, debug=debug)
95
+ if not sync_success:
96
+ return False, f"Failed to register token:\n{sync_msg}"
97
+ return True, "Success"
98
+
99
+
100
+ def edit_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
101
+ """
102
+ Persist the token's in-memory state to the tokens pipe.
103
+ """
104
+ if not token.id:
105
+ return False, "Token ID is not set."
106
+
107
+ if not token.exists(debug=debug):
108
+ return False, f"Token {token.id} does not exist."
109
+
110
+ if not token.creation:
111
+ token_model = self.get_token_model(token.id)
112
+ token.creation = token_model.creation
113
+
114
+ tokens_pipe = self.get_tokens_pipe()
115
+ doc = {
116
+ 'id': token.id,
117
+ 'creation': token.creation,
118
+ 'expiration': token.expiration,
119
+ 'label': token.label,
120
+ 'is_valid': token.is_valid,
121
+ 'scopes': list(token.scopes) if token.scopes else [],
122
+ }
123
+ sync_success, sync_msg = tokens_pipe.sync([doc], debug=debug)
124
+ if not sync_success:
125
+ return False, f"Failed to edit token '{token.id}':\n{sync_msg}"
126
+
127
+ return True, "Success"
128
+
129
+
130
+ def invalidate_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
131
+ """
132
+ Set `is_valid` to `False` for the given token.
133
+ """
134
+ if not token.id:
135
+ return False, "Token ID is not set."
136
+
137
+ if not token.exists(debug=debug):
138
+ return False, f"Token {token.id} does not exist."
139
+
140
+ if not token.creation:
141
+ token_model = self.get_token_model(token.id)
142
+ token.creation = token_model.creation
143
+
144
+ token.is_valid = False
145
+ tokens_pipe = self.get_tokens_pipe()
146
+ doc = {
147
+ 'id': token.id,
148
+ 'creation': token.creation,
149
+ 'is_valid': False,
150
+ }
151
+ sync_success, sync_msg = tokens_pipe.sync([doc], debug=debug)
152
+ if not sync_success:
153
+ return False, f"Failed to invalidate token '{token.id}':\n{sync_msg}"
154
+
155
+ return True, "Success"
156
+
157
+
158
+ def delete_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
159
+ """
160
+ Delete the given token from the tokens table.
161
+ """
162
+ if not token.id:
163
+ return False, "Token ID is not set."
164
+
165
+ if not token.exists(debug=debug):
166
+ return False, f"Token {token.id} does not exist."
167
+
168
+ if not token.creation:
169
+ token_model = self.get_token_model(token.id)
170
+ token.creation = token_model.creation
171
+
172
+ token.is_valid = False
173
+ tokens_pipe = self.get_tokens_pipe()
174
+ clear_success, clear_msg = tokens_pipe.clear(params={'id': token.id}, debug=debug)
175
+ if not clear_success:
176
+ return False, f"Failed to delete token '{token.id}':\n{clear_msg}"
177
+
178
+ return True, "Success"
179
+
180
+
181
+ def get_tokens(
182
+ self,
183
+ user: Optional[User] = None,
184
+ labels: Optional[List[str]] = None,
185
+ ids: Optional[List[uuid.UUID]] = None,
186
+ debug: bool = False,
187
+ ) -> List[Token]:
188
+ """
189
+ Return a list of `Token` objects.
190
+ """
191
+ tokens_pipe = self.get_tokens_pipe()
192
+ user_id = (
193
+ self.get_user_id(user, debug=debug)
194
+ if user is not None
195
+ else None
196
+ )
197
+ user_type = self.get_user_type(user, debug=debug) if user is not None else None
198
+ params = (
199
+ {
200
+ 'user_id': (
201
+ user_id
202
+ if user_type != 'admin'
203
+ else [user_id, None]
204
+ )
205
+ }
206
+ if user_id is not None
207
+ else {}
208
+ )
209
+ if labels:
210
+ params['label'] = labels
211
+ if ids:
212
+ params['id'] = ids
213
+
214
+ if debug:
215
+ dprint(f"Getting tokens with {user_id=}, {params=}")
216
+
217
+ tokens_df = tokens_pipe.get_data(params=params, debug=debug)
218
+ if tokens_df is None:
219
+ return []
220
+
221
+ if debug:
222
+ dprint(f"Retrieved tokens dataframe:\n{tokens_df}")
223
+
224
+ tokens_docs = tokens_df.to_dict(orient='records')
225
+ return [
226
+ Token(
227
+ instance=self,
228
+ **token_doc
229
+ )
230
+ for token_doc in reversed(tokens_docs)
231
+ ]
232
+
233
+
234
+ def get_token(self, token_id: Union[uuid.UUID, str], debug: bool = False) -> Union[Token, None]:
235
+ """
236
+ Return the `Token` from its ID.
237
+ """
238
+ from meerschaum.utils.misc import is_uuid
239
+ if isinstance(token_id, str):
240
+ if is_uuid(token_id):
241
+ token_id = uuid.UUID(token_id)
242
+ else:
243
+ raise ValueError("Invalid token ID.")
244
+ token_model = self.get_token_model(token_id)
245
+ if token_model is None:
246
+ return None
247
+ return Token(**dict(token_model))
248
+
249
+
250
+ def get_token_model(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> 'Union[TokenModel, None]':
251
+ """
252
+ Return a token's model from the instance.
253
+ """
254
+ from meerschaum.models import TokenModel
255
+ if isinstance(token_id, Token):
256
+ token_id = Token.id
257
+ if not token_id:
258
+ raise ValueError("Invalid token ID.")
259
+ tokens_pipe = self.get_tokens_pipe()
260
+ doc = tokens_pipe.get_doc(
261
+ params={'id': token_id},
262
+ debug=debug,
263
+ )
264
+ if doc is None:
265
+ return None
266
+ return TokenModel(**doc)
267
+
268
+
269
+ def get_token_secret_hash(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> Union[str, None]:
270
+ """
271
+ Return the secret hash for a given token.
272
+ """
273
+ if isinstance(token_id, Token):
274
+ token_id = token_id.id
275
+ if not token_id:
276
+ raise ValueError("Invalid token ID.")
277
+ tokens_pipe = self.get_tokens_pipe()
278
+ return tokens_pipe.get_value('secret_hash', params={'id': token_id}, debug=debug)
279
+
280
+
281
+ def get_token_user_id(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> Union[int, str, uuid.UUID, None]:
282
+ """
283
+ Return a token's user_id.
284
+ """
285
+ if isinstance(token_id, Token):
286
+ token_id = token_id.id
287
+ if not token_id:
288
+ raise ValueError("Invalid token ID.")
289
+
290
+ tokens_pipe = self.get_tokens_pipe()
291
+ return tokens_pipe.get_value('user_id', params={'id': token_id}, debug=debug)
292
+
293
+
294
+ def get_token_scopes(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> List[str]:
295
+ """
296
+ Return the scopes for a token.
297
+ """
298
+ if isinstance(token_id, Token):
299
+ token_id = token_id.id
300
+ if not token_id:
301
+ raise ValueError("Invalid token ID.")
302
+
303
+ tokens_pipe = self.get_tokens_pipe()
304
+ return tokens_pipe.get_value('scopes', params={'id': token_id}, debug=debug) or []
305
+
306
+
307
+ def token_exists(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> bool:
308
+ """
309
+ Return `True` if a token exists in the tokens pipe.
310
+ """
311
+ if isinstance(token_id, Token):
312
+ token_id = token_id.id
313
+ if not token_id:
314
+ raise ValueError("Invalid token ID.")
315
+
316
+ tokens_pipe = self.get_tokens_pipe()
317
+ return tokens_pipe.get_value('creation', params={'id': token_id}, debug=debug) is not None
@@ -0,0 +1,188 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define high-level user-management methods for instance connectors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import uuid
11
+ from typing import Any, Union, Optional, List, Dict
12
+
13
+ import meerschaum as mrsm
14
+ from meerschaum.core import User
15
+
16
+
17
+ def get_users_pipe(self) -> 'mrsm.Pipe':
18
+ """
19
+ Return the pipe used for users registration.
20
+ """
21
+ if '_users_pipe' in self.__dict__:
22
+ return self._users_pipe
23
+
24
+ cache_connector = self.__dict__.get('_cache_connector', None)
25
+ self._users_pipe = mrsm.Pipe(
26
+ 'mrsm', 'users',
27
+ instance=self,
28
+ target='mrsm_users',
29
+ temporary=True,
30
+ cache=True,
31
+ cache_connector_keys=cache_connector,
32
+ static=True,
33
+ null_indices=False,
34
+ columns={
35
+ 'primary': 'user_id',
36
+ },
37
+ dtypes={
38
+ 'user_id': 'uuid',
39
+ 'username': 'string',
40
+ 'password_hash': 'string',
41
+ 'email': 'string',
42
+ 'user_type': 'string',
43
+ 'attributes': 'json',
44
+ },
45
+ indices={
46
+ 'unique': 'username',
47
+ },
48
+ )
49
+ return self._users_pipe
50
+
51
+
52
+ def register_user(
53
+ self,
54
+ user: User,
55
+ debug: bool = False,
56
+ **kwargs: Any
57
+ ) -> mrsm.SuccessTuple:
58
+ """
59
+ Register a new user to the users pipe.
60
+ """
61
+ users_pipe = self.get_users_pipe()
62
+ user.user_id = uuid.uuid4()
63
+ sync_success, sync_msg = users_pipe.sync(
64
+ [{
65
+ 'user_id': user.user_id,
66
+ 'username': user.username,
67
+ 'email': user.email,
68
+ 'password_hash': user.password_hash,
69
+ 'user_type': user.type,
70
+ 'attributes': user.attributes,
71
+ }],
72
+ check_existing=False,
73
+ debug=debug,
74
+ )
75
+ if not sync_success:
76
+ return False, f"Failed to register user '{user.username}':\n{sync_msg}"
77
+
78
+ return True, "Success"
79
+
80
+
81
+ def get_user_id(self, user: User, debug: bool = False) -> Union[uuid.UUID, None]:
82
+ """
83
+ Return a user's ID from the username.
84
+ """
85
+ users_pipe = self.get_users_pipe()
86
+ result_df = users_pipe.get_data(['user_id'], params={'username': user.username}, limit=1)
87
+ if result_df is None or len(result_df) == 0:
88
+ return None
89
+ return result_df['user_id'][0]
90
+
91
+
92
+ def get_username(self, user_id: Any, debug: bool = False) -> Any:
93
+ """
94
+ Return the username from the given ID.
95
+ """
96
+ users_pipe = self.get_users_pipe()
97
+ return users_pipe.get_value('username', {'user_id': user_id}, debug=debug)
98
+
99
+
100
+ def get_users(
101
+ self,
102
+ debug: bool = False,
103
+ **kw: Any
104
+ ) -> List[str]:
105
+ """
106
+ Get the registered usernames.
107
+ """
108
+ users_pipe = self.get_users_pipe()
109
+ df = users_pipe.get_data()
110
+ if df is None:
111
+ return []
112
+
113
+ return list(df['username'])
114
+
115
+
116
+ def edit_user(self, user: User, debug: bool = False) -> mrsm.SuccessTuple:
117
+ """
118
+ Edit the attributes for an existing user.
119
+ """
120
+ users_pipe = self.get_users_pipe()
121
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
122
+
123
+ doc = {'user_id': user_id}
124
+ if user.email != '':
125
+ doc['email'] = user.email
126
+ if user.password_hash != '':
127
+ doc['password_hash'] = user.password_hash
128
+ if user.type != '':
129
+ doc['user_type'] = user.type
130
+ if user.attributes:
131
+ doc['attributes'] = user.attributes
132
+
133
+ sync_success, sync_msg = users_pipe.sync([doc], debug=debug)
134
+ if not sync_success:
135
+ return False, f"Failed to edit user '{user.username}':\n{sync_msg}"
136
+
137
+ return True, "Success"
138
+
139
+
140
+ def delete_user(self, user: User, debug: bool = False) -> mrsm.SuccessTuple:
141
+ """
142
+ Delete a user from the users table.
143
+ """
144
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
145
+ users_pipe = self.get_users_pipe()
146
+ clear_success, clear_msg = users_pipe.clear(params={'user_id': user_id}, debug=debug)
147
+ if not clear_success:
148
+ return False, f"Failed to delete user '{user}':\n{clear_msg}"
149
+ return True, "Success"
150
+
151
+
152
+ def get_user_password_hash(self, user: User, debug: bool = False) -> Union[uuid.UUID, None]:
153
+ """
154
+ Get a user's password hash from the users table.
155
+ """
156
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
157
+ users_pipe = self.get_users_pipe()
158
+ result_df = users_pipe.get_data(['password_hash'], params={'user_id': user_id}, debug=debug)
159
+ if result_df is None or len(result_df) == 0:
160
+ return None
161
+
162
+ return result_df['password_hash'][0]
163
+
164
+
165
+ def get_user_type(self, user: User, debug: bool = False) -> Union[str, None]:
166
+ """
167
+ Get a user's type from the users table.
168
+ """
169
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
170
+ users_pipe = self.get_users_pipe()
171
+ result_df = users_pipe.get_data(['user_type'], params={'user_id': user_id}, debug=debug)
172
+ if result_df is None or len(result_df) == 0:
173
+ return None
174
+
175
+ return result_df['user_type'][0]
176
+
177
+
178
+ def get_user_attributes(self, user: User, debug: bool = False) -> Union[Dict[str, Any], None]:
179
+ """
180
+ Get a user's attributes from the users table.
181
+ """
182
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
183
+ users_pipe = self.get_users_pipe()
184
+ result_df = users_pipe.get_data(['attributes'], params={'user_id': user_id}, debug=debug)
185
+ if result_df is None or len(result_df) == 0:
186
+ return None
187
+
188
+ return result_df['attributes'][0]
@@ -56,7 +56,7 @@ def parse_connector_keys(
56
56
  import copy
57
57
  from meerschaum.connectors import get_connector
58
58
  from meerschaum.config import get_config
59
- from meerschaum.config.static import STATIC_CONFIG
59
+ from meerschaum._internal.static import STATIC_CONFIG
60
60
  from meerschaum.utils.warnings import error
61
61
 
62
62
  ### `get_connector()` handles the logic for falling back to 'main',
@@ -111,11 +111,14 @@ def parse_repo_keys(keys: Optional[str] = None, **kw):
111
111
  """Parse the Meerschaum repository value into an APIConnector."""
112
112
  from meerschaum.config import get_config
113
113
  if keys is None:
114
- keys = get_config('meerschaum', 'default_repository', patch=True)
114
+ keys = get_config('meerschaum', 'repository', patch=True)
115
115
  keys = str(keys)
116
116
  if ':' not in keys:
117
117
  keys = 'api:' + keys
118
118
 
119
+ if not keys.startswith('api:'):
120
+ raise ValueError("Only APIConnectors may be treated as repositories.")
121
+
119
122
  return parse_connector_keys(keys, **kw)
120
123
 
121
124
 
@@ -7,14 +7,16 @@ Interface with SQL servers using sqlalchemy.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
11
+ import pathlib
10
12
  import meerschaum as mrsm
11
13
  from meerschaum.utils.typing import Optional, Any, Union
12
14
 
13
- from meerschaum.connectors import Connector
15
+ from meerschaum.connectors import InstanceConnector
14
16
  from meerschaum.utils.warnings import error, warn
15
17
 
16
18
 
17
- class SQLConnector(Connector):
19
+ class SQLConnector(InstanceConnector):
18
20
  """
19
21
  Connect to SQL databases via `sqlalchemy`.
20
22
 
@@ -24,8 +26,6 @@ class SQLConnector(Connector):
24
26
 
25
27
  """
26
28
 
27
- IS_INSTANCE: bool = True
28
-
29
29
  from ._create_engine import flavor_configs, create_engine
30
30
  from ._sql import (
31
31
  read,
@@ -75,6 +75,7 @@ class SQLConnector(Connector):
75
75
  get_pipe_index_names,
76
76
  )
77
77
  from ._plugins import (
78
+ get_plugins_pipe,
78
79
  register_plugin,
79
80
  delete_plugin,
80
81
  get_plugin_id,
@@ -85,6 +86,7 @@ class SQLConnector(Connector):
85
86
  get_plugin_attributes,
86
87
  )
87
88
  from ._users import (
89
+ get_users_pipe,
88
90
  register_user,
89
91
  get_user_id,
90
92
  get_users,
@@ -151,6 +153,9 @@ class SQLConnector(Connector):
151
153
  if uri.startswith('timescaledb://'):
152
154
  uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
153
155
  flavor = 'timescaledb'
156
+ if uri.startswith('timescaledb-ha://'):
157
+ uri = uri.replace('timescaledb-ha://', 'postgresql+psycopg://', 1)
158
+ flavor = 'timescaledb-ha'
154
159
  if uri.startswith('postgis://'):
155
160
  uri = uri.replace('postgis://', 'postgresql+psycopg://', 1)
156
161
  flavor = 'postgis'
@@ -313,7 +318,7 @@ class SQLConnector(Connector):
313
318
  """
314
319
  Return the schema name for internal tables.
315
320
  """
316
- from meerschaum.config.static import STATIC_CONFIG
321
+ from meerschaum._internal.static import STATIC_CONFIG
317
322
  from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
318
323
  schema_name = self.__dict__.get('internal_schema', None) or (
319
324
  STATIC_CONFIG['sql']['internal_schema']
@@ -373,6 +378,18 @@ class SQLConnector(Connector):
373
378
  self.__dict__['schema'] = _schema
374
379
  return _schema
375
380
 
381
+ def get_metadata_cache_path(self, kind: str = 'json') -> pathlib.Path:
382
+ """
383
+ Return the path to the file to which to write metadata cache.
384
+ """
385
+ from meerschaum.config.paths import SQL_CONN_CACHE_RESOURCES_PATH
386
+ filename = (
387
+ f'{self.label}-metadata.pkl'
388
+ if kind == 'pkl'
389
+ else f'{self.label}.json'
390
+ )
391
+ return SQL_CONN_CACHE_RESOURCES_PATH / filename
392
+
376
393
  def __getstate__(self):
377
394
  return self.__dict__
378
395
 
@@ -14,17 +14,18 @@ import copy
14
14
  from meerschaum.utils.typing import SuccessTuple
15
15
 
16
16
  flavor_clis = {
17
- 'postgresql' : 'pgcli',
18
- 'postgis' : 'pgcli',
19
- 'timescaledb' : 'pgcli',
20
- 'cockroachdb' : 'pgcli',
21
- 'citus' : 'pgcli',
22
- 'mysql' : 'mycli',
23
- 'mariadb' : 'mycli',
24
- 'percona' : 'mycli',
25
- 'sqlite' : 'litecli',
26
- 'mssql' : 'mssqlcli',
27
- 'duckdb' : 'gadwall',
17
+ 'postgresql' : 'pgcli',
18
+ 'postgis' : 'pgcli',
19
+ 'timescaledb' : 'pgcli',
20
+ 'timescaledb-ha' : 'pgcli',
21
+ 'cockroachdb' : 'pgcli',
22
+ 'citus' : 'pgcli',
23
+ 'mysql' : 'mycli',
24
+ 'mariadb' : 'mycli',
25
+ 'percona' : 'mycli',
26
+ 'sqlite' : 'litecli',
27
+ 'mssql' : 'mssqlcli',
28
+ 'duckdb' : 'gadwall',
28
29
  }
29
30
  cli_deps = {
30
31
  'pgcli': ['pgspecial', 'pendulum', 'cli_helpers'],