meerschaum 2.3.6__py3-none-any.whl → 2.4.0.dev1__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/actions/bootstrap.py +36 -10
- meerschaum/actions/copy.py +3 -3
- meerschaum/actions/start.py +13 -14
- meerschaum/api/dash/__init__.py +7 -6
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/dashboard.py +7 -5
- meerschaum/api/dash/callbacks/pipes.py +42 -0
- meerschaum/api/dash/pages/__init__.py +1 -0
- meerschaum/api/dash/pages/pipes.py +16 -0
- meerschaum/api/dash/pipes.py +79 -47
- meerschaum/api/dash/users.py +19 -6
- meerschaum/api/routes/_login.py +4 -4
- meerschaum/api/routes/_pipes.py +3 -3
- meerschaum/config/_default.py +9 -1
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +59 -16
- meerschaum/connectors/Connector.py +19 -13
- meerschaum/connectors/__init__.py +9 -5
- meerschaum/connectors/poll.py +30 -24
- meerschaum/connectors/sql/_pipes.py +126 -154
- meerschaum/connectors/sql/_plugins.py +45 -43
- meerschaum/connectors/sql/_users.py +46 -38
- meerschaum/connectors/valkey/ValkeyConnector.py +535 -0
- meerschaum/connectors/valkey/__init__.py +8 -0
- meerschaum/connectors/valkey/_fetch.py +75 -0
- meerschaum/connectors/valkey/_pipes.py +839 -0
- meerschaum/connectors/valkey/_plugins.py +265 -0
- meerschaum/connectors/valkey/_users.py +305 -0
- meerschaum/core/Pipe/__init__.py +3 -0
- meerschaum/core/Pipe/_attributes.py +1 -2
- meerschaum/core/Pipe/_clear.py +16 -13
- meerschaum/core/Pipe/_copy.py +106 -0
- meerschaum/core/Pipe/_drop.py +4 -4
- meerschaum/core/Pipe/_dtypes.py +14 -14
- meerschaum/core/Pipe/_edit.py +15 -14
- meerschaum/core/Pipe/_sync.py +134 -51
- meerschaum/core/Pipe/_verify.py +11 -11
- meerschaum/core/User/_User.py +14 -12
- meerschaum/plugins/_Plugin.py +17 -13
- meerschaum/utils/_get_pipes.py +14 -20
- meerschaum/utils/dataframe.py +288 -101
- meerschaum/utils/dtypes/__init__.py +31 -6
- meerschaum/utils/dtypes/sql.py +4 -4
- meerschaum/utils/misc.py +3 -3
- meerschaum/utils/packages/_packages.py +1 -0
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/METADATA +3 -1
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/RECORD +53 -44
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/WHEEL +1 -1
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/top_level.txt +0 -0
- {meerschaum-2.3.6.dist-info → meerschaum-2.4.0.dev1.dist-info}/zip-safe +0 -0
@@ -7,14 +7,17 @@ Manage users via the SQL Connector
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
|
+
|
11
|
+
import meerschaum as mrsm
|
10
12
|
from meerschaum.utils.typing import SuccessTuple, Optional, Any, Dict, List, Union
|
11
13
|
|
14
|
+
|
12
15
|
def register_user(
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
self,
|
17
|
+
user: mrsm.core.User,
|
18
|
+
debug: bool = False,
|
19
|
+
**kw: Any
|
20
|
+
) -> SuccessTuple:
|
18
21
|
"""Register a new user."""
|
19
22
|
from meerschaum.utils.warnings import warn, error, info
|
20
23
|
from meerschaum.utils.packages import attempt_import
|
@@ -57,6 +60,7 @@ def register_user(
|
|
57
60
|
return False, f"Failed to register user '{user}'."
|
58
61
|
return True, f"Successfully registered user '{user}'."
|
59
62
|
|
63
|
+
|
60
64
|
def valid_username(username: str) -> SuccessTuple:
|
61
65
|
"""Verify that a given username is valid."""
|
62
66
|
from meerschaum.config.static import STATIC_CONFIG
|
@@ -75,7 +79,7 @@ def valid_username(username: str) -> SuccessTuple:
|
|
75
79
|
if not c.isalnum() and c not in acceptable_chars:
|
76
80
|
fail_reasons.append(
|
77
81
|
(
|
78
|
-
|
82
|
+
"Usernames may only contain alphanumeric characters " +
|
79
83
|
"and the following special characters: "
|
80
84
|
+ str(list(acceptable_chars))
|
81
85
|
)
|
@@ -92,11 +96,11 @@ def valid_username(username: str) -> SuccessTuple:
|
|
92
96
|
|
93
97
|
|
94
98
|
def edit_user(
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
99
|
+
self,
|
100
|
+
user: 'mrsm.core.User',
|
101
|
+
debug: bool = False,
|
102
|
+
**kw: Any
|
103
|
+
) -> SuccessTuple:
|
100
104
|
"""Update an existing user's metadata."""
|
101
105
|
from meerschaum.utils.packages import attempt_import
|
102
106
|
sqlalchemy = attempt_import('sqlalchemy')
|
@@ -145,11 +149,12 @@ def edit_user(
|
|
145
149
|
return False, f"Failed to edit user '{user}'."
|
146
150
|
return True, f"Successfully edited user '{user}'."
|
147
151
|
|
152
|
+
|
148
153
|
def get_user_id(
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
154
|
+
self,
|
155
|
+
user: 'mrsm.core.User',
|
156
|
+
debug: bool = False
|
157
|
+
) -> Optional[int]:
|
153
158
|
"""If a user is registered, return the `user_id`."""
|
154
159
|
### ensure users table exists
|
155
160
|
from meerschaum.utils.packages import attempt_import
|
@@ -168,10 +173,10 @@ def get_user_id(
|
|
168
173
|
return None
|
169
174
|
|
170
175
|
def get_user_attributes(
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
176
|
+
self,
|
177
|
+
user: 'mrsm.core.User',
|
178
|
+
debug: bool = False
|
179
|
+
) -> Union[Dict[str, Any], None]:
|
175
180
|
"""
|
176
181
|
Return the user's attributes.
|
177
182
|
"""
|
@@ -208,10 +213,10 @@ def get_user_attributes(
|
|
208
213
|
return result
|
209
214
|
|
210
215
|
def delete_user(
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
216
|
+
self,
|
217
|
+
user: 'mrsm.core.User',
|
218
|
+
debug: bool = False
|
219
|
+
) -> SuccessTuple:
|
215
220
|
"""Delete a user's record from the users table."""
|
216
221
|
### ensure users table exists
|
217
222
|
from meerschaum.connectors.sql.tables import get_tables
|
@@ -238,11 +243,12 @@ def delete_user(
|
|
238
243
|
|
239
244
|
return True, f"Successfully deleted user '{user}'"
|
240
245
|
|
246
|
+
|
241
247
|
def get_users(
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
248
|
+
self,
|
249
|
+
debug: bool = False,
|
250
|
+
**kw: Any
|
251
|
+
) -> List[str]:
|
246
252
|
"""
|
247
253
|
Get the registered usernames.
|
248
254
|
"""
|
@@ -256,12 +262,13 @@ def get_users(
|
|
256
262
|
|
257
263
|
return list(self.read(query, debug=debug)['username'])
|
258
264
|
|
265
|
+
|
259
266
|
def get_user_password_hash(
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
267
|
+
self,
|
268
|
+
user: 'mrsm.core.User',
|
269
|
+
debug: bool = False,
|
270
|
+
**kw: Any
|
271
|
+
) -> Optional[str]:
|
265
272
|
"""
|
266
273
|
Return the password has for a user.
|
267
274
|
**NOTE**: This may be dangerous and is only allowed if the security settings explicity allow it.
|
@@ -278,7 +285,7 @@ def get_user_password_hash(
|
|
278
285
|
dprint(f"Already given user_id: {user_id}")
|
279
286
|
else:
|
280
287
|
if debug:
|
281
|
-
dprint(
|
288
|
+
dprint("Fetching user_id...")
|
282
289
|
user_id = self.get_user_id(user, debug=debug)
|
283
290
|
|
284
291
|
if user_id is None:
|
@@ -288,12 +295,13 @@ def get_user_password_hash(
|
|
288
295
|
|
289
296
|
return self.value(query, debug=debug)
|
290
297
|
|
298
|
+
|
291
299
|
def get_user_type(
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
300
|
+
self,
|
301
|
+
user: 'mrsm.core.User',
|
302
|
+
debug: bool = False,
|
303
|
+
**kw: Any
|
304
|
+
) -> Optional[str]:
|
297
305
|
"""
|
298
306
|
Return the user's type.
|
299
307
|
"""
|
@@ -0,0 +1,535 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# vim:fenc=utf-8
|
3
|
+
|
4
|
+
"""
|
5
|
+
Define the `ValkeyConnector`.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import shlex
|
9
|
+
import json
|
10
|
+
from datetime import datetime, timezone
|
11
|
+
|
12
|
+
import meerschaum as mrsm
|
13
|
+
from meerschaum.connectors import Connector, make_connector
|
14
|
+
from meerschaum.utils.typing import List, Dict, Any, Iterator, Optional, Union
|
15
|
+
from meerschaum.utils.warnings import warn, dprint
|
16
|
+
from meerschaum.utils.misc import json_serialize_datetime
|
17
|
+
|
18
|
+
|
19
|
+
@make_connector
|
20
|
+
class ValkeyConnector(Connector):
|
21
|
+
"""
|
22
|
+
Manage a Valkey instance.
|
23
|
+
|
24
|
+
Build a `ValkeyConnector` from connection attributes or a URI string.
|
25
|
+
"""
|
26
|
+
IS_INSTANCE: bool = True
|
27
|
+
REQUIRED_ATTRIBUTES: List[str] = ['host']
|
28
|
+
OPTIONAL_ATTRIBUTES: List[str] = [
|
29
|
+
'port', 'username', 'password', 'db', 'socket_timeout',
|
30
|
+
]
|
31
|
+
DEFAULT_ATTRIBUTES: Dict[str, Any] = {
|
32
|
+
'username': 'default',
|
33
|
+
'port': 6379,
|
34
|
+
'db': 0,
|
35
|
+
'socket_timeout': 300,
|
36
|
+
}
|
37
|
+
KEY_SEPARATOR: str = ':'
|
38
|
+
|
39
|
+
from ._pipes import (
|
40
|
+
register_pipe,
|
41
|
+
get_pipe_id,
|
42
|
+
get_pipe_attributes,
|
43
|
+
edit_pipe,
|
44
|
+
pipe_exists,
|
45
|
+
drop_pipe,
|
46
|
+
delete_pipe,
|
47
|
+
get_pipe_data,
|
48
|
+
sync_pipe,
|
49
|
+
get_pipe_columns_types,
|
50
|
+
clear_pipe,
|
51
|
+
get_sync_time,
|
52
|
+
get_pipe_rowcount,
|
53
|
+
fetch_pipes_keys,
|
54
|
+
)
|
55
|
+
from ._fetch import (
|
56
|
+
fetch,
|
57
|
+
)
|
58
|
+
|
59
|
+
from ._users import (
|
60
|
+
get_users_pipe,
|
61
|
+
get_user_key,
|
62
|
+
get_user_keys_vals,
|
63
|
+
register_user,
|
64
|
+
get_user_id,
|
65
|
+
edit_user,
|
66
|
+
get_user_attributes,
|
67
|
+
delete_user,
|
68
|
+
get_users,
|
69
|
+
get_user_password_hash,
|
70
|
+
get_user_type,
|
71
|
+
)
|
72
|
+
from ._plugins import (
|
73
|
+
get_plugins_pipe,
|
74
|
+
get_plugin_key,
|
75
|
+
get_plugin_keys_vals,
|
76
|
+
register_plugin,
|
77
|
+
get_plugin_id,
|
78
|
+
get_plugin_version,
|
79
|
+
get_plugin_user_id,
|
80
|
+
get_plugin_username,
|
81
|
+
get_plugin_attributes,
|
82
|
+
get_plugins,
|
83
|
+
delete_plugin,
|
84
|
+
)
|
85
|
+
|
86
|
+
@property
|
87
|
+
def client(self):
|
88
|
+
"""
|
89
|
+
Return the Valkey client.
|
90
|
+
"""
|
91
|
+
if '_client' in self.__dict__:
|
92
|
+
return self.__dict__['_client']
|
93
|
+
|
94
|
+
valkey = mrsm.attempt_import('valkey')
|
95
|
+
|
96
|
+
if 'uri' in self.__dict__:
|
97
|
+
self._client = valkey.Valkey.from_url(self.__dict__.get('uri'))
|
98
|
+
return self._client
|
99
|
+
|
100
|
+
optional_kwargs = {
|
101
|
+
key: self.__dict__.get(key)
|
102
|
+
for key in self.OPTIONAL_ATTRIBUTES
|
103
|
+
if key in self.__dict__
|
104
|
+
}
|
105
|
+
connection_kwargs = {
|
106
|
+
'host': self.host,
|
107
|
+
**optional_kwargs
|
108
|
+
}
|
109
|
+
|
110
|
+
self._client = valkey.Valkey(**connection_kwargs)
|
111
|
+
return self._client
|
112
|
+
|
113
|
+
@property
|
114
|
+
def URI(self) -> str:
|
115
|
+
"""
|
116
|
+
Return the connection URI for this connector.
|
117
|
+
"""
|
118
|
+
import urllib.parse
|
119
|
+
|
120
|
+
if 'uri' in self.__dict__:
|
121
|
+
return self.__dict__.get('uri')
|
122
|
+
|
123
|
+
uri = "valkey://"
|
124
|
+
if 'username' in self.__dict__:
|
125
|
+
uri += urllib.parse.quote_plus(self.username) + ':'
|
126
|
+
|
127
|
+
if 'password' in self.__dict__:
|
128
|
+
uri += urllib.parse.quote_plus(self.password) + '@'
|
129
|
+
|
130
|
+
if 'host' in self.__dict__:
|
131
|
+
uri += self.host
|
132
|
+
|
133
|
+
if 'port' in self.__dict__:
|
134
|
+
uri += f':{self.port}'
|
135
|
+
|
136
|
+
if 'db' in self.__dict__:
|
137
|
+
uri += f"/{self.db}"
|
138
|
+
|
139
|
+
if 'socket_timeout' in self.__dict__:
|
140
|
+
uri += f"?timeout={self.socket_timeout}s"
|
141
|
+
|
142
|
+
return uri
|
143
|
+
|
144
|
+
def set(self, key: str, value: Any) -> None:
|
145
|
+
"""
|
146
|
+
Set the `key` to `value`.
|
147
|
+
"""
|
148
|
+
return self.client.set(key, value)
|
149
|
+
|
150
|
+
def get(self, key: str) -> Union[str, None]:
|
151
|
+
"""
|
152
|
+
Get the value for `key`.
|
153
|
+
"""
|
154
|
+
val = self.client.get(key)
|
155
|
+
if val is None:
|
156
|
+
return None
|
157
|
+
|
158
|
+
return val.decode('utf-8')
|
159
|
+
|
160
|
+
def test_connection(self) -> bool:
|
161
|
+
"""
|
162
|
+
Return whether a connection may be established.
|
163
|
+
"""
|
164
|
+
return self.client.ping()
|
165
|
+
|
166
|
+
@classmethod
|
167
|
+
def quote_table(cls, table: str) -> str:
|
168
|
+
"""
|
169
|
+
Return a quoted key.
|
170
|
+
"""
|
171
|
+
return shlex.quote(table)
|
172
|
+
|
173
|
+
@classmethod
|
174
|
+
def get_counter_key(cls, table: str) -> str:
|
175
|
+
"""
|
176
|
+
Return the counter key for a given table.
|
177
|
+
"""
|
178
|
+
table_name = cls.quote_table(table)
|
179
|
+
return f"{table_name}:counter"
|
180
|
+
|
181
|
+
def push_df(
|
182
|
+
self,
|
183
|
+
df: 'pd.DataFrame',
|
184
|
+
table: str,
|
185
|
+
datetime_column: Optional[str] = None,
|
186
|
+
debug: bool = False,
|
187
|
+
) -> int:
|
188
|
+
"""
|
189
|
+
Append a pandas DataFrame to a table.
|
190
|
+
|
191
|
+
Parameters
|
192
|
+
----------
|
193
|
+
df: pd.DataFrame
|
194
|
+
The pandas DataFrame to append to the table.
|
195
|
+
|
196
|
+
table: str
|
197
|
+
The "table" name (root key).
|
198
|
+
|
199
|
+
datetime_column: Optional[str], default None
|
200
|
+
If provided, use this key as the datetime index.
|
201
|
+
|
202
|
+
Returns
|
203
|
+
-------
|
204
|
+
The current index counter value (how many docs have been pushed).
|
205
|
+
"""
|
206
|
+
docs_str = df.to_json(
|
207
|
+
date_format='iso',
|
208
|
+
orient='records',
|
209
|
+
date_unit='us',
|
210
|
+
)
|
211
|
+
docs = json.loads(docs_str)
|
212
|
+
return self.push_docs(
|
213
|
+
docs,
|
214
|
+
table,
|
215
|
+
datetime_column=datetime_column,
|
216
|
+
debug=debug,
|
217
|
+
)
|
218
|
+
|
219
|
+
def push_docs(
|
220
|
+
self,
|
221
|
+
docs: List[Dict[str, Any]],
|
222
|
+
table: str,
|
223
|
+
datetime_column: Optional[str] = None,
|
224
|
+
debug: bool = False,
|
225
|
+
) -> int:
|
226
|
+
"""
|
227
|
+
Append a list of documents to a table.
|
228
|
+
|
229
|
+
Parameters
|
230
|
+
----------
|
231
|
+
docs: List[Dict[str, Any]]
|
232
|
+
The docs to be pushed.
|
233
|
+
All keys and values will be coerced into strings.
|
234
|
+
|
235
|
+
table: str
|
236
|
+
The "table" name (root key).
|
237
|
+
|
238
|
+
datetime_column: Optional[str], default None
|
239
|
+
If set, create a sorted set with this datetime column as the index.
|
240
|
+
Otherwise push the docs to a list.
|
241
|
+
|
242
|
+
Returns
|
243
|
+
-------
|
244
|
+
The current index counter value (how many docs have been pushed).
|
245
|
+
"""
|
246
|
+
table_name = self.quote_table(table)
|
247
|
+
datetime_column_key = self.get_datetime_column_key(table)
|
248
|
+
remote_datetime_column = self.get(datetime_column_key)
|
249
|
+
datetime_column = datetime_column or remote_datetime_column
|
250
|
+
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
251
|
+
|
252
|
+
old_len = (
|
253
|
+
self.client.zcard(table_name)
|
254
|
+
if datetime_column
|
255
|
+
else self.client.scard(table_name)
|
256
|
+
)
|
257
|
+
for doc in docs:
|
258
|
+
original_dt_val = (
|
259
|
+
doc[datetime_column]
|
260
|
+
if datetime_column and datetime_column in doc
|
261
|
+
else 0
|
262
|
+
)
|
263
|
+
dt_val = (
|
264
|
+
dateutil_parser.parse(str(original_dt_val))
|
265
|
+
if not isinstance(original_dt_val, int)
|
266
|
+
else int(original_dt_val)
|
267
|
+
) if datetime_column else None
|
268
|
+
ts = (
|
269
|
+
int(dt_val.replace(tzinfo=timezone.utc).timestamp())
|
270
|
+
if isinstance(dt_val, datetime)
|
271
|
+
else int(dt_val)
|
272
|
+
) if datetime_column else None
|
273
|
+
doc_str = json.dumps(
|
274
|
+
doc,
|
275
|
+
default=(lambda x: json_serialize_datetime(x) if hasattr(x, 'tzinfo') else str(x)),
|
276
|
+
separators=(',', ':'),
|
277
|
+
sort_keys=True,
|
278
|
+
)
|
279
|
+
if datetime_column:
|
280
|
+
self.client.zadd(table_name, {doc_str: ts})
|
281
|
+
else:
|
282
|
+
self.client.sadd(table_name, doc_str)
|
283
|
+
|
284
|
+
if datetime_column:
|
285
|
+
self.set(datetime_column_key, datetime_column)
|
286
|
+
new_len = (
|
287
|
+
self.client.zcard(table_name)
|
288
|
+
if datetime_column
|
289
|
+
else self.client.scard(table_name)
|
290
|
+
)
|
291
|
+
|
292
|
+
return new_len - old_len
|
293
|
+
|
294
|
+
def _push_hash_docs_to_list(self, docs: List[Dict[str, Any]], table: str) -> int:
|
295
|
+
table_name = self.quote_table(table)
|
296
|
+
next_ix = max(self.client.llen(table_name) or 0, 1)
|
297
|
+
for i, doc in enumerate(docs):
|
298
|
+
doc_key = f"{table_name}:{next_ix + i}"
|
299
|
+
self.client.hset(
|
300
|
+
doc_key,
|
301
|
+
mapping={
|
302
|
+
str(k): str(v)
|
303
|
+
for k, v in doc.items()
|
304
|
+
},
|
305
|
+
)
|
306
|
+
self.client.rpush(table_name, doc_key)
|
307
|
+
|
308
|
+
return next_ix + len(docs)
|
309
|
+
|
310
|
+
def get_datetime_column_key(self, table: str) -> str:
|
311
|
+
"""
|
312
|
+
Return the key to store the datetime index for `table`.
|
313
|
+
"""
|
314
|
+
table_name = self.quote_table(table)
|
315
|
+
return f'{table_name}:datetime_column'
|
316
|
+
|
317
|
+
def read(
|
318
|
+
self,
|
319
|
+
table: str,
|
320
|
+
begin: Union[datetime, int, str, None] = None,
|
321
|
+
end: Union[datetime, int, str, None] = None,
|
322
|
+
params: Optional[Dict[str, Any]] = None,
|
323
|
+
datetime_column: Optional[str] = None,
|
324
|
+
select_columns: Optional[List[str]] = None,
|
325
|
+
omit_columns: Optional[List[str]] = None,
|
326
|
+
debug: bool = False
|
327
|
+
) -> Union['pd.DataFrame', None]:
|
328
|
+
"""
|
329
|
+
Query the table and return the result dataframe.
|
330
|
+
|
331
|
+
Parameters
|
332
|
+
----------
|
333
|
+
table: str
|
334
|
+
The "table" name to be queried.
|
335
|
+
|
336
|
+
begin: Union[datetime, int, str, None], default None
|
337
|
+
If provided, only return rows greater than or equal to this datetime.
|
338
|
+
|
339
|
+
end: Union[datetime, int, str, None], default None
|
340
|
+
If provided, only return rows older than this datetime.
|
341
|
+
|
342
|
+
params: Optional[Dict[str, Any]]
|
343
|
+
Additional Meerschaum filter parameters.
|
344
|
+
|
345
|
+
datetime_column: Optional[str], default None
|
346
|
+
If provided, use this column for the datetime index.
|
347
|
+
Otherwise infer from the table metadata.
|
348
|
+
|
349
|
+
select_columns: Optional[List[str]], default None
|
350
|
+
If provided, only return these columns.
|
351
|
+
|
352
|
+
omit_columns: Optional[List[str]], default None
|
353
|
+
If provided, do not include these columns in the result.
|
354
|
+
|
355
|
+
Returns
|
356
|
+
-------
|
357
|
+
A Pandas DataFrame of the result, or `None`.
|
358
|
+
"""
|
359
|
+
from meerschaum.utils.dataframe import parse_df_datetimes, query_df
|
360
|
+
docs = self.read_docs(
|
361
|
+
table,
|
362
|
+
begin=begin,
|
363
|
+
end=end,
|
364
|
+
debug=debug,
|
365
|
+
)
|
366
|
+
df = parse_df_datetimes(docs)
|
367
|
+
datetime_column_key = self.get_datetime_column_key(table)
|
368
|
+
datetime_column = datetime_column or self.get(datetime_column_key)
|
369
|
+
|
370
|
+
return query_df(
|
371
|
+
df,
|
372
|
+
begin=(begin if datetime_column is not None else None),
|
373
|
+
end=(end if datetime_column is not None else None),
|
374
|
+
params=params,
|
375
|
+
datetime_column=datetime_column,
|
376
|
+
select_columns=select_columns,
|
377
|
+
omit_columns=omit_columns,
|
378
|
+
inplace=True,
|
379
|
+
reset_index=True,
|
380
|
+
debug=debug,
|
381
|
+
)
|
382
|
+
|
383
|
+
def read_docs(
|
384
|
+
self,
|
385
|
+
table: str,
|
386
|
+
begin: Union[datetime, int, str, None] = None,
|
387
|
+
end: Union[datetime, int, str, None] = None,
|
388
|
+
debug: bool = False,
|
389
|
+
) -> List[Dict[str, str]]:
|
390
|
+
"""
|
391
|
+
Return a list of previously pushed docs.
|
392
|
+
|
393
|
+
Parameters
|
394
|
+
----------
|
395
|
+
table: str
|
396
|
+
The "table" name (root key) under which the docs were pushed.
|
397
|
+
|
398
|
+
begin: Union[datetime, int, str, None], default None
|
399
|
+
If provided and the table was created with a datetime index, only return documents
|
400
|
+
newer than this datetime.
|
401
|
+
If the table was not created with a datetime index and `begin` is an `int`,
|
402
|
+
return documents with a positional index greater than or equal to this value.
|
403
|
+
|
404
|
+
end: Union[datetime, int, str, None], default None
|
405
|
+
If provided and the table was created with a datetime index, only return documents
|
406
|
+
older than this datetime.
|
407
|
+
If the table was not created with a datetime index and `begin` is an `int`,
|
408
|
+
return documents with a positional index less than this value.
|
409
|
+
|
410
|
+
Returns
|
411
|
+
-------
|
412
|
+
A list of dictionaries, where all keys and values are strings.
|
413
|
+
"""
|
414
|
+
table_name = self.quote_table(table)
|
415
|
+
datetime_column_key = self.get_datetime_column_key(table)
|
416
|
+
datetime_column = self.get(datetime_column_key)
|
417
|
+
|
418
|
+
if debug:
|
419
|
+
dprint(f"Reading documents from '{table}' with {begin=}, {end=}")
|
420
|
+
|
421
|
+
if not datetime_column:
|
422
|
+
return [
|
423
|
+
json.loads(doc_bytes.decode('utf-8'))
|
424
|
+
for doc_bytes in self.client.smembers(table_name)
|
425
|
+
]
|
426
|
+
|
427
|
+
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
428
|
+
|
429
|
+
if isinstance(begin, str):
|
430
|
+
begin = dateutil_parser.parse(begin)
|
431
|
+
|
432
|
+
if isinstance(end, str):
|
433
|
+
end = dateutil_parser.parse(end)
|
434
|
+
|
435
|
+
begin_ts = (
|
436
|
+
(
|
437
|
+
int(begin.replace(tzinfo=timezone.utc).timestamp())
|
438
|
+
if isinstance(begin, datetime)
|
439
|
+
else int(begin)
|
440
|
+
)
|
441
|
+
if begin is not None else '-inf'
|
442
|
+
)
|
443
|
+
end_ts = (
|
444
|
+
(
|
445
|
+
int(end.replace(tzinfo=timezone.utc).timestamp())
|
446
|
+
if isinstance(end, datetime)
|
447
|
+
else int(end)
|
448
|
+
)
|
449
|
+
if end is not None else '+inf'
|
450
|
+
)
|
451
|
+
|
452
|
+
if debug:
|
453
|
+
dprint(f"Reading documents with {begin_ts=}, {end_ts=}")
|
454
|
+
|
455
|
+
return [
|
456
|
+
json.loads(doc_bytes.decode('utf-8'))
|
457
|
+
for doc_bytes in self.client.zrangebyscore(
|
458
|
+
table_name,
|
459
|
+
begin_ts,
|
460
|
+
end_ts,
|
461
|
+
withscores=False,
|
462
|
+
)
|
463
|
+
]
|
464
|
+
|
465
|
+
def _read_docs_from_list(
|
466
|
+
self,
|
467
|
+
table: str,
|
468
|
+
begin_ix: Optional[int] = 0,
|
469
|
+
end_ix: Optional[int] = -1,
|
470
|
+
debug: bool = False,
|
471
|
+
):
|
472
|
+
"""
|
473
|
+
Read a list of documents from a "table".
|
474
|
+
|
475
|
+
Parameters
|
476
|
+
----------
|
477
|
+
table: str
|
478
|
+
The "table" (root key) from which to read docs.
|
479
|
+
|
480
|
+
begin_ix: Optional[int], default 0
|
481
|
+
If provided, only read documents from this starting index.
|
482
|
+
|
483
|
+
end_ix: Optional[int], default -1
|
484
|
+
If provided, only read documents up to (not including) this index.
|
485
|
+
|
486
|
+
Returns
|
487
|
+
-------
|
488
|
+
A list of documents.
|
489
|
+
"""
|
490
|
+
if begin_ix is None:
|
491
|
+
begin_ix = 0
|
492
|
+
|
493
|
+
if end_ix == 0:
|
494
|
+
return
|
495
|
+
|
496
|
+
if end_ix is None:
|
497
|
+
end_ix = -1
|
498
|
+
else:
|
499
|
+
end_ix -= 1
|
500
|
+
|
501
|
+
table_name = self.quote_table(table)
|
502
|
+
doc_keys = self.client.lrange(table_name, begin_ix, end_ix)
|
503
|
+
for doc_key in doc_keys:
|
504
|
+
yield {
|
505
|
+
key.decode('utf-8'): value.decode('utf-8')
|
506
|
+
for key, value in self.client.hgetall(doc_key).items()
|
507
|
+
}
|
508
|
+
|
509
|
+
def drop_table(self, table: str, debug: bool = False) -> None:
|
510
|
+
"""
|
511
|
+
Drop a "table" of documents.
|
512
|
+
|
513
|
+
Parameters
|
514
|
+
----------
|
515
|
+
table: str
|
516
|
+
The "table" name (root key) to be deleted.
|
517
|
+
"""
|
518
|
+
table_name = self.quote_table(table)
|
519
|
+
datetime_column_key = self.get_datetime_column_key(table)
|
520
|
+
self.client.delete(table_name)
|
521
|
+
self.client.delete(datetime_column_key)
|
522
|
+
|
523
|
+
@classmethod
|
524
|
+
def get_entity_key(cls, *keys: Any) -> str:
|
525
|
+
"""
|
526
|
+
Return a joined key to set an entity.
|
527
|
+
"""
|
528
|
+
if not keys:
|
529
|
+
raise ValueError("No keys to be joined.")
|
530
|
+
|
531
|
+
for key in keys:
|
532
|
+
if cls.KEY_SEPARATOR in str(key):
|
533
|
+
raise ValueError(f"Key cannot contain separator '{cls.KEY_SEPARATOR}'.")
|
534
|
+
|
535
|
+
return cls.KEY_SEPARATOR.join([str(key) for key in keys])
|