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
meerschaum/utils/misc.py CHANGED
@@ -6,8 +6,11 @@ Miscellaneous functions go here
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+
9
10
  import sys
10
- from datetime import timedelta, datetime, timezone
11
+ import functools
12
+ from datetime import timedelta, datetime
13
+
11
14
  from meerschaum.utils.typing import (
12
15
  Union,
13
16
  Any,
@@ -19,13 +22,8 @@ from meerschaum.utils.typing import (
19
22
  Iterable,
20
23
  PipesDict,
21
24
  Tuple,
22
- InstanceConnector,
23
- Hashable,
24
- Generator,
25
- Iterator,
26
25
  TYPE_CHECKING,
27
26
  )
28
- import meerschaum as mrsm
29
27
  if TYPE_CHECKING:
30
28
  import collections
31
29
 
@@ -42,15 +40,18 @@ __pdoc__: Dict[str, bool] = {
42
40
  'df_is_chunk_generator': False,
43
41
  'choices_docstring': False,
44
42
  '_get_subaction_names': False,
43
+ 'is_pipe_registered': False,
44
+ 'replace_pipes_in_dict': False,
45
+ 'round_time': False,
45
46
  }
46
47
 
47
48
 
48
49
  def add_method_to_class(
49
- func: Callable[[Any], Any],
50
- class_def: 'Class',
51
- method_name: Optional[str] = None,
52
- keep_self: Optional[bool] = None,
53
- ) -> Callable[[Any], Any]:
50
+ func: Callable[[Any], Any],
51
+ class_def: 'Class',
52
+ method_name: Optional[str] = None,
53
+ keep_self: Optional[bool] = None,
54
+ ) -> Callable[[Any], Any]:
54
55
  """
55
56
  Add function `func` to class `class_def`.
56
57
 
@@ -122,16 +123,33 @@ def is_int(s: str) -> bool:
122
123
 
123
124
  """
124
125
  try:
125
- float(s)
126
+ return float(s).is_integer()
126
127
  except Exception:
127
128
  return False
128
-
129
- return float(s).is_integer()
130
129
 
131
130
 
132
- def string_to_dict(
133
- params_string: str
134
- ) -> Dict[str, Any]:
131
+ def is_uuid(s: str) -> bool:
132
+ """
133
+ Check if a string is a valid UUID.
134
+
135
+ Parameters
136
+ ----------
137
+ s: str
138
+ The string to be checked.
139
+
140
+ Returns
141
+ -------
142
+ A bool indicating whether the string is a valid UUID.
143
+ """
144
+ import uuid
145
+ try:
146
+ uuid.UUID(str(s))
147
+ return True
148
+ except Exception:
149
+ return False
150
+
151
+
152
+ def string_to_dict(params_string: str) -> Dict[str, Any]:
135
153
  """
136
154
  Parse a string into a dictionary.
137
155
 
@@ -154,7 +172,7 @@ def string_to_dict(
154
172
  {'a': 1, 'b': 2}
155
173
 
156
174
  """
157
- if params_string == "":
175
+ if not params_string:
158
176
  return {}
159
177
 
160
178
  import json
@@ -169,13 +187,60 @@ def string_to_dict(
169
187
  and params_string[-2] == "}"
170
188
  ):
171
189
  return json.loads(params_string[1:-1])
190
+
172
191
  if str(params_string).startswith('{'):
173
192
  return json.loads(params_string)
174
193
 
175
194
  import ast
176
195
  params_dict = {}
177
- for param in params_string.split(","):
196
+
197
+ items = []
198
+ bracket_level = 0
199
+ brace_level = 0
200
+ current_item = ''
201
+ in_quotes = False
202
+ quote_char = ''
203
+
204
+ i = 0
205
+ while i < len(params_string):
206
+ char = params_string[i]
207
+
208
+ if in_quotes:
209
+ if char == quote_char and (i == 0 or params_string[i-1] != '\\'):
210
+ in_quotes = False
211
+ else:
212
+ if char in ('"', "'"):
213
+ in_quotes = True
214
+ quote_char = char
215
+ elif char == '[':
216
+ bracket_level += 1
217
+ elif char == ']':
218
+ bracket_level -= 1
219
+ elif char == '{':
220
+ brace_level += 1
221
+ elif char == '}':
222
+ brace_level -= 1
223
+ elif char == ',' and bracket_level == 0 and brace_level == 0:
224
+ items.append(current_item)
225
+ current_item = ''
226
+ i += 1
227
+ continue
228
+
229
+ current_item += char
230
+ i += 1
231
+
232
+ if current_item:
233
+ items.append(current_item)
234
+
235
+ for param in items:
236
+ param = param.strip()
237
+ if not param:
238
+ continue
239
+
178
240
  _keys = param.split(":", maxsplit=1)
