meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc2__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 (158) 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 +19 -2
  5. meerschaum/_internal/docs/index.py +49 -2
  6. meerschaum/_internal/entry.py +6 -6
  7. meerschaum/_internal/shell/Shell.py +1 -1
  8. meerschaum/_internal/static.py +356 -0
  9. meerschaum/actions/api.py +12 -2
  10. meerschaum/actions/bootstrap.py +7 -7
  11. meerschaum/actions/edit.py +142 -18
  12. meerschaum/actions/register.py +137 -6
  13. meerschaum/actions/show.py +117 -29
  14. meerschaum/actions/stop.py +4 -1
  15. meerschaum/actions/sync.py +1 -1
  16. meerschaum/actions/tag.py +9 -8
  17. meerschaum/actions/verify.py +5 -8
  18. meerschaum/api/__init__.py +11 -3
  19. meerschaum/api/_events.py +39 -2
  20. meerschaum/api/_oauth2.py +118 -8
  21. meerschaum/api/_tokens.py +102 -0
  22. meerschaum/api/dash/__init__.py +0 -3
  23. meerschaum/api/dash/callbacks/custom.py +2 -2
  24. meerschaum/api/dash/callbacks/dashboard.py +103 -19
  25. meerschaum/api/dash/callbacks/plugins.py +0 -1
  26. meerschaum/api/dash/callbacks/register.py +1 -1
  27. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  28. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  29. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  30. meerschaum/api/dash/components.py +30 -8
  31. meerschaum/api/dash/keys.py +19 -93
  32. meerschaum/api/dash/pages/dashboard.py +1 -20
  33. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  34. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  35. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  36. meerschaum/api/dash/pipes.py +94 -59
  37. meerschaum/api/dash/sessions.py +12 -0
  38. meerschaum/api/dash/tokens.py +606 -0
  39. meerschaum/api/dash/websockets.py +1 -1
  40. meerschaum/api/dash/webterm.py +4 -0
  41. meerschaum/api/models/__init__.py +23 -3
  42. meerschaum/api/models/_actions.py +22 -0
  43. meerschaum/api/models/_pipes.py +85 -7
  44. meerschaum/api/models/_tokens.py +81 -0
  45. meerschaum/api/resources/templates/termpage.html +12 -0
  46. meerschaum/api/routes/__init__.py +1 -0
  47. meerschaum/api/routes/_actions.py +3 -4
  48. meerschaum/api/routes/_connectors.py +3 -7
  49. meerschaum/api/routes/_jobs.py +14 -35
  50. meerschaum/api/routes/_login.py +49 -12
  51. meerschaum/api/routes/_misc.py +5 -10
  52. meerschaum/api/routes/_pipes.py +173 -140
  53. meerschaum/api/routes/_plugins.py +38 -28
  54. meerschaum/api/routes/_tokens.py +236 -0
  55. meerschaum/api/routes/_users.py +47 -35
  56. meerschaum/api/routes/_version.py +3 -3
  57. meerschaum/config/__init__.py +43 -20
  58. meerschaum/config/_default.py +43 -6
  59. meerschaum/config/_edit.py +28 -24
  60. meerschaum/config/_environment.py +1 -1
  61. meerschaum/config/_patch.py +6 -6
  62. meerschaum/config/_paths.py +5 -1
  63. meerschaum/config/_read_config.py +65 -34
  64. meerschaum/config/_sync.py +6 -3
  65. meerschaum/config/_version.py +1 -1
  66. meerschaum/config/stack/__init__.py +31 -11
  67. meerschaum/config/static.py +18 -0
  68. meerschaum/connectors/_Connector.py +10 -4
  69. meerschaum/connectors/__init__.py +4 -20
  70. meerschaum/connectors/api/_APIConnector.py +34 -6
  71. meerschaum/connectors/api/_actions.py +2 -2
  72. meerschaum/connectors/api/_jobs.py +1 -1
  73. meerschaum/connectors/api/_login.py +33 -7
  74. meerschaum/connectors/api/_misc.py +2 -2
  75. meerschaum/connectors/api/_pipes.py +16 -31
  76. meerschaum/connectors/api/_plugins.py +2 -2
  77. meerschaum/connectors/api/_request.py +1 -1
  78. meerschaum/connectors/api/_tokens.py +146 -0
  79. meerschaum/connectors/api/_users.py +70 -58
  80. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  81. meerschaum/connectors/instance/__init__.py +10 -0
  82. meerschaum/connectors/instance/_pipes.py +442 -0
  83. meerschaum/connectors/instance/_plugins.py +151 -0
  84. meerschaum/connectors/instance/_tokens.py +296 -0
  85. meerschaum/connectors/instance/_users.py +181 -0
  86. meerschaum/connectors/parse.py +4 -1
  87. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  88. meerschaum/connectors/sql/_cli.py +12 -11
  89. meerschaum/connectors/sql/_create_engine.py +9 -168
  90. meerschaum/connectors/sql/_fetch.py +2 -18
  91. meerschaum/connectors/sql/_pipes.py +156 -190
  92. meerschaum/connectors/sql/_plugins.py +29 -0
  93. meerschaum/connectors/sql/_sql.py +46 -21
  94. meerschaum/connectors/sql/_users.py +29 -2
  95. meerschaum/connectors/sql/tables/__init__.py +1 -1
  96. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  97. meerschaum/connectors/valkey/_pipes.py +53 -26
  98. meerschaum/connectors/valkey/_plugins.py +2 -26
  99. meerschaum/core/Pipe/__init__.py +59 -19
  100. meerschaum/core/Pipe/_attributes.py +412 -90
  101. meerschaum/core/Pipe/_bootstrap.py +54 -24
  102. meerschaum/core/Pipe/_data.py +96 -18
  103. meerschaum/core/Pipe/_dtypes.py +48 -18
  104. meerschaum/core/Pipe/_edit.py +14 -4
  105. meerschaum/core/Pipe/_fetch.py +1 -1
  106. meerschaum/core/Pipe/_show.py +5 -5
  107. meerschaum/core/Pipe/_sync.py +118 -193
  108. meerschaum/core/Pipe/_verify.py +4 -4
  109. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  110. meerschaum/core/Plugin/__init__.py +1 -1
  111. meerschaum/core/Token/_Token.py +220 -0
  112. meerschaum/core/Token/__init__.py +12 -0
  113. meerschaum/core/User/_User.py +34 -8
  114. meerschaum/core/User/__init__.py +9 -1
  115. meerschaum/core/__init__.py +1 -0
  116. meerschaum/jobs/_Job.py +3 -2
  117. meerschaum/jobs/__init__.py +3 -2
  118. meerschaum/jobs/systemd.py +1 -1
  119. meerschaum/models/__init__.py +35 -0
  120. meerschaum/models/pipes.py +247 -0
  121. meerschaum/models/tokens.py +38 -0
  122. meerschaum/models/users.py +26 -0
  123. meerschaum/plugins/__init__.py +22 -7
  124. meerschaum/plugins/bootstrap.py +2 -1
  125. meerschaum/utils/_get_pipes.py +68 -27
  126. meerschaum/utils/daemon/Daemon.py +2 -1
  127. meerschaum/utils/daemon/__init__.py +30 -2
  128. meerschaum/utils/dataframe.py +473 -81
  129. meerschaum/utils/debug.py +15 -15
  130. meerschaum/utils/dtypes/__init__.py +473 -34
  131. meerschaum/utils/dtypes/sql.py +368 -28
  132. meerschaum/utils/formatting/__init__.py +1 -1
  133. meerschaum/utils/formatting/_pipes.py +5 -4
  134. meerschaum/utils/formatting/_shell.py +11 -9
  135. meerschaum/utils/misc.py +246 -148
  136. meerschaum/utils/packages/__init__.py +10 -27
  137. meerschaum/utils/packages/_packages.py +41 -34
  138. meerschaum/utils/pipes.py +181 -0
  139. meerschaum/utils/process.py +1 -1
  140. meerschaum/utils/prompt.py +3 -1
  141. meerschaum/utils/schedule.py +2 -1
  142. meerschaum/utils/sql.py +121 -44
  143. meerschaum/utils/typing.py +1 -4
  144. meerschaum/utils/venv/_Venv.py +2 -2
  145. meerschaum/utils/venv/__init__.py +5 -7
  146. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/METADATA +92 -96
  147. meerschaum-3.0.0rc2.dist-info/RECORD +283 -0
  148. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/WHEEL +1 -1
  149. meerschaum-3.0.0rc2.dist-info/licenses/NOTICE +2 -0
  150. meerschaum/api/models/_interfaces.py +0 -15
  151. meerschaum/api/models/_locations.py +0 -15
  152. meerschaum/api/models/_metrics.py +0 -15
  153. meerschaum/config/static/__init__.py +0 -186
  154. meerschaum-2.9.5.dist-info/RECORD +0 -263
  155. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  156. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  157. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/top_level.txt +0 -0
  158. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/zip-safe +0 -0
