meerschaum 2.3.5.dev0__py3-none-any.whl → 2.4.0.dev0__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 (62) hide show
  1. meerschaum/_internal/arguments/__init__.py +2 -1
  2. meerschaum/_internal/arguments/_parse_arguments.py +86 -7
  3. meerschaum/_internal/entry.py +29 -13
  4. meerschaum/actions/api.py +16 -16
  5. meerschaum/actions/bootstrap.py +36 -10
  6. meerschaum/actions/start.py +16 -15
  7. meerschaum/api/_events.py +11 -7
  8. meerschaum/api/dash/__init__.py +7 -6
  9. meerschaum/api/dash/callbacks/__init__.py +1 -0
  10. meerschaum/api/dash/callbacks/dashboard.py +7 -5
  11. meerschaum/api/dash/callbacks/pipes.py +42 -0
  12. meerschaum/api/dash/pages/__init__.py +1 -0
  13. meerschaum/api/dash/pages/pipes.py +16 -0
  14. meerschaum/api/dash/pipes.py +79 -47
  15. meerschaum/api/dash/users.py +19 -6
  16. meerschaum/api/routes/_actions.py +0 -98
  17. meerschaum/api/routes/_jobs.py +38 -18
  18. meerschaum/api/routes/_login.py +4 -4
  19. meerschaum/api/routes/_pipes.py +3 -3
  20. meerschaum/config/_default.py +9 -2
  21. meerschaum/config/_version.py +1 -1
  22. meerschaum/config/stack/__init__.py +59 -18
  23. meerschaum/config/static/__init__.py +2 -0
  24. meerschaum/connectors/Connector.py +19 -13
  25. meerschaum/connectors/__init__.py +9 -5
  26. meerschaum/connectors/api/_actions.py +22 -36
  27. meerschaum/connectors/api/_jobs.py +1 -0
  28. meerschaum/connectors/poll.py +30 -24
  29. meerschaum/connectors/sql/_pipes.py +126 -154
  30. meerschaum/connectors/sql/_plugins.py +45 -43
  31. meerschaum/connectors/sql/_users.py +46 -38
  32. meerschaum/connectors/valkey/ValkeyConnector.py +535 -0
  33. meerschaum/connectors/valkey/__init__.py +8 -0
  34. meerschaum/connectors/valkey/_fetch.py +75 -0
  35. meerschaum/connectors/valkey/_pipes.py +839 -0
  36. meerschaum/connectors/valkey/_plugins.py +265 -0
  37. meerschaum/connectors/valkey/_users.py +305 -0
  38. meerschaum/core/Pipe/__init__.py +2 -0
  39. meerschaum/core/Pipe/_attributes.py +1 -2
  40. meerschaum/core/Pipe/_drop.py +4 -4
  41. meerschaum/core/Pipe/_dtypes.py +14 -14
  42. meerschaum/core/Pipe/_edit.py +15 -14
  43. meerschaum/core/Pipe/_sync.py +134 -51
  44. meerschaum/core/User/_User.py +14 -12
  45. meerschaum/jobs/_Job.py +26 -8
  46. meerschaum/jobs/systemd.py +20 -8
  47. meerschaum/plugins/_Plugin.py +17 -13
  48. meerschaum/utils/_get_pipes.py +14 -20
  49. meerschaum/utils/dataframe.py +288 -101
  50. meerschaum/utils/dtypes/__init__.py +31 -6
  51. meerschaum/utils/dtypes/sql.py +4 -4
  52. meerschaum/utils/misc.py +3 -3
  53. meerschaum/utils/packages/_packages.py +1 -0
  54. meerschaum/utils/prompt.py +1 -1
  55. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/METADATA +3 -1
  56. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/RECORD +62 -54
  57. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/WHEEL +1 -1
  58. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/LICENSE +0 -0
  59. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/NOTICE +0 -0
  60. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/entry_points.txt +0 -0
  61. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/top_level.txt +0 -0
  62. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.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
- self,
14
- user: meerschaum.core.User,
15
- debug: bool = False,
16
- **kw: Any
17
- ) -> SuccessTuple:
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
- f"Usernames may only contain alphanumeric characters " +
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
- self,
96
- user: meerschaum.core.User,
97
- debug: bool = False,
98
- **kw: Any
99
- ) -> SuccessTuple:
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
- self,
150
- user: meerschaum.core.User,
151
- debug : bool = False
152
- ) -> Optional[int]:
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
- self,
172
- user: meerschaum.core.User,
173
- debug: bool = False
174
- ) -> Union[Dict[str, Any], None]:
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
- self,
212
- user: meerschaum.core.User,
213
- debug: bool = False
214
- ) -> SuccessTuple:
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
- self,
243
- debug: bool = False,
244
- **kw: Any
245
- ) -> List[str]:
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
- self,
261
- user: meerschaum.core.User,
262
- debug: bool = False,
263
- **kw: Any
264
- ) -> Optional[str]:
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(f"Fetching user_id...")
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
- self,
293
- user: meerschaum.core.User,
294
- debug: bool = False,
295
- **kw: Any
296
- ) -> Optional[str]:
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])
@@ -0,0 +1,8 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Import the `ValkeyConnector`.
6
+ """
7
+
8
+ from meerschaum.connectors.valkey.ValkeyConnector import ValkeyConnector