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.
- meerschaum/__init__.py +5 -2
- meerschaum/_internal/__init__.py +1 -0
- meerschaum/_internal/arguments/_parse_arguments.py +4 -4
- meerschaum/_internal/arguments/_parser.py +33 -4
- meerschaum/_internal/cli/__init__.py +6 -0
- meerschaum/_internal/cli/daemons.py +103 -0
- meerschaum/_internal/cli/entry.py +220 -0
- meerschaum/_internal/cli/workers.py +435 -0
- meerschaum/_internal/docs/index.py +48 -2
- meerschaum/_internal/entry.py +50 -14
- meerschaum/_internal/shell/Shell.py +121 -29
- meerschaum/_internal/shell/__init__.py +4 -1
- meerschaum/_internal/static.py +359 -0
- meerschaum/_internal/term/TermPageHandler.py +1 -2
- meerschaum/_internal/term/__init__.py +40 -6
- meerschaum/_internal/term/tools.py +33 -8
- meerschaum/actions/__init__.py +6 -4
- meerschaum/actions/api.py +53 -13
- meerschaum/actions/attach.py +1 -0
- meerschaum/actions/bootstrap.py +8 -8
- meerschaum/actions/delete.py +4 -2
- meerschaum/actions/edit.py +171 -25
- meerschaum/actions/login.py +8 -8
- meerschaum/actions/register.py +143 -6
- meerschaum/actions/reload.py +22 -5
- meerschaum/actions/restart.py +14 -0
- meerschaum/actions/show.py +184 -31
- meerschaum/actions/start.py +166 -17
- meerschaum/actions/stop.py +38 -2
- meerschaum/actions/sync.py +7 -2
- meerschaum/actions/tag.py +9 -8
- meerschaum/actions/verify.py +5 -8
- meerschaum/api/__init__.py +45 -15
- meerschaum/api/_events.py +46 -4
- meerschaum/api/_oauth2.py +162 -9
- meerschaum/api/_tokens.py +102 -0
- meerschaum/api/dash/__init__.py +0 -3
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/custom.py +4 -3
- meerschaum/api/dash/callbacks/dashboard.py +228 -117
- meerschaum/api/dash/callbacks/jobs.py +14 -7
- meerschaum/api/dash/callbacks/login.py +10 -1
- meerschaum/api/dash/callbacks/pipes.py +194 -14
- meerschaum/api/dash/callbacks/plugins.py +0 -1
- meerschaum/api/dash/callbacks/register.py +10 -3
- meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
- meerschaum/api/dash/callbacks/tokens.py +389 -0
- meerschaum/api/dash/components.py +36 -15
- meerschaum/api/dash/jobs.py +1 -1
- meerschaum/api/dash/keys.py +35 -93
- meerschaum/api/dash/pages/__init__.py +2 -1
- meerschaum/api/dash/pages/dashboard.py +1 -20
- meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
- meerschaum/api/dash/pages/login.py +2 -2
- meerschaum/api/dash/pages/pipes.py +16 -5
- meerschaum/api/dash/pages/settings/password_reset.py +1 -1
- meerschaum/api/dash/pages/tokens.py +53 -0
- meerschaum/api/dash/pipes.py +438 -88
- meerschaum/api/dash/sessions.py +12 -0
- meerschaum/api/dash/tokens.py +603 -0
- meerschaum/api/dash/websockets.py +1 -1
- meerschaum/api/dash/webterm.py +18 -6
- meerschaum/api/models/__init__.py +23 -3
- meerschaum/api/models/_actions.py +22 -0
- meerschaum/api/models/_pipes.py +91 -7
- meerschaum/api/models/_tokens.py +81 -0
- meerschaum/api/resources/static/css/dash.css +16 -0
- meerschaum/api/resources/static/js/terminado.js +3 -0
- meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
- meerschaum/api/resources/templates/termpage.html +13 -0
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +3 -4
- meerschaum/api/routes/_connectors.py +3 -7
- meerschaum/api/routes/_jobs.py +26 -35
- meerschaum/api/routes/_login.py +120 -15
- meerschaum/api/routes/_misc.py +5 -10
- meerschaum/api/routes/_pipes.py +178 -143
- meerschaum/api/routes/_plugins.py +38 -28
- meerschaum/api/routes/_tokens.py +236 -0
- meerschaum/api/routes/_users.py +47 -35
- meerschaum/api/routes/_version.py +3 -3
- meerschaum/api/routes/_webterm.py +3 -3
- meerschaum/config/__init__.py +100 -30
- meerschaum/config/_default.py +132 -64
- meerschaum/config/_edit.py +38 -32
- meerschaum/config/_formatting.py +2 -0
- meerschaum/config/_patch.py +10 -8
- meerschaum/config/_paths.py +133 -13
- meerschaum/config/_read_config.py +87 -36
- meerschaum/config/_sync.py +6 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/environment.py +262 -0
- meerschaum/config/stack/__init__.py +37 -15
- meerschaum/config/static.py +18 -0
- meerschaum/connectors/_Connector.py +11 -6
- meerschaum/connectors/__init__.py +41 -22
- meerschaum/connectors/api/_APIConnector.py +34 -6
- meerschaum/connectors/api/_actions.py +2 -2
- meerschaum/connectors/api/_jobs.py +12 -1
- meerschaum/connectors/api/_login.py +33 -7
- meerschaum/connectors/api/_misc.py +2 -2
- meerschaum/connectors/api/_pipes.py +23 -32
- meerschaum/connectors/api/_plugins.py +2 -2
- meerschaum/connectors/api/_request.py +1 -1
- meerschaum/connectors/api/_tokens.py +146 -0
- meerschaum/connectors/api/_users.py +70 -58
- meerschaum/connectors/instance/_InstanceConnector.py +83 -0
- meerschaum/connectors/instance/__init__.py +10 -0
- meerschaum/connectors/instance/_pipes.py +442 -0
- meerschaum/connectors/instance/_plugins.py +159 -0
- meerschaum/connectors/instance/_tokens.py +317 -0
- meerschaum/connectors/instance/_users.py +188 -0
- meerschaum/connectors/parse.py +5 -2
- meerschaum/connectors/sql/_SQLConnector.py +22 -5
- meerschaum/connectors/sql/_cli.py +12 -11
- meerschaum/connectors/sql/_create_engine.py +12 -168
- meerschaum/connectors/sql/_fetch.py +2 -18
- meerschaum/connectors/sql/_pipes.py +295 -278
- meerschaum/connectors/sql/_plugins.py +29 -0
- meerschaum/connectors/sql/_sql.py +47 -22
- meerschaum/connectors/sql/_users.py +36 -2
- meerschaum/connectors/sql/tables/__init__.py +254 -122
- meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
- meerschaum/connectors/valkey/_pipes.py +60 -31
- meerschaum/connectors/valkey/_plugins.py +2 -26
- meerschaum/core/Pipe/__init__.py +115 -85
- meerschaum/core/Pipe/_attributes.py +425 -124
- meerschaum/core/Pipe/_bootstrap.py +54 -24
- meerschaum/core/Pipe/_cache.py +555 -0
- meerschaum/core/Pipe/_clear.py +0 -11
- meerschaum/core/Pipe/_data.py +96 -68
- meerschaum/core/Pipe/_deduplicate.py +0 -13
- meerschaum/core/Pipe/_delete.py +12 -21
- meerschaum/core/Pipe/_drop.py +11 -23
- meerschaum/core/Pipe/_dtypes.py +49 -19
- meerschaum/core/Pipe/_edit.py +14 -4
- meerschaum/core/Pipe/_fetch.py +1 -1
- meerschaum/core/Pipe/_index.py +8 -14
- meerschaum/core/Pipe/_show.py +5 -5
- meerschaum/core/Pipe/_sync.py +123 -204
- meerschaum/core/Pipe/_verify.py +4 -4
- meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
- meerschaum/core/Plugin/__init__.py +1 -1
- meerschaum/core/Token/_Token.py +220 -0
- meerschaum/core/Token/__init__.py +12 -0
- meerschaum/core/User/_User.py +35 -10
- meerschaum/core/User/__init__.py +9 -1
- meerschaum/core/__init__.py +1 -0
- meerschaum/jobs/_Executor.py +88 -4
- meerschaum/jobs/_Job.py +149 -38
- meerschaum/jobs/__init__.py +3 -2
- meerschaum/jobs/systemd.py +8 -3
- meerschaum/models/__init__.py +35 -0
- meerschaum/models/pipes.py +247 -0
- meerschaum/models/tokens.py +38 -0
- meerschaum/models/users.py +26 -0
- meerschaum/plugins/__init__.py +301 -88
- meerschaum/plugins/bootstrap.py +510 -4
- meerschaum/utils/_get_pipes.py +97 -30
- meerschaum/utils/daemon/Daemon.py +199 -43
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
- meerschaum/utils/daemon/RotatingFile.py +63 -36
- meerschaum/utils/daemon/StdinFile.py +53 -13
- meerschaum/utils/daemon/__init__.py +47 -6
- meerschaum/utils/daemon/_names.py +6 -3
- meerschaum/utils/dataframe.py +480 -82
- meerschaum/utils/debug.py +49 -19
- meerschaum/utils/dtypes/__init__.py +478 -37
- meerschaum/utils/dtypes/sql.py +369 -29
- meerschaum/utils/formatting/__init__.py +5 -2
- meerschaum/utils/formatting/_jobs.py +1 -1
- meerschaum/utils/formatting/_pipes.py +52 -50
- meerschaum/utils/formatting/_pprint.py +1 -0
- meerschaum/utils/formatting/_shell.py +44 -18
- meerschaum/utils/misc.py +268 -186
- meerschaum/utils/packages/__init__.py +25 -40
- meerschaum/utils/packages/_packages.py +42 -34
- meerschaum/utils/pipes.py +213 -0
- meerschaum/utils/process.py +2 -2
- meerschaum/utils/prompt.py +175 -144
- meerschaum/utils/schedule.py +2 -1
- meerschaum/utils/sql.py +135 -49
- meerschaum/utils/threading.py +42 -0
- meerschaum/utils/typing.py +1 -4
- meerschaum/utils/venv/_Venv.py +2 -2
- meerschaum/utils/venv/__init__.py +7 -7
- meerschaum/utils/warnings.py +19 -13
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
- meerschaum-3.0.0.dist-info/RECORD +289 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
- meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
- meerschaum/api/models/_interfaces.py +0 -15
- meerschaum/api/models/_locations.py +0 -15
- meerschaum/api/models/_metrics.py +0 -15
- meerschaum/config/_environment.py +0 -145
- meerschaum/config/static/__init__.py +0 -186
- meerschaum-2.9.4.dist-info/RECORD +0 -263
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.4.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]
|
meerschaum/connectors/parse.py
CHANGED
@@ -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.
|
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', '
|
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
|
15
|
+
from meerschaum.connectors import InstanceConnector
|
14
16
|
from meerschaum.utils.warnings import error, warn
|
15
17
|
|
16
18
|
|
17
|
-
class SQLConnector(
|
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.
|
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'
|
18
|
-
'postgis'
|
19
|
-
'timescaledb'
|
20
|
-
'
|
21
|
-
'
|
22
|
-
'
|
23
|
-
'
|
24
|
-
'
|
25
|
-
'
|
26
|
-
'
|
27
|
-
'
|
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'],
|