meerschaum 2.9.5__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +198 -118
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +382 -95
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/js/terminado.js +3 -0
  68. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  69. meerschaum/api/resources/templates/termpage.html +13 -0
  70. meerschaum/api/routes/__init__.py +1 -0
  71. meerschaum/api/routes/_actions.py +3 -4
  72. meerschaum/api/routes/_connectors.py +3 -7
  73. meerschaum/api/routes/_jobs.py +26 -35
  74. meerschaum/api/routes/_login.py +120 -15
  75. meerschaum/api/routes/_misc.py +5 -10
  76. meerschaum/api/routes/_pipes.py +178 -143
  77. meerschaum/api/routes/_plugins.py +38 -28
  78. meerschaum/api/routes/_tokens.py +236 -0
  79. meerschaum/api/routes/_users.py +47 -35
  80. meerschaum/api/routes/_version.py +3 -3
  81. meerschaum/api/routes/_webterm.py +3 -3
  82. meerschaum/config/__init__.py +100 -30
  83. meerschaum/config/_default.py +132 -64
  84. meerschaum/config/_edit.py +38 -32
  85. meerschaum/config/_formatting.py +2 -0
  86. meerschaum/config/_patch.py +10 -8
  87. meerschaum/config/_paths.py +133 -13
  88. meerschaum/config/_read_config.py +87 -36
  89. meerschaum/config/_sync.py +6 -3
  90. meerschaum/config/_version.py +1 -1
  91. meerschaum/config/environment.py +262 -0
  92. meerschaum/config/stack/__init__.py +37 -15
  93. meerschaum/config/static.py +18 -0
  94. meerschaum/connectors/_Connector.py +11 -6
  95. meerschaum/connectors/__init__.py +41 -22
  96. meerschaum/connectors/api/_APIConnector.py +34 -6
  97. meerschaum/connectors/api/_actions.py +2 -2
  98. meerschaum/connectors/api/_jobs.py +12 -1
  99. meerschaum/connectors/api/_login.py +33 -7
  100. meerschaum/connectors/api/_misc.py +2 -2
  101. meerschaum/connectors/api/_pipes.py +23 -32
  102. meerschaum/connectors/api/_plugins.py +2 -2
  103. meerschaum/connectors/api/_request.py +1 -1
  104. meerschaum/connectors/api/_tokens.py +146 -0
  105. meerschaum/connectors/api/_users.py +70 -58
  106. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  107. meerschaum/connectors/instance/__init__.py +10 -0
  108. meerschaum/connectors/instance/_pipes.py +442 -0
  109. meerschaum/connectors/instance/_plugins.py +159 -0
  110. meerschaum/connectors/instance/_tokens.py +317 -0
  111. meerschaum/connectors/instance/_users.py +188 -0
  112. meerschaum/connectors/parse.py +5 -2
  113. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  114. meerschaum/connectors/sql/_cli.py +12 -11
  115. meerschaum/connectors/sql/_create_engine.py +12 -168
  116. meerschaum/connectors/sql/_fetch.py +2 -18
  117. meerschaum/connectors/sql/_pipes.py +295 -278
  118. meerschaum/connectors/sql/_plugins.py +29 -0
  119. meerschaum/connectors/sql/_sql.py +46 -21
  120. meerschaum/connectors/sql/_users.py +36 -2
  121. meerschaum/connectors/sql/tables/__init__.py +254 -122
  122. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  123. meerschaum/connectors/valkey/_pipes.py +60 -31
  124. meerschaum/connectors/valkey/_plugins.py +2 -26
  125. meerschaum/core/Pipe/__init__.py +115 -85
  126. meerschaum/core/Pipe/_attributes.py +425 -124
  127. meerschaum/core/Pipe/_bootstrap.py +54 -24
  128. meerschaum/core/Pipe/_cache.py +555 -0
  129. meerschaum/core/Pipe/_clear.py +0 -11
  130. meerschaum/core/Pipe/_data.py +96 -68
  131. meerschaum/core/Pipe/_deduplicate.py +0 -13
  132. meerschaum/core/Pipe/_delete.py +12 -21
  133. meerschaum/core/Pipe/_drop.py +11 -23
  134. meerschaum/core/Pipe/_dtypes.py +49 -19
  135. meerschaum/core/Pipe/_edit.py +14 -4
  136. meerschaum/core/Pipe/_fetch.py +1 -1
  137. meerschaum/core/Pipe/_index.py +8 -14
  138. meerschaum/core/Pipe/_show.py +5 -5
  139. meerschaum/core/Pipe/_sync.py +123 -204
  140. meerschaum/core/Pipe/_verify.py +4 -4
  141. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  142. meerschaum/core/Plugin/__init__.py +1 -1
  143. meerschaum/core/Token/_Token.py +220 -0
  144. meerschaum/core/Token/__init__.py +12 -0
  145. meerschaum/core/User/_User.py +35 -10
  146. meerschaum/core/User/__init__.py +9 -1
  147. meerschaum/core/__init__.py +1 -0
  148. meerschaum/jobs/_Executor.py +88 -4
  149. meerschaum/jobs/_Job.py +149 -38
  150. meerschaum/jobs/__init__.py +3 -2
  151. meerschaum/jobs/systemd.py +8 -3
  152. meerschaum/models/__init__.py +35 -0
  153. meerschaum/models/pipes.py +247 -0
  154. meerschaum/models/tokens.py +38 -0
  155. meerschaum/models/users.py +26 -0
  156. meerschaum/plugins/__init__.py +301 -88
  157. meerschaum/plugins/bootstrap.py +510 -4
  158. meerschaum/utils/_get_pipes.py +97 -30
  159. meerschaum/utils/daemon/Daemon.py +199 -43
  160. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  161. meerschaum/utils/daemon/RotatingFile.py +63 -36
  162. meerschaum/utils/daemon/StdinFile.py +53 -13
  163. meerschaum/utils/daemon/__init__.py +47 -6
  164. meerschaum/utils/daemon/_names.py +6 -3
  165. meerschaum/utils/dataframe.py +479 -81
  166. meerschaum/utils/debug.py +49 -19
  167. meerschaum/utils/dtypes/__init__.py +476 -34
  168. meerschaum/utils/dtypes/sql.py +369 -29
  169. meerschaum/utils/formatting/__init__.py +5 -2
  170. meerschaum/utils/formatting/_jobs.py +1 -1
  171. meerschaum/utils/formatting/_pipes.py +52 -50
  172. meerschaum/utils/formatting/_pprint.py +1 -0
  173. meerschaum/utils/formatting/_shell.py +44 -18
  174. meerschaum/utils/misc.py +268 -186
  175. meerschaum/utils/packages/__init__.py +25 -40
  176. meerschaum/utils/packages/_packages.py +42 -34
  177. meerschaum/utils/pipes.py +213 -0
  178. meerschaum/utils/process.py +2 -2
  179. meerschaum/utils/prompt.py +175 -144
  180. meerschaum/utils/schedule.py +2 -1
  181. meerschaum/utils/sql.py +134 -47
  182. meerschaum/utils/threading.py +42 -0
  183. meerschaum/utils/typing.py +1 -4
  184. meerschaum/utils/venv/_Venv.py +2 -2
  185. meerschaum/utils/venv/__init__.py +7 -7
  186. meerschaum/utils/warnings.py +19 -13
  187. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  188. meerschaum-3.0.0.dist-info/RECORD +289 -0
  189. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  190. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  191. meerschaum/api/models/_interfaces.py +0 -15
  192. meerschaum/api/models/_locations.py +0 -15
  193. meerschaum/api/models/_metrics.py +0 -15
  194. meerschaum/config/_environment.py +0 -145
  195. meerschaum/config/static/__init__.py +0 -186
  196. meerschaum-2.9.5.dist-info/RECORD +0 -263
  197. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  198. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  199. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  200. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -7,6 +7,8 @@ Attempt to create a pipe's requirements in one method.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