@@ -0,0 +1,296 @@
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
+
19
+
20
+ def get_tokens_pipe(self) -> mrsm.Pipe:
21
+ """
22
+ Return the internal pipe for tokens management.
23
+ """
24
+ users_pipe = self.get_users_pipe()
25
+ user_id_dtype = users_pipe.dtypes.get('user_id', 'uuid')
26
+
27
+ return mrsm.Pipe(
28
+ 'mrsm', 'tokens',
29
+ instance=self,
30
+ target='mrsm_tokens',
31
+ temporary=True,
32
+ static=True,
33
+ autotime=True,
34
+ null_indices=True,
35
+ columns={
36
+ 'datetime': 'creation',
37
+ 'primary': 'id',
38
+ },
39
+ indices={
40
+ 'unique': 'label',
41
+ 'user_id': 'user_id',
42
+ },
43
+ dtypes={
44
+ 'id': 'uuid',
45
+ 'creation': 'datetime',
46
+ 'expiration': 'datetime',
47
+ 'is_valid': 'bool',
48
+ 'label': 'string',
49
+ 'user_id': user_id_dtype,
50
+ 'scopes': 'json',
51
+ 'secret_hash': 'string',
52
+ },
53
+ )
54
+
55
+
56
+ def register_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
57
+ """
58
+ Register the new token to the tokens table.
59
+ """
60
+ token_id, token_secret = token.generate_credentials()
61
+ tokens_pipe = self.get_tokens_pipe()
62
+ user_id = self.get_user_id(token.user) if token.user is not None else None
63
+ if user_id is None:
64
+ raise ValueError("Cannot register a token without a user.")
65
+
66
+ doc = {
67
+ 'id': token_id,
68
+ 'user_id': user_id,
69
+ 'creation': datetime.now(timezone.utc),
70
+ 'expiration': token.expiration,
71
+ 'label': token.label,
72
+ 'is_valid': token.is_valid,
73
+ 'scopes': list(token.scopes) if token.scopes else [],
74
+ 'secret_hash': hash_password(
75
+ str(token_secret),
76
+ rounds=STATIC_CONFIG['tokens']['hash_rounds']
77
+ ),
78
+ }
79
+ sync_success, sync_msg = tokens_pipe.sync([doc], check_existing=False, debug=debug)
80
+ if not sync_success:
81
+ return False, f"Failed to register token:\n{sync_msg}"
82
+ return True, "Success"
83
+
84
+
85
+ def edit_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
86
+ """
87
+ Persist the token's in-memory state to the tokens pipe.
88
+ """
89
+ if not token.id:
90
+ return False, "Token ID is not set."
91
+
92
+ if not token.exists(debug=debug):
93
+ return False, f"Token {token.id} does not exist."
94
+
95
+ if not token.creation:
96
+ token_model = self.get_token_model(token.id)
97
+ token.creation = token_model.creation
98
+
99
+ tokens_pipe = self.get_tokens_pipe()
100
+ doc = {
101
+ 'id': token.id,
102
+ 'creation': token.creation,
103
+ 'expiration': token.expiration,
104
+ 'label': token.label,
105
+ 'is_valid': token.is_valid,
106
+ 'scopes': list(token.scopes) if token.scopes else [],
107
+ }
108
+ sync_success, sync_msg = tokens_pipe.sync([doc], debug=debug)
109
+ if not sync_success:
110
+ return False, f"Failed to edit token '{token.id}':\n{sync_msg}"
111
+
112
+ return True, "Success"
113
+
114
+
115
+ def invalidate_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
116
+ """
117
+ Set `is_valid` to `False` for the given token.
118
+ """
119
+ if not token.id:
120
+ return False, "Token ID is not set."
121
+
122
+ if not token.exists(debug=debug):
123
+ return False, f"Token {token.id} does not exist."
124
+
125
+ if not token.creation:
126
+ token_model = self.get_token_model(token.id)
127
+ token.creation = token_model.creation
128
+
129
+ token.is_valid = False
130
+ tokens_pipe = self.get_tokens_pipe()
131
+ doc = {
132
+ 'id': token.id,
133
+ 'creation': token.creation,
134
+ 'is_valid': False,
135
+ }
136
+ sync_success, sync_msg = tokens_pipe.sync([doc], debug=debug)
137
+ if not sync_success:
138
+ return False, f"Failed to invalidate token '{token.id}':\n{sync_msg}"
139
+
140
+ return True, "Success"
141
+
142
+
143
+ def delete_token(self, token: Token, debug: bool = False) -> mrsm.SuccessTuple:
144
+ """
145
+ Delete the given token from the tokens table.
146
+ """
147
+ if not token.id:
148
+ return False, "Token ID is not set."
149
+
150
+ if not token.exists(debug=debug):
151
+ return False, f"Token {token.id} does not exist."
152
+
153
+ if not token.creation:
154
+ token_model = self.get_token_model(token.id)
155
+ token.creation = token_model.creation
156
+
157
+ token.is_valid = False
158
+ tokens_pipe = self.get_tokens_pipe()
159
+ clear_success, clear_msg = tokens_pipe.clear(params={'id': token.id}, debug=debug)
160
+ if not clear_success:
161
+ return False, f"Failed to delete token '{token.id}':\n{clear_msg}"
162
+
163
+ return True, "Success"
164
+
165
+
166
+ def get_tokens(
167
+ self,
168
+ user: Optional[User] = None,
169
+ labels: Optional[List[str]] = None,
170
+ ids: Optional[List[uuid.UUID]] = None,
171
+ debug: bool = False,
172
+ ) -> List[Token]:
173
+ """
174
+ Return a list of `Token` objects.
175
+ """
176
+ tokens_pipe = self.get_tokens_pipe()
177
+ user_id = (
178
+ self.get_user_id(user, debug=debug)
179
+ if user is not None
180
+ else None
181
+ )
182
+ user_type = self.get_user_type(user, debug=debug) if user is not None else None
183
+ params = (
184
+ {
185
+ 'user_id': (
186
+ user_id
187
+ if user_type != 'admin'
188
+ else [user_id, None]
189
+ )
190
+ }
191
+ if user_id is not None
192
+ else {}
193
+ )
194
+ if labels:
195
+ params['label'] = labels
196
+ if ids:
197
+ params['id'] = ids
198
+
199
+ tokens_df = tokens_pipe.get_data(params=params, debug=debug)
200
+ if tokens_df is None:
201
+ return []
202
+
203
+ tokens_docs = tokens_df.to_dict(orient='records')
204
+ return [
205
+ Token(
206
+ instance=self,
207
+ **token_doc
208
+ )
209
+ for token_doc in reversed(tokens_docs)
210
+ ]
211
+
212
+
213
+ def get_token(self, token_id: Union[uuid.UUID, str], debug: bool = False) -> Union[Token, None]:
214
+ """
215
+ Return the `Token` from its ID.
216
+ """
217
+ from meerschaum.utils.misc import is_uuid
218
+ if isinstance(token_id, str):
219
+ if is_uuid(token_id):
220
+ token_id = uuid.UUID(token_id)
221
+ else:
222
+ raise ValueError("Invalid token ID.")
223
+ token_model = self.get_token_model(token_id)
224
+ if token_model is None:
225
+ return None
226
+ return Token(**dict(token_model))
227
+
228
+
229
+ def get_token_model(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> 'Union[TokenModel, None]':
230
+ """
231
+ Return a token's model from the instance.
232
+ """
233
+ from meerschaum.models import TokenModel
234
+ if isinstance(token_id, Token):
235
+ token_id = Token.id
236
+ if not token_id:
237
+ raise ValueError("Invalid token ID.")
238
+ tokens_pipe = self.get_tokens_pipe()
239
+ doc = tokens_pipe.get_doc(
240
+ params={'id': token_id},
241
+ debug=debug,
242
+ )
243
+ if doc is None:
244
+ return None
245
+ return TokenModel(**doc)
246
+
247
+
248
+ def get_token_secret_hash(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> Union[str, None]:
249
+ """
250
+ Return the secret hash for a given token.
251
+ """
252
+ if isinstance(token_id, Token):
253
+ token_id = token_id.id
254
+ if not token_id:
255
+ raise ValueError("Invalid token ID.")
256
+ tokens_pipe = self.get_tokens_pipe()
257
+ return tokens_pipe.get_value('secret_hash', params={'id': token_id}, debug=debug)
258
+
259
+
260
+ def get_token_user_id(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> Union[int, str, uuid.UUID, None]:
261
+ """
262
+ Return a token's user_id.
263
+ """
264
+ if isinstance(token_id, Token):
265
+ token_id = token_id.id
266
+ if not token_id:
267
+ raise ValueError("Invalid token ID.")
268
+
269
+ tokens_pipe = self.get_tokens_pipe()
270
+ return tokens_pipe.get_value('user_id', params={'id': token_id}, debug=debug)
271
+
272
+
273
+ def get_token_scopes(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> List[str]:
274
+ """
275
+ Return the scopes for a token.
276
+ """
277
+ if isinstance(token_id, Token):
278
+ token_id = token_id.id
279
+ if not token_id:
280
+ raise ValueError("Invalid token ID.")
281
+
282
+ tokens_pipe = self.get_tokens_pipe()
283
+ return tokens_pipe.get_value('scopes', params={'id': token_id}, debug=debug) or []
284
+
285
+
286
+ def token_exists(self, token_id: Union[uuid.UUID, Token], debug: bool = False) -> bool:
287
+ """
288
+ Return `True` if a token exists in the tokens pipe.
289
+ """
290
+ if isinstance(token_id, Token):
291
+ token_id = token_id.id
292
+ if not token_id:
293
+ raise ValueError("Invalid token ID.")
294
+
295
+ tokens_pipe = self.get_tokens_pipe()
296
+ return tokens_pipe.get_value('creation', params={'id': token_id}, debug=debug) is not None
@@ -0,0 +1,181 @@
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
+ return mrsm.Pipe(
22
+ 'mrsm', 'users',
23
+ instance=self,
24
+ target='mrsm_users',
25
+ temporary=True,
26
+ static=True,
27
+ null_indices=False,
28
+ columns={
29
+ 'primary': 'user_id',
30
+ },
31
+ dtypes={
32
+ 'user_id': 'uuid',
33
+ 'username': 'string',
34
+ 'password_hash': 'string',
35
+ 'email': 'string',
36
+ 'user_type': 'string',
37
+ 'attributes': 'json',
38
+ },
39
+ indices={
40
+ 'unique': 'username',
41
+ },
42
+ )
43
+
44
+
45
+ def register_user(
46
+ self,
47
+ user: User,
48
+ debug: bool = False,
49
+ **kwargs: Any
50
+ ) -> mrsm.SuccessTuple:
51
+ """
52
+ Register a new user to the users pipe.
53
+ """
54
+ users_pipe = self.get_users_pipe()
55
+ user.user_id = uuid.uuid4()
56
+ sync_success, sync_msg = users_pipe.sync(
57
+ [{
58
+ 'user_id': user.user_id,
59
+ 'username': user.username,
60
+ 'email': user.email,
61
+ 'password_hash': user.password_hash,
62
+ 'user_type': user.type,
63
+ 'attributes': user.attributes,
64
+ }],
65
+ check_existing=False,
66
+ debug=debug,
67
+ )
68
+ if not sync_success:
69
+ return False, f"Failed to register user '{user.username}':\n{sync_msg}"
70
+
71
+ return True, "Success"
72
+
73
+
74
+ def get_user_id(self, user: User, debug: bool = False) -> Union[uuid.UUID, None]:
75
+ """
76
+ Return a user's ID from the username.
77
+ """
78
+ users_pipe = self.get_users_pipe()
79
+ result_df = users_pipe.get_data(['user_id'], params={'username': user.username}, limit=1)
80
+ if result_df is None or len(result_df) == 0:
81
+ return None
82
+ return result_df['user_id'][0]
83
+
84
+
85
+ def get_username(self, user_id: Any, debug: bool = False) -> Any:
86
+ """
87
+ Return the username from the given ID.
88
+ """
89
+ users_pipe = self.get_users_pipe()
90
+ return users_pipe.get_value('username', {'user_id': user_id}, debug=debug)
91
+
92
+
93
+ def get_users(
94
+ self,
95
+ debug: bool = False,
96
+ **kw: Any
97
+ ) -> List[str]:
98
+ """
99
+ Get the registered usernames.
100
+ """
101
+ users_pipe = self.get_users_pipe()
102
+ df = users_pipe.get_data()
103
+ if df is None:
104
+ return []
105
+
106
+ return list(df['username'])
107
+
108
+
109
+ def edit_user(self, user: User, debug: bool = False) -> mrsm.SuccessTuple:
110
+ """
111
+ Edit the attributes for an existing user.
112
+ """
113
+ users_pipe = self.get_users_pipe()
114
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
115
+
116
+ doc = {'user_id': user_id}
117
+ if user.email != '':
118
+ doc['email'] = user.email
119
+ if user.password_hash != '':
120
+ doc['password_hash'] = user.password_hash
121
+ if user.type != '':
122
+ doc['user_type'] = user.type
123
+ if user.attributes:
124
+ doc['attributes'] = user.attributes
125
+
126
+ sync_success, sync_msg = users_pipe.sync([doc], debug=debug)
127
+ if not sync_success:
128
+ return False, f"Failed to edit user '{user.username}':\n{sync_msg}"
129
+
130
+ return True, "Success"
131
+
132
+
133
+ def delete_user(self, user: User, debug: bool = False) -> mrsm.SuccessTuple:
134
+ """
135
+ Delete a user from the users table.
136
+ """
137
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
138
+ users_pipe = self.get_users_pipe()
139
+ clear_success, clear_msg = users_pipe.clear(params={'user_id': user_id}, debug=debug)
140
+ if not clear_success:
141
+ return False, f"Failed to delete user '{user}':\n{clear_msg}"
142
+ return True, "Success"
143
+
144
+
145
+ def get_user_password_hash(self, user: User, debug: bool = False) -> Union[uuid.UUID, None]:
146
+ """
147
+ Get a user's password hash from the users table.
148
+ """
149
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
150
+ users_pipe = self.get_users_pipe()
151
+ result_df = users_pipe.get_data(['password_hash'], params={'user_id': user_id}, debug=debug)
152
+ if result_df is None or len(result_df) == 0:
153
+ return None
154
+
155
+ return result_df['password_hash'][0]
156
+
157
+
158
+ def get_user_type(self, user: User, debug: bool = False) -> Union[str, None]:
159
+ """
160
+ Get a user's type from the users table.
161
+ """
162
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
163
+ users_pipe = self.get_users_pipe()
164
+ result_df = users_pipe.get_data(['user_type'], params={'user_id': user_id}, debug=debug)
165
+ if result_df is None or len(result_df) == 0:
166
+ return None
167
+
168
+ return result_df['user_type'][0]
169
+
170
+
171
+ def get_user_attributes(self, user: User, debug: bool = False) -> Union[Dict[str, Any], None]:
172
+ """
173
+ Get a user's attributes from the users table.
174
+ """
175
+ user_id = user.user_id if user.user_id is not None else self.get_user_id(user, debug=debug)
176
+ users_pipe = self.get_users_pipe()
177
+ result_df = users_pipe.get_data(['attributes'], params={'user_id': user_id}, debug=debug)
178
+ if result_df is None or len(result_df) == 0:
179
+ return None
180
+
181
+ 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',
@@ -116,6 +116,9 @@ def parse_repo_keys(keys: Optional[str] = None, **kw):
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
 
@@ -10,11 +10,11 @@ from __future__ import annotations
10
10
  import meerschaum as mrsm
11
11
  from meerschaum.utils.typing import Optional, Any, Union
12
12
 
13
- from meerschaum.connectors import Connector
13
+ from meerschaum.connectors import InstanceConnector
14
14
  from meerschaum.utils.warnings import error, warn
15
15
 
16
16
 
17
- class SQLConnector(Connector):
17
+ class SQLConnector(InstanceConnector):
18
18
  """
19
19
  Connect to SQL databases via `sqlalchemy`.
20
20
 
@@ -24,8 +24,6 @@ class SQLConnector(Connector):
24
24
 
25
25
  """
26
26
 
27
- IS_INSTANCE: bool = True
28
-
29
27
  from ._create_engine import flavor_configs, create_engine
30
28
  from ._sql import (
31
29
  read,
@@ -75,6 +73,7 @@ class SQLConnector(Connector):
75
73
  get_pipe_index_names,
76
74
  )
77
75
  from ._plugins import (
76
+ get_plugins_pipe,
78
77
  register_plugin,
79
78
  delete_plugin,
80
79
  get_plugin_id,
@@ -85,6 +84,7 @@ class SQLConnector(Connector):
85
84
  get_plugin_attributes,
86
85
  )
87
86
  from ._users import (
87
+ get_users_pipe,
88
88
  register_user,
89
89
  get_user_id,
90
90
  get_users,
@@ -151,6 +151,9 @@ class SQLConnector(Connector):
151
151
  if uri.startswith('timescaledb://'):
152
152
  uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
153
153
  flavor = 'timescaledb'
154
+ if uri.startswith('timescaledb-ha://'):
155
+ uri = uri.replace('timescaledb-ha://', 'postgresql+psycopg://', 1)
156
+ flavor = 'timescaledb-ha'
154
157
  if uri.startswith('postgis://'):
155
158
  uri = uri.replace('postgis://', 'postgresql+psycopg://', 1)
156
159
  flavor = 'postgis'
@@ -313,7 +316,7 @@ class SQLConnector(Connector):
313
316
  """
314
317
  Return the schema name for internal tables.
315
318
  """
316
- from meerschaum.config.static import STATIC_CONFIG
319
+ from meerschaum._internal.static import STATIC_CONFIG
317
320
  from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
318
321
  schema_name = self.__dict__.get('internal_schema', None) or (
319
322
  STATIC_CONFIG['sql']['internal_schema']
@@ -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'],