241
+ if len(_keys) != 2:
242
+ continue
243
+
179
244
  keys = _keys[:-1]
180
245
  try:
181
246
  val = ast.literal_eval(_keys[-1])
@@ -197,12 +262,35 @@ def string_to_dict(
197
262
  return params_dict
198
263
 
199
264
 
265
+ def to_simple_dict(doc: Dict[str, Any]) -> str:
266
+ """
267
+ Serialize a document dictionary in simple-dict format.
268
+ """
269
+ import json
270
+ import ast
271
+ from meerschaum.utils.dtypes import json_serialize_value
272
+
273
+ def serialize_value(value):
274
+ if isinstance(value, str):
275
+ try:
276
+ evaluated = ast.literal_eval(value)
277
+ if not isinstance(evaluated, str):
278
+ return json.dumps(value, separators=(',', ':'), default=json_serialize_value)
279
+ return value
280
+ except (ValueError, SyntaxError, TypeError, MemoryError):
281
+ return value
282
+
283
+ return json.dumps(value, separators=(',', ':'), default=json_serialize_value)
284
+
285
+ return ','.join(f"{key}:{serialize_value(val)}" for key, val in doc.items())
286
+
287
+
200
288
  def parse_config_substitution(
201
289
  value: str,
202
290
  leading_key: str = 'MRSM',
203
291
  begin_key: str = '{',
204
292
  end_key: str = '}',
205
- delimeter: str = ':'
293
+ delimeter: str = ':',
206
294
  ) -> List[Any]:
207
295
  """
208
296
  Parse Meerschaum substitution syntax
@@ -259,37 +347,6 @@ def edit_file(
259
347
  return rc == 0
260
348
 
261
349
 
262
- def is_pipe_registered(
263
- pipe: mrsm.Pipe,
264
- pipes: PipesDict,
265
- debug: bool = False
266
- ) -> bool:
267
- """
268
- Check if a Pipe is inside the pipes dictionary.
269
-
270
- Parameters
271
- ----------
272
- pipe: meerschaum.Pipe
273
- The pipe to see if it's in the dictionary.
274
-
275
- pipes: PipesDict
276
- The dictionary to search inside.
277
-
278
- debug: bool, default False
279
- Verbosity toggle.
280
-
281
- Returns
282
- -------
283
- A bool indicating whether the pipe is inside the dictionary.
284
- """
285
- from meerschaum.utils.debug import dprint
286
- ck, mk, lk = pipe.connector_keys, pipe.metric_key, pipe.location_key
287
- if debug:
288
- dprint(f'{ck}, {mk}, {lk}')
289
- dprint(f'{pipe}, {pipes}')
290
- return ck in pipes and mk in pipes[ck] and lk in pipes[ck][mk]
291
-
292
-
293
350
  def get_cols_lines(default_cols: int = 100, default_lines: int = 120) -> Tuple[int, int]:
294
351
  """
295
352
  Determine the columns and lines in the terminal.
@@ -311,7 +368,7 @@ def get_cols_lines(default_cols: int = 100, default_lines: int = 120) -> Tuple[i
311
368
  try:
312
369
  size = os.get_terminal_size()
313
370
  _cols, _lines = size.columns, size.lines
314
- except Exception as e:
371
+ except Exception:
315
372
  _cols, _lines = (
316
373
  int(os.environ.get('COLUMNS', str(default_cols))),
317
374
  int(os.environ.get('LINES', str(default_lines))),
@@ -367,7 +424,7 @@ def sorted_dict(d: Dict[Any, Any]) -> Dict[Any, Any]:
367
424
  """
368
425
  try:
369
426
  return {key: value for key, value in sorted(d.items(), key=lambda item: item[1])}
370
- except Exception as e:
427
+ except Exception:
371
428
  return d
372
429
 
373
430
  def flatten_pipes_dict(pipes_dict: PipesDict) -> List[Pipe]:
@@ -391,81 +448,13 @@ def flatten_pipes_dict(pipes_dict: PipesDict) -> List[Pipe]:
391
448
  return pipes_list
392
449
 
393
450
 
394
- def round_time(
395
- dt: Optional[datetime] = None,
396
- date_delta: Optional[timedelta] = None,
397
- to: 'str' = 'down'
398
- ) -> datetime:
399
- """
400
- Round a datetime object to a multiple of a timedelta.
401
- http://stackoverflow.com/questions/3463930/how-to-round-the-minute-of-a-datetime-object-python
402
-
403
- NOTE: This function strips timezone information!
404
-
405
- Parameters
406
- ----------
407
- dt: Optional[datetime], default None
408
- If `None`, grab the current UTC datetime.
409
-
410
- date_delta: Optional[timedelta], default None
411
- If `None`, use a delta of 1 minute.
412
-
413
- to: 'str', default 'down'
414
- Available options are `'up'`, `'down'`, and `'closest'`.
415
-
416
- Returns
417
- -------
418
- A rounded `datetime` object.
419
-
420
- Examples
421
- --------
422
- >>> round_time(datetime(2022, 1, 1, 12, 15, 57, 200))
423
- datetime.datetime(2022, 1, 1, 12, 15)
424
- >>> round_time(datetime(2022, 1, 1, 12, 15, 57, 200), to='up')
425
- datetime.datetime(2022, 1, 1, 12, 16)
426
- >>> round_time(datetime(2022, 1, 1, 12, 15, 57, 200), timedelta(hours=1))
427
- datetime.datetime(2022, 1, 1, 12, 0)
428
- >>> round_time(
429
- ... datetime(2022, 1, 1, 12, 15, 57, 200),
430
- ... timedelta(hours=1),
431
- ... to = 'closest'
432
- ... )
433
- datetime.datetime(2022, 1, 1, 12, 0)
434
- >>> round_time(
435
- ... datetime(2022, 1, 1, 12, 45, 57, 200),
436
- ... datetime.timedelta(hours=1),
437
- ... to = 'closest'
438
- ... )
439
- datetime.datetime(2022, 1, 1, 13, 0)
440
-
441
- """
442
- if date_delta is None:
443
- date_delta = timedelta(minutes=1)
444
- round_to = date_delta.total_seconds()
445
- if dt is None:
446
- dt = datetime.now(timezone.utc).replace(tzinfo=None)
447
- seconds = (dt.replace(tzinfo=None) - dt.min.replace(tzinfo=None)).seconds
448
-
449
- if seconds % round_to == 0 and dt.microsecond == 0:
450
- rounding = (seconds + round_to / 2) // round_to * round_to
451
- else:
452
- if to == 'up':
453
- rounding = (seconds + dt.microsecond/1000000 + round_to) // round_to * round_to
454
- elif to == 'down':
455
- rounding = seconds // round_to * round_to
456
- else:
457
- rounding = (seconds + round_to / 2) // round_to * round_to
458
-
459
- return dt + timedelta(0, rounding - seconds, - dt.microsecond)
460
-
461
-
462
451
  def timed_input(
463
- seconds: int = 10,
464
- timeout_message: str = "",
465
- prompt: str = "",
466
- icon: bool = False,
467
- **kw
468
- ) -> Union[str, None]:
452
+ seconds: int = 10,
453
+ timeout_message: str = "",
454
+ prompt: str = "",
455
+ icon: bool = False,
456
+ **kw
457
+ ) -> Union[str, None]:
469
458
  """
470
459
  Accept user input only for a brief period of time.
471
460
 
@@ -515,52 +504,6 @@ def timed_input(
515
504
  signal.alarm(0) # cancel alarm
516
505
 
517
506
 
518
-
519
-
520
-
521
- def replace_pipes_in_dict(
522
- pipes : Optional[PipesDict] = None,
523
- func: 'function' = str,
524
- debug: bool = False,
525
- **kw
526
- ) -> PipesDict:
527
- """
528
- Replace the Pipes in a Pipes dict with the result of another function.
529
-
530
- Parameters
531
- ----------
532
- pipes: Optional[PipesDict], default None
533
- The pipes dict to be processed.
534
-
535
- func: Callable[[Any], Any], default str
536
- The function to be applied to every pipe.
537
- Defaults to the string constructor.
538
-
539
- debug: bool, default False
540
- Verbosity toggle.
541
-
542
-
543
- Returns
544
- -------
545
- A dictionary where every pipe is replaced with the output of a function.
546
-
547
- """
548
- import copy
549
- def change_dict(d : Dict[Any, Any], func : 'function') -> None:
550
- for k, v in d.items():
551
- if isinstance(v, dict):
552
- change_dict(v, func)
553
- else:
554
- d[k] = func(v)
555
-
556
- if pipes is None:
557
- from meerschaum import get_pipes
558
- pipes = get_pipes(debug=debug, **kw)
559
-
560
- result = copy.deepcopy(pipes)
561
- change_dict(result, func)
562
- return result
563
-
564
507
  def enforce_gevent_monkey_patch():
565
508
  """
566
509
  Check if gevent monkey patching is enabled, and if not, then apply patching.
@@ -634,10 +577,10 @@ def string_width(string: str, widest: bool = True) -> int:
634
577
  return _widest()
635
578
 
636
579
  def _pyinstaller_traverse_dir(
637
- directory: str,
638
- ignore_patterns: Iterable[str] = ('.pyc', 'dist', 'build', '.git', '.log'),
639
- include_dotfiles: bool = False
640
- ) -> list:
580
+ directory: str,
581
+ ignore_patterns: Iterable[str] = ('.pyc', 'dist', 'build', '.git', '.log'),
582
+ include_dotfiles: bool = False
583
+ ) -> list:
641
584
  """
642
585
  Recursively traverse a directory and return a list of its contents.
643
586
  """
@@ -677,6 +620,43 @@ def _pyinstaller_traverse_dir(
677
620
  return paths
678
621
 
679
622
 
623
+ def get_val_from_dict_path(d: Dict[Any, Any], path: Tuple[Any, ...]) -> Any:
624
+ """
625
+ Get a value from a dictionary with a tuple of keys.
626
+
627
+ Parameters
628
+ ----------
629
+ d: Dict[Any, Any]
630
+ The dictionary to search.
631
+
632
+ path: Tuple[Any, ...]
633
+ The path of keys to traverse.
634
+
635
+ Returns
636
+ -------
637
+ The value from the end of the path.
638
+ """
639
+ return functools.reduce(lambda di, key: di[key], path, d)
640
+
641
+
642
+ def set_val_in_dict_path(d: Dict[Any, Any], path: Tuple[Any, ...], val: Any) -> None:
643
+ """
644
+ Set a value in a dictionary with a tuple of keys.
645
+
646
+ Parameters
647
+ ----------
648
+ d: Dict[Any, Any]
649
+ The dictionary to search.
650
+
651
+ path: Tuple[Any, ...]
652
+ The path of keys to traverse.
653
+
654
+ val: Any
655
+ The value to set at the end of the path.
656
+ """
657
+ get_val_from_dict_path(d, path[:-1])[path[-1]] = val
658
+
659
+
680
660
  def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, Any]:
681
661
  """
682
662
  Recursively replace passwords in a dictionary.
@@ -717,7 +697,7 @@ def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, An
717
697
  from meerschaum.connectors.sql import SQLConnector
718
698
  try:
719
699
  uri_params = SQLConnector.parse_uri(v)
720
- except Exception as e:
700
+ except Exception:
721
701
  uri_params = None
722
702
  if not uri_params:
723
703
  continue
@@ -877,6 +857,7 @@ def dict_from_od(od: collections.OrderedDict) -> Dict[Any, Any]:
877
857
  _d[k] = dict_from_od(v)
878
858
  return _d
879
859
 
860
+
880
861
  def remove_ansi(s: str) -> str:
881
862
  """
882
863
  Remove ANSI escape characters from a string.
@@ -1153,7 +1134,7 @@ def items_str(
1153
1134
  return output
1154
1135
 
1155
1136
 
1156
- def interval_str(delta: Union[timedelta, int]) -> str:
1137
+ def interval_str(delta: Union[timedelta, int], round_unit: bool = False) -> str:
1157
1138
  """
1158
1139
  Return a human-readable string for a `timedelta` (or `int` minutes).
1159
1140
 
@@ -1162,20 +1143,57 @@ def interval_str(delta: Union[timedelta, int]) -> str:
1162
1143
  delta: Union[timedelta, int]
1163
1144
  The interval to print. If `delta` is an integer, assume it corresponds to minutes.
1164
1145
 
1146
+ round_unit: bool, default False
1147
+ If `True`, round the output to a single unit.
1148
+
1165
1149
  Returns
1166
1150
  -------
1167
1151
  A formatted string, fit for human eyes.
1168
1152
  """
1169
1153
  from meerschaum.utils.packages import attempt_import
1170
- if is_int(delta):
1154
+ if is_int(str(delta)) and not round_unit:
1171
1155
  return str(delta)
1172
- humanfriendly = attempt_import('humanfriendly')
1156
+
1157
+ humanfriendly = attempt_import('humanfriendly', lazy=False)
1173
1158
  delta_seconds = (
1174
1159
  delta.total_seconds()
1175
- if isinstance(delta, timedelta)
1160
+ if hasattr(delta, 'total_seconds')
1176
1161
  else (delta * 60)
1177
1162
  )
1178
- return humanfriendly.format_timespan(delta_seconds)
1163
+
1164
+ is_negative = delta_seconds < 0
1165
+ delta_seconds = abs(delta_seconds)
1166
+ replace_units = {}
1167
+
1168
+ if round_unit:
1169
+ if delta_seconds < 1:
1170
+ delta_seconds = round(delta_seconds, 2)
1171
+ elif delta_seconds < 60:
1172
+ delta_seconds = int(delta_seconds)
1173
+ elif delta_seconds < 3600:
1174
+ delta_seconds = int(delta_seconds / 60) * 60
1175
+ elif delta_seconds < 86400:
1176
+ delta_seconds = int(delta_seconds / 3600) * 3600
1177
+ elif delta_seconds < (86400 * 7):
1178
+ delta_seconds = int(delta_seconds / 86400) * 86400
1179
+ elif delta_seconds < (86400 * 7 * 4):
1180
+ delta_seconds = int(delta_seconds / (86400 * 7)) * (86400 * 7)
1181
+ elif delta_seconds < (86400 * 7 * 4 * 13):
1182
+ delta_seconds = int(delta_seconds / (86400 * 7 * 4)) * (86400 * 7)
1183
+ replace_units['weeks'] = 'months'
1184
+ else:
1185
+ delta_seconds = int(delta_seconds / (86400 * 364)) * (86400 * 364)
1186
+
1187
+ delta_str = humanfriendly.format_timespan(delta_seconds)
1188
+ if ',' in delta_str and round_unit:
1189
+ delta_str = delta_str.split(',')[0]
1190
+ elif ' and ' in delta_str and round_unit:
1191
+ delta_str = delta_str.split(' and ')[0]
1192
+
1193
+ for parsed_unit, replacement_unit in replace_units.items():
1194
+ delta_str = delta_str.replace(parsed_unit, replacement_unit)
1195
+
1196
+ return delta_str + (' ago' if is_negative else '')
1179
1197
 
1180
1198
 
1181
1199
  def is_docker_available() -> bool:
@@ -1214,11 +1232,15 @@ def is_systemd_available() -> bool:
1214
1232
  import subprocess
1215
1233
  try:
1216
1234
  has_systemctl = subprocess.call(
1217
- ['systemctl', '-h'],
1235
+ ['systemctl', 'whoami'],
1218
1236
  stdout=subprocess.DEVNULL,
1219
1237
  stderr=subprocess.STDOUT,
1220
1238
  ) == 0
1239
+ except FileNotFoundError:
1240
+ has_systemctl = False
1221
1241
  except Exception:
1242
+ import traceback
1243
+ traceback.print_exc()
1222
1244
  has_systemctl = False
1223
1245
  return has_systemctl
1224
1246
 
@@ -1234,6 +1256,8 @@ def is_tmux_available() -> bool:
1234
1256
  stdout=subprocess.DEVNULL,
1235
1257
  stderr=subprocess.STDOUT
1236
1258
  ) == 0
1259
+ except FileNotFoundError:
1260
+ has_tmux = False
1237
1261
  except Exception:
1238
1262
  has_tmux = False
1239
1263
  return has_tmux
@@ -1406,7 +1430,7 @@ def separate_negation_values(
1406
1430
  If `None`, use the system default (`_`).
1407
1431
  """
1408
1432
  if negation_prefix is None:
1409
- from meerschaum.config.static import STATIC_CONFIG
1433
+ from meerschaum._internal.static import STATIC_CONFIG
1410
1434
  negation_prefix = STATIC_CONFIG['system']['fetch_pipes_keys']['negation_prefix']
1411
1435
  _in_vals, _ex_vals = [], []
1412
1436
  for v in vals:
@@ -1442,7 +1466,7 @@ def get_in_ex_params(params: Optional[Dict[str, Any]]) -> Dict[str, Tuple[List[A
1442
1466
  col: separate_negation_values(
1443
1467
  (
1444
1468
  val
1445
- if isinstance(val, (list, tuple))
1469
+ if isinstance(val, (list, tuple, set)) or hasattr(val, 'astype')
1446
1470
  else [val]
1447
1471
  )
1448
1472
  )
@@ -1610,6 +1634,36 @@ def safely_extract_tar(tarf: 'file', output_dir: Union[str, 'pathlib.Path']) ->
1610
1634
  return safe_extract(tarf, output_dir)
1611
1635
 
1612
1636
 
1637
+ def to_snake_case(name: str) -> str:
1638
+ """
1639
+ Return the given string in snake-case-style.
1640
+
1641
+ Parameters
1642
+ ----------
1643
+ name: str
1644
+ The input text to convert to snake case.
1645
+
1646
+ Returns
1647
+ -------
1648
+ A snake-case version of `name`.
1649
+
1650
+ Examples
1651
+ --------
1652
+ >>> to_snake_case("HelloWorld!")
1653
+ 'hello_world'
1654
+ >>> to_snake_case("This has spaces in it.")
1655
+ 'this_has_spaces_in_it'
1656
+ >>> to_snake_case("already_in_snake_case")
1657
+ 'already_in_snake_case'
1658
+ """
1659
+ import re
1660
+ name = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
1661
+ name = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', name)
1662
+ name = re.sub(r'[^\w\s]', '', name)
1663
+ name = re.sub(r'\s+', '_', name)
1664
+ return name.lower()
1665
+
1666
+
1613
1667
  ##################
1614
1668
  # Legacy imports #
1615
1669
  ##################
@@ -1756,6 +1810,33 @@ def json_serialize_datetime(dt: datetime) -> Union[str, None]:
1756
1810
  return serialize_datetime(dt)
1757
1811
 
1758
1812
 
1813
+ def replace_pipes_in_dict(*args, **kwargs):
1814
+ """
1815
+ Placeholder function to prevent breaking legacy behavior.
1816
+ See `meerschaum.utils.pipes.replace_pipes_in_dict`.
1817
+ """
1818
+ from meerschaum.utils.pipes import replace_pipes_in_dict
1819
+ return replace_pipes_in_dict(*args, **kwargs)
1820
+
1821
+
1822
+ def is_pipe_registered(*args, **kwargs):
1823
+ """
1824
+ Placeholder function to prevent breaking legacy behavior.
1825
+ See `meerschaum.utils.pipes.is_pipe_registered`.
1826
+ """
1827
+ from meerschaum.utils.pipes import is_pipe_registered
1828
+ return is_pipe_registered(*args, **kwargs)
1829
+
1830
+
1831
+ def round_time(*args, **kwargs):
1832
+ """
1833
+ Placeholder function to prevent breaking legacy behavior.
1834
+ See `meerschaum.utils.dtypes.round_time`.
1835
+ """
1836
+ from meerschaum.utils.dtypes import round_time
1837
+ return round_time(*args, **kwargs)
1838
+
1839
+
1759
1840
  _current_module = sys.modules[__name__]
1760
1841
  __all__ = tuple(
1761
1842
  name
@@ -1763,4 +1844,5 @@ __all__ = tuple(
1763
1844
  if callable(obj)
1764
1845
  and name not in __pdoc__
1765
1846
  and getattr(obj, '__module__', None) == _current_module.__name__
1847
+ and not name.startswith('_')
1766
1848
  )