11
+ import meerschaum as mrsm
10
12
  from meerschaum.utils.typing import SuccessTuple, Dict, Any
11
13
 
12
14
 
@@ -207,28 +209,56 @@ def _ask_for_columns(pipe, debug: bool=False) -> Dict[str, str]:
207
209
  """
208
210
  Prompt the user for the column names.
209
211
  """
210
- from meerschaum.utils.warnings import info, warn
211
- from meerschaum.utils.prompt import prompt
212
+ import json
213
+ from meerschaum.utils.warnings import info
214
+ from meerschaum.utils.prompt import prompt, yes_no
215
+ from meerschaum.utils.formatting import get_console
216
+ from meerschaum.utils.formatting._shell import clear_screen
217
+ from meerschaum.utils.misc import to_snake_case
218
+ from meerschaum.config import get_config
219
+ rich_json = mrsm.attempt_import('rich.json')
212
220
 
213
- info(f"Please enter column names for {pipe}:")
214
- while True:
215
- try:
216
- datetime_name = prompt(f"Datetime column (empty to omit):", icon=False)
217
- except KeyboardInterrupt:
218
- return False, f"Cancelled bootstrapping {pipe}."
219
- if datetime_name == '':
220
- datetime_name = None
221
-
222
- try:
223
- id_name = prompt(f"ID column (empty to omit):", icon=False)
224
- except KeyboardInterrupt:
225
- return False, f"Cancelled bootstrapping {pipe}."
226
- if id_name == '':
227
- id_name = None
228
-
229
- break
230
-
231
- return {
232
- 'datetime': datetime_name,
233
- 'id': id_name,
234
- }
221
+ do_clear = get_config('shell', 'clear_screen')
222
+
223
+ cols = {}
224
+
225
+ info(f"Please enter index columns for {pipe}:")
226
+ try:
227
+ datetime_name = prompt("Datetime column (empty to omit):", icon=False)
228
+ except KeyboardInterrupt:
229
+ datetime_name = None
230
+
231
+ if datetime_name:
232
+ cols['datetime'] = datetime_name
233
+
234
+ try:
235
+ id_name = prompt("ID column (empty to omit):", icon=False)
236
+ except KeyboardInterrupt:
237
+ id_name = None
238
+
239
+ if id_name:
240
+ cols['id'] = id_name
241
+
242
+ if yes_no("Add more columns?"):
243
+ while True:
244
+ if do_clear:
245
+ clear_screen(debug=debug)
246
+
247
+ cols_text = json.dumps(cols, indent=4)
248
+ info("Current index columns:")
249
+ get_console().print(rich_json.JSON(cols_text))
250
+
251
+ col_name = prompt("Enter index column (empty to stop):")
252
+ if not col_name:
253
+ break
254
+
255
+ if col_name in cols.values():
256
+ continue
257
+
258
+ col_ix = to_snake_case(col_name)
259
+ if col_ix in cols:
260
+ col_ix = col_ix + '_'
261
+
262
+ cols[col_ix] = col_name
263
+
264
+ return cols
@@ -0,0 +1,555 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define logic for caching pipes' attributes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import pickle
12
+ import json
13
+ import pathlib
14
+ from datetime import datetime, timedelta
15
+ from typing import Any, Union, List
16
+
17
+ import meerschaum as mrsm
18
+ from meerschaum.utils.warnings import warn, dprint
19
+
20
+
21
+ def _get_in_memory_key(cache_key: str) -> str:
22
+ """
23
+ Return the in-memory version of a cache key.
24
+ """
25
+ return (
26
+ ('_' + cache_key)
27
+ if not cache_key.startswith('_')
28
+ else cache_key
29
+ )
30
+
31
+
32
+ def _get_cache_conn_cache_key(pipe: mrsm.Pipe, cache_key: str) -> str:
33
+ """
34
+ Return the cache key to use in the cache connector.
35
+ """
36
+ return f'.cache:pipes:{pipe.connector_keys}:{pipe.metric_key}:{pipe.location_key}:{cache_key}'
37
+
38
+
39
+ def _get_cache_connector(self) -> 'Union[None, ValkeyConnector]':
40
+ """
41
+ Return the cache connector if required.
42
+ """
43
+ enable_valkey_cache = mrsm.get_config('system', 'experimental', 'valkey_session_cache')
44
+ if not enable_valkey_cache:
45
+ return None
46
+
47
+ if self.cache_connector_keys is None:
48
+ return None
49
+
50
+ if not self.cache_connector_keys.startswith('valkey:'):
51
+ warn(f"Invalid cache connector keys: '{self.cache_connector_keys}'")
52
+ return None
53
+
54
+ return mrsm.get_connector(self.cache_connector_keys)
55
+
56
+
57
+ def _cache_value(
58
+ self,
59
+ cache_key: str,
60
+ value: Any,
61
+ memory_only: bool = False,
62
+ debug: bool = False,
63
+ ) -> None:
64
+ """
65
+ Cache a value in-memory and (if `Pipe.cache` is `True`) on-disk or to the cache connector.
66
+ """
67
+ if value is None:
68
+ if debug:
69
+ dprint(f"Skip caching '{cache_key}': received value of `None`")
70
+ return
71
+
72
+ in_memory_key = _get_in_memory_key(cache_key)
73
+ self.__dict__[in_memory_key] = value
74
+ if memory_only:
75
+ return
76
+
77
+ write_success, write_msg = (
78
+ self._write_cache_key(cache_key, value)
79
+ if self.cache
80
+ else (True, "Success")
81
+ )
82
+ if not write_success and debug:
83
+ dprint(f"Failed to cache '{cache_key}':\n{write_msg}")
84
+
85
+
86
+ def _get_cached_value(
87
+ self,
88
+ cache_key: str,
89
+ debug: bool = False,
90
+ ) -> Any:
91
+ """
92
+ Attempt to retrieve a cached value from in-memory on on-disk.
93
+ """
94
+ in_memory_key = _get_in_memory_key(cache_key)
95
+ if in_memory_key in self.__dict__:
96
+ return self.__dict__[in_memory_key]
97
+
98
+ return self._read_cache_key(cache_key, debug=debug)
99
+
100
+
101
+ def _invalidate_cache(
102
+ self,
103
+ hard: bool = False,
104
+ debug: bool = False,
105
+ ) -> mrsm.SuccessTuple:
106
+ """
107
+ Invalidate temporary in-memory cache.
108
+ Note this does not affect in on-disk cache created when `cache=True`.
109
+
110
+ Parameters
111
+ ----------
112
+ hard: bool, default False
113
+ If `True`, clear all temporary cache.
114
+ Otherwise only clear soft cache.
115
+
116
+ Returns
117
+ -------
118
+ A `SuccessTuple` to indicate success.
119
+ """
120
+ if debug:
121
+ dprint(f"Invalidating {'some' if not hard else 'all'} cache for {self}.")
122
+
123
+ self._clear_cache_key('_exists', debug=debug)
124
+ self._clear_cache_key('sync_ts', debug=debug)
125
+
126
+ if not hard:
127
+ return True, "Success"
128
+
129
+ if self.__dict__.get('_static', None):
130
+ return True, "Success"
131
+
132
+ cache_keys = self._get_cache_keys(debug=debug)
133
+ for cache_key in cache_keys:
134
+ if cache_keys == 'attributes':
135
+ continue
136
+ self._clear_cache_key(cache_key, debug=debug)
137
+
138
+ return True, "Success"
139
+
140
+
141
+ def _get_cache_dir_path(self, create_if_not_exists: bool = False) -> pathlib.Path:
142
+ """
143
+ Return the path to the cache directory.
144
+ """
145
+ from meerschaum.config.paths import PIPES_CACHE_RESOURCES_PATH, ROOT_DIR_PATH
146
+ cache_dir_path = (
147
+ PIPES_CACHE_RESOURCES_PATH
148
+ / self.instance_keys
149
+ / self.connector_keys
150
+ / self.metric_key
151
+ / str(self.location_key)
152
+ )
153
+ if create_if_not_exists and not cache_dir_path.exists():
154
+ try:
155
+ cache_dir_path.mkdir(parents=True, exist_ok=True)
156
+ except Exception as e:
157
+ warn(f"Encountered an issue when creating local pipe metadata cache:\n{e}")
158
+
159
+ return cache_dir_path
160
+
161
+
162
+ def _write_cache_key(
163
+ self,
164
+ cache_key: str,
165
+ obj_to_write: Any,
166
+ debug: bool = False,
167
+ ) -> mrsm.SuccessTuple:
168
+ """
169
+ Pickle and write the object to cache.
170
+ """
171
+ cache_connector = self._get_cache_connector()
172
+ if cache_connector is None:
173
+ return self._write_cache_file(cache_key, obj_to_write, debug=debug)
174
+
175
+ return self._write_cache_conn_key(cache_key, obj_to_write, debug=debug)
176
+
177
+
178
+ def _write_cache_file(
179
+ self,
180
+ cache_key: str,
181
+ obj_to_write: Any,
182
+ debug: bool = False,
183
+ ) -> mrsm.SuccessTuple:
184
+ """
185
+ Write a pickle-able object to a cache file.
186
+ """
187
+ from meerschaum.utils.dtypes import get_current_timestamp, json_serialize_value
188
+ now = get_current_timestamp()
189
+ _checked_if_cache_dir_exists = self.__dict__.get('_checked_if_cache_dir_exists', None)
190
+ cache_dir_path = self._get_cache_dir_path(create_if_not_exists=(not _checked_if_cache_dir_exists))
191
+ if not _checked_if_cache_dir_exists:
192
+ self._checked_if_cache_dir_exists = True
193
+
194
+ file_path = cache_dir_path / (cache_key + '.pkl')
195
+ meta_file_path = cache_dir_path / (cache_key + '.meta.json')
196
+ metadata = {
197
+ 'created': now,
198
+ }
199
+
200
+ if debug:
201
+ dprint(f"Writing cache file '{file_path}'.")
202
+
203
+ try:
204
+ with open(file_path, 'wb+') as f:
205
+ pickle.dump(obj_to_write, f)
206
+ with open(meta_file_path, 'w+', encoding='utf-8') as f:
207
+ json.dump(metadata, f, default=json_serialize_value)
208
+ except Exception as e:
209
+ if debug:
210
+ dprint(f"Failed to write cache file:\n{e}")
211
+ return False, f"Failed to write cache file:\n{e}"
212
+
213
+ return True, "Success"
214
+
215
+
216
+ def _write_cache_conn_key(
217
+ self,
218
+ cache_key: str,
219
+ obj_to_write: Any,
220
+ debug: bool = False,
221
+ ) -> mrsm.SuccessTuple:
222
+ """
223
+ Write the object to the cache connector.
224
+ """
225
+ cache_connector = self._get_cache_connector()
226
+ if cache_connector is None:
227
+ return False, f"No cache connector is set for {self}."
228
+
229
+ cache_conn_cache_key = _get_cache_conn_cache_key(self, cache_key)
230
+ local_cache_timeout_seconds = int(mrsm.get_config(
231
+ 'pipes', 'attributes', 'local_cache_timeout_seconds'
232
+ ))
233
+ obj_bytes = pickle.dumps(obj_to_write)
234
+ if debug:
235
+ dprint(f"Setting '{cache_conn_cache_key}' on '{cache_connector}'.")
236
+
237
+ success = cache_connector.set(
238
+ cache_conn_cache_key,
239
+ obj_bytes,
240
+ ex=local_cache_timeout_seconds,
241
+ )
242
+ if not success:
243
+ return False, f"Failed to set '{cache_conn_cache_key}' on '{cache_connector}'."
244
+
245
+ return True, "Success"
246
+
247
+
248
+ def _read_cache_key(
249
+ self,
250
+ cache_key: str,
251
+ debug: bool = False,
252
+ ) -> Any:
253
+ """
254
+ Read the cache file if the cache connector is None, otherwise read from Valkey.
255
+ """
256
+ cache_connector = self._get_cache_connector()
257
+ if cache_connector is None:
258
+ return self._read_cache_file(cache_key, debug=debug)
259
+
260
+ return self._read_cache_conn_key(cache_key, debug=debug)
261
+
262
+
263
+ def _read_cache_file(
264
+ self,
265
+ cache_key: str,
266
+ debug: bool = False,
267
+ ) -> Any:
268
+ """
269
+ Read a cache file and return the pickled object.
270
+ Returns `None` if the cache file does not exist or is expired.
271
+ """
272
+ from meerschaum.utils.dtypes import get_current_timestamp
273
+ now = get_current_timestamp()
274
+ cache_dir_path = self._get_cache_dir_path()
275
+ file_path = cache_dir_path / (cache_key + '.pkl')
276
+ meta_file_path = cache_dir_path / (cache_key + '.meta.json')
277
+ local_cache_timeout_seconds = mrsm.get_config(
278
+ 'pipes', 'attributes', 'local_cache_timeout_seconds'
279
+ )
280
+
281
+ if not meta_file_path.exists() or not file_path.exists():
282
+ return None
283
+
284
+ try:
285
+ if debug:
286
+ dprint(f"Reading cache file '{file_path}'.")
287
+
288
+ with open(meta_file_path, 'r', encoding='utf-8') as f:
289
+ metadata = json.load(f)
290
+ except Exception as e:
291
+ if debug:
292
+ dprint(f"Failed to read cache metadata file '{meta_file_path}':\n{e}")
293
+ return None
294
+
295
+ created_str = metadata.get('created', None)
296
+ created = datetime.fromisoformat(created_str) if created_str else None
297
+ if not created:
298
+ if debug:
299
+ dprint(f"Could not read cache `created` timestamp for '{meta_file_path}'.")
300
+ return None
301
+
302
+ is_expired = (now - created) >= timedelta(seconds=local_cache_timeout_seconds)
303
+ if is_expired:
304
+ self._clear_cache_file(cache_key, debug=debug)
305
+ return None
306
+
307
+ try:
308
+ with open(file_path, 'rb') as f:
309
+ obj = pickle.load(f)
310
+ except Exception as e:
311
+ if debug:
312
+ dprint(f"Failed to read cache file:\n{e}")
313
+
314
+ return None
315
+
316
+ return obj
317
+
318
+
319
+ def _read_cache_conn_key(
320
+ self,
321
+ cache_key: str,
322
+ debug: bool = False,
323
+ ) -> Any:
324
+ """
325
+ Read a cache key from the cache connector.
326
+ """
327
+ cache_connector = self._get_cache_connector()
328
+ if cache_connector is None:
329
+ return None
330
+
331
+ cache_conn_cache_key = _get_cache_conn_cache_key(self, cache_key)
332
+ try:
333
+ obj_bytes = cache_connector.get(cache_conn_cache_key, decode=False)
334
+ if obj_bytes is None:
335
+ return None
336
+ obj = pickle.loads(obj_bytes)
337
+ except Exception as e:
338
+ warn(f"Failed to load '{cache_conn_cache_key}' from '{cache_connector}':\n{e}")
339
+ return None
340
+
341
+ return obj
342
+
343
+
344
+ def _load_cache_keys(self, debug: bool = False) -> mrsm.SuccessTuple:
345
+ """
346
+ Discover and load existing cache keys.
347
+ """
348
+ if not self.cache:
349
+ return True, f"Skip checking for cache for {self}."
350
+
351
+ cache_connector = self._get_cache_connector()
352
+ if cache_connector is None:
353
+ return self._load_cache_files(debug=debug)
354
+
355
+ return self._load_cache_conn_keys(debug=debug)
356
+
357
+
358
+ def _load_cache_files(self, debug: bool = False) -> mrsm.SuccessTuple:
359
+ """
360
+ Load all the existing pickle cache files.
361
+ """
362
+ if not self.cache:
363
+ return True, f"Skip checking for cache for {self}."
364
+
365
+ cache_dir_path = self._get_cache_dir_path(create_if_not_exists=True)
366
+ if not cache_dir_path.exists():
367
+ return True, f"No cache directory for {self}."
368
+
369
+ cache_keys = self._get_cache_file_keys(debug=debug)
370
+ if not cache_keys:
371
+ if debug:
372
+ dprint(f"No local cache found for {self}.")
373
+ return True, "No cache to load."
374
+
375
+ if debug:
376
+ dprint(
377
+ f"Will load {len(cache_keys)} cache file"
378
+ + ('s' if len(cache_keys) != 1 else '')
379
+ + f' into {self}.'
380
+ )
381
+
382
+ cache_objs = {
383
+ cache_key: self._read_cache_file(cache_key, debug=debug)
384
+ for cache_key in cache_keys
385
+ }
386
+ cache_patch = {
387
+ in_memory_key: obj
388
+ for cache_key, obj in cache_objs.items()
389
+ if (
390
+ obj is not None
391
+ and (in_memory_key := _get_in_memory_key(cache_key)) not in self.__dict__
392
+ )
393
+ }
394
+ if debug:
395
+ dprint(f"Loading cache keys into {self}:")
396
+ mrsm.pprint(cache_patch)
397
+
398
+ self.__dict__.update(cache_patch)
399
+ return True, "Success"
400
+
401
+
402
+ def _load_cache_conn_keys(self, debug: bool = False) -> mrsm.SuccessTuple:
403
+ """
404
+ Discover and load cache keys from the cache connector.
405
+ """
406
+ if not self.cache:
407
+ return True, f"Skip checking for cache for {self}."
408
+
409
+ cache_connector = self._get_cache_connector()
410
+ if cache_connector is None:
411
+ return False, f"No cache connector is set for {self}."
412
+
413
+ keys = self._get_cache_conn_keys(debug=debug)
414
+ try:
415
+ cache_keys_bytes = {
416
+ key.split(':')[-1]: cache_connector.get(key, decode=False)
417
+ for key in keys
418
+ }
419
+ except Exception as e:
420
+ return False, f"Failed to retrieve cache keys for {self} from '{cache_connector}':\n{e}"
421
+
422
+ try:
423
+ cache_keys_objs = {
424
+ cache_key: pickle.loads(obj_bytes)
425
+ for cache_key, obj_bytes in cache_keys_bytes.items()
426
+ }
427
+ except Exception as e:
428
+ return False, f"Failed to de-pickle cache bytes from '{self}':\n{e}"
429
+
430
+ cache_patch = {
431
+ in_memory_key: obj
432
+ for cache_key, obj in cache_keys_objs.items()
433
+ if (
434
+ obj is not None
435
+ and (in_memory_key := _get_in_memory_key(cache_key)) not in self.__dict__
436
+ )
437
+ }
438
+ if debug:
439
+ dprint("Loading cache keys into {self}:")
440
+ mrsm.pprint(cache_patch)
441
+
442
+ self.__dict__.update(cache_patch)
443
+ return True, "Success"
444
+
445
+
446
+ def _get_cache_keys(self, debug: bool = False) -> List[str]:
447
+ """
448
+ Return a list of existing cache keys.
449
+ """
450
+ cache_connector = self._get_cache_connector()
451
+ if cache_connector is None:
452
+ return self._get_cache_file_keys(debug=debug)
453
+
454
+ return self._get_cache_conn_keys(debug=debug)
455
+
456
+
457
+ def _get_cache_file_keys(self, debug: bool = False) -> List[str]:
458
+ """
459
+ Return the cache keys from disk.
460
+ """
461
+ cache_dir_path = self._get_cache_dir_path()
462
+ if not cache_dir_path.exists():
463
+ if debug:
464
+ dprint(f"Cache path '{cache_dir_path}' does not exist; no keys to return.")
465
+ return []
466
+
467
+ if debug:
468
+ dprint(f"Listing cache files from '{cache_dir_path}'.")
469
+
470
+ return [
471
+ filename[:(-1 * len('.pkl'))]
472
+ for filename in os.listdir(cache_dir_path)
473
+ if filename.endswith('.pkl')
474
+ ]
475
+
476
+
477
+ def _get_cache_conn_keys(self, debug: bool = False) -> List[str]:
478
+ """
479
+ Return the cache keys from the cache connector.
480
+ """
481
+ cache_connector = self._get_cache_connector()
482
+ if cache_connector is None:
483
+ return []
484
+
485
+ keys_prefix = _get_cache_conn_cache_key(self, '')
486
+
487
+ try:
488
+ return cache_connector.client.keys(keys_prefix + '*')
489
+ except Exception as e:
490
+ warn(f"Failed to get cache keys for {self} from '{cache_connector}':\n{e}")
491
+ return []
492
+
493
+
494
+ def _clear_cache_key(
495
+ self,
496
+ cache_key: str,
497
+ debug: bool = False,
498
+ ) -> None:
499
+ """
500
+ Clear a cached value from in-memory and on-disk / from Valkey.
501
+ """
502
+ in_memory_key = _get_in_memory_key(cache_key)
503
+ _ = self.__dict__.pop(in_memory_key, None)
504
+
505
+ cache_connector = self._get_cache_connector()
506
+ if cache_connector is None:
507
+ self._clear_cache_file(cache_key, debug=debug)
508
+ else:
509
+ self._clear_cache_conn_key(cache_key, debug=debug)
510
+
511
+
512
+ def _clear_cache_file(
513
+ self,
514
+ cache_key: str,
515
+ debug: bool = False,
516
+ ) -> None:
517
+ """
518
+ Clear a cached value from on-disk.
519
+ """
520
+ cache_dir_path = self._get_cache_dir_path()
521
+ file_path = cache_dir_path / (cache_key + '.pkl')
522
+ meta_file_path = cache_dir_path / (cache_key + '.meta.json')
523
+
524
+ try:
525
+ if file_path.exists():
526
+ file_path.unlink()
527
+ except Exception as e:
528
+ if debug:
529
+ dprint(f"Failed to delete cache file '{file_path}':\n{e}")
530
+
531
+ try:
532
+ if meta_file_path.exists():
533
+ meta_file_path.unlink()
534
+ except Exception as e:
535
+ if debug:
536
+ dprint(f"Failed to delete meta cache file '{meta_file_path}':{e}")
537
+
538
+
539
+ def _clear_cache_conn_key(
540
+ self,
541
+ cache_key: str,
542
+ debug: bool = False,
543
+ ) -> None:
544
+ """
545
+ Clear a cached value from Valkey.
546
+ """
547
+ cache_connector = self._get_cache_connector()
548
+ if cache_connector is None:
549
+ return
550
+
551
+ cache_conn_cache_key = _get_cache_conn_cache_key(self, cache_key)
552
+ try:
553
+ cache_connector.client.unlink(cache_conn_cache_key)
554
+ except Exception as e:
555
+ warn(f"Failed to clear cache key '{cache_key}' from '{cache_connector}':\n{e}")
@@ -60,17 +60,6 @@ def clear(
60
60
 
61
61
  begin, end = self.parse_date_bounds(begin, end)
62
62
 
63
- if self.cache_pipe is not None:
64
- success, msg = self.cache_pipe.clear(
65
- begin=begin,
66
- end=end,
67
- params=params,
68
- debug=debug,
69
- **kwargs
70
- )
71
- if not success:
72
- warn(msg)
73
-
74
63
  with Venv(get_connector_plugin(self.instance_connector)):
75
64
  return self.instance_connector.clear_pipe(
76
65
  self,