meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc2__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 (158) 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 +19 -2
  5. meerschaum/_internal/docs/index.py +49 -2
  6. meerschaum/_internal/entry.py +6 -6
  7. meerschaum/_internal/shell/Shell.py +1 -1
  8. meerschaum/_internal/static.py +356 -0
  9. meerschaum/actions/api.py +12 -2
  10. meerschaum/actions/bootstrap.py +7 -7
  11. meerschaum/actions/edit.py +142 -18
  12. meerschaum/actions/register.py +137 -6
  13. meerschaum/actions/show.py +117 -29
  14. meerschaum/actions/stop.py +4 -1
  15. meerschaum/actions/sync.py +1 -1
  16. meerschaum/actions/tag.py +9 -8
  17. meerschaum/actions/verify.py +5 -8
  18. meerschaum/api/__init__.py +11 -3
  19. meerschaum/api/_events.py +39 -2
  20. meerschaum/api/_oauth2.py +118 -8
  21. meerschaum/api/_tokens.py +102 -0
  22. meerschaum/api/dash/__init__.py +0 -3
  23. meerschaum/api/dash/callbacks/custom.py +2 -2
  24. meerschaum/api/dash/callbacks/dashboard.py +103 -19
  25. meerschaum/api/dash/callbacks/plugins.py +0 -1
  26. meerschaum/api/dash/callbacks/register.py +1 -1
  27. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  28. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  29. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  30. meerschaum/api/dash/components.py +30 -8
  31. meerschaum/api/dash/keys.py +19 -93
  32. meerschaum/api/dash/pages/dashboard.py +1 -20
  33. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  34. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  35. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  36. meerschaum/api/dash/pipes.py +94 -59
  37. meerschaum/api/dash/sessions.py +12 -0
  38. meerschaum/api/dash/tokens.py +606 -0
  39. meerschaum/api/dash/websockets.py +1 -1
  40. meerschaum/api/dash/webterm.py +4 -0
  41. meerschaum/api/models/__init__.py +23 -3
  42. meerschaum/api/models/_actions.py +22 -0
  43. meerschaum/api/models/_pipes.py +85 -7
  44. meerschaum/api/models/_tokens.py +81 -0
  45. meerschaum/api/resources/templates/termpage.html +12 -0
  46. meerschaum/api/routes/__init__.py +1 -0
  47. meerschaum/api/routes/_actions.py +3 -4
  48. meerschaum/api/routes/_connectors.py +3 -7
  49. meerschaum/api/routes/_jobs.py +14 -35
  50. meerschaum/api/routes/_login.py +49 -12
  51. meerschaum/api/routes/_misc.py +5 -10
  52. meerschaum/api/routes/_pipes.py +173 -140
  53. meerschaum/api/routes/_plugins.py +38 -28
  54. meerschaum/api/routes/_tokens.py +236 -0
  55. meerschaum/api/routes/_users.py +47 -35
  56. meerschaum/api/routes/_version.py +3 -3
  57. meerschaum/config/__init__.py +43 -20
  58. meerschaum/config/_default.py +43 -6
  59. meerschaum/config/_edit.py +28 -24
  60. meerschaum/config/_environment.py +1 -1
  61. meerschaum/config/_patch.py +6 -6
  62. meerschaum/config/_paths.py +5 -1
  63. meerschaum/config/_read_config.py +65 -34
  64. meerschaum/config/_sync.py +6 -3
  65. meerschaum/config/_version.py +1 -1
  66. meerschaum/config/stack/__init__.py +31 -11
  67. meerschaum/config/static.py +18 -0
  68. meerschaum/connectors/_Connector.py +10 -4
  69. meerschaum/connectors/__init__.py +4 -20
  70. meerschaum/connectors/api/_APIConnector.py +34 -6
  71. meerschaum/connectors/api/_actions.py +2 -2
  72. meerschaum/connectors/api/_jobs.py +1 -1
  73. meerschaum/connectors/api/_login.py +33 -7
  74. meerschaum/connectors/api/_misc.py +2 -2
  75. meerschaum/connectors/api/_pipes.py +16 -31
  76. meerschaum/connectors/api/_plugins.py +2 -2
  77. meerschaum/connectors/api/_request.py +1 -1
  78. meerschaum/connectors/api/_tokens.py +146 -0
  79. meerschaum/connectors/api/_users.py +70 -58
  80. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  81. meerschaum/connectors/instance/__init__.py +10 -0
  82. meerschaum/connectors/instance/_pipes.py +442 -0
  83. meerschaum/connectors/instance/_plugins.py +151 -0
  84. meerschaum/connectors/instance/_tokens.py +296 -0
  85. meerschaum/connectors/instance/_users.py +181 -0
  86. meerschaum/connectors/parse.py +4 -1
  87. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  88. meerschaum/connectors/sql/_cli.py +12 -11
  89. meerschaum/connectors/sql/_create_engine.py +9 -168
  90. meerschaum/connectors/sql/_fetch.py +2 -18
  91. meerschaum/connectors/sql/_pipes.py +156 -190
  92. meerschaum/connectors/sql/_plugins.py +29 -0
  93. meerschaum/connectors/sql/_sql.py +46 -21
  94. meerschaum/connectors/sql/_users.py +29 -2
  95. meerschaum/connectors/sql/tables/__init__.py +1 -1
  96. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  97. meerschaum/connectors/valkey/_pipes.py +53 -26
  98. meerschaum/connectors/valkey/_plugins.py +2 -26
  99. meerschaum/core/Pipe/__init__.py +59 -19
  100. meerschaum/core/Pipe/_attributes.py +412 -90
  101. meerschaum/core/Pipe/_bootstrap.py +54 -24
  102. meerschaum/core/Pipe/_data.py +96 -18
  103. meerschaum/core/Pipe/_dtypes.py +48 -18
  104. meerschaum/core/Pipe/_edit.py +14 -4
  105. meerschaum/core/Pipe/_fetch.py +1 -1
  106. meerschaum/core/Pipe/_show.py +5 -5
  107. meerschaum/core/Pipe/_sync.py +118 -193
  108. meerschaum/core/Pipe/_verify.py +4 -4
  109. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  110. meerschaum/core/Plugin/__init__.py +1 -1
  111. meerschaum/core/Token/_Token.py +220 -0
  112. meerschaum/core/Token/__init__.py +12 -0
  113. meerschaum/core/User/_User.py +34 -8
  114. meerschaum/core/User/__init__.py +9 -1
  115. meerschaum/core/__init__.py +1 -0
  116. meerschaum/jobs/_Job.py +3 -2
  117. meerschaum/jobs/__init__.py +3 -2
  118. meerschaum/jobs/systemd.py +1 -1
  119. meerschaum/models/__init__.py +35 -0
  120. meerschaum/models/pipes.py +247 -0
  121. meerschaum/models/tokens.py +38 -0
  122. meerschaum/models/users.py +26 -0
  123. meerschaum/plugins/__init__.py +22 -7
  124. meerschaum/plugins/bootstrap.py +2 -1
  125. meerschaum/utils/_get_pipes.py +68 -27
  126. meerschaum/utils/daemon/Daemon.py +2 -1
  127. meerschaum/utils/daemon/__init__.py +30 -2
  128. meerschaum/utils/dataframe.py +473 -81
  129. meerschaum/utils/debug.py +15 -15
  130. meerschaum/utils/dtypes/__init__.py +473 -34
  131. meerschaum/utils/dtypes/sql.py +368 -28
  132. meerschaum/utils/formatting/__init__.py +1 -1
  133. meerschaum/utils/formatting/_pipes.py +5 -4
  134. meerschaum/utils/formatting/_shell.py +11 -9
  135. meerschaum/utils/misc.py +246 -148
  136. meerschaum/utils/packages/__init__.py +10 -27
  137. meerschaum/utils/packages/_packages.py +41 -34
  138. meerschaum/utils/pipes.py +181 -0
  139. meerschaum/utils/process.py +1 -1
  140. meerschaum/utils/prompt.py +3 -1
  141. meerschaum/utils/schedule.py +2 -1
  142. meerschaum/utils/sql.py +121 -44
  143. meerschaum/utils/typing.py +1 -4
  144. meerschaum/utils/venv/_Venv.py +2 -2
  145. meerschaum/utils/venv/__init__.py +5 -7
  146. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/METADATA +92 -96
  147. meerschaum-3.0.0rc2.dist-info/RECORD +283 -0
  148. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/WHEEL +1 -1
  149. meerschaum-3.0.0rc2.dist-info/licenses/NOTICE +2 -0
  150. meerschaum/api/models/_interfaces.py +0 -15
  151. meerschaum/api/models/_locations.py +0 -15
  152. meerschaum/api/models/_metrics.py +0 -15
  153. meerschaum/config/static/__init__.py +0 -186
  154. meerschaum-2.9.5.dist-info/RECORD +0 -263
  155. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  156. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  157. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/top_level.txt +0 -0
  158. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/zip-safe +0 -0
@@ -247,7 +247,7 @@ def print_tuple(
247
247
  If `True`, use the default emoji and color scheme.
248
248
 
249
249
  """
250
- from meerschaum.config.static import STATIC_CONFIG
250
+ from meerschaum._internal.static import STATIC_CONFIG
251
251
  do_print = True
252
252
 
253
253
  omit_messages = STATIC_CONFIG['system']['success']['ignore']
@@ -12,6 +12,7 @@ import meerschaum as mrsm
12
12
  from meerschaum.utils.typing import PipesDict, Dict, Union, Optional, SuccessTuple, Any, List
13
13
  from meerschaum.config import get_config
14
14
 
15
+
15
16
  def pprint_pipes(pipes: PipesDict) -> None:
16
17
  """Print a stylized tree of a Pipes dictionary.
17
18
  Supports ANSI and UNICODE global settings."""
@@ -379,10 +380,10 @@ def highlight_pipes(message: str) -> str:
379
380
 
380
381
 
381
382
  def format_pipe_success_tuple(
382
- pipe: mrsm.Pipe,
383
- success_tuple: SuccessTuple,
384
- nopretty: bool = False,
385
- ) -> str:
383
+ pipe: mrsm.Pipe,
384
+ success_tuple: SuccessTuple,
385
+ nopretty: bool = False,
386
+ ) -> str:
386
387
  """
387
388
  Return a formatted string of a pipe and its resulting SuccessTuple.
388
389
 
@@ -11,15 +11,12 @@ from meerschaum.utils.threading import Lock
11
11
  _locks = {'_tried_clear_command': Lock()}
12
12
 
13
13
 
14
- def make_header(
15
- message: str,
16
- ruler: str = '─',
17
- ) -> str:
14
+ def make_header(message: str, ruler: str = '─', left_pad: int = 2) -> str:
18
15
  """Format a message string with a ruler.
19
16
  Length of the ruler is the length of the longest word.
20
17
 
21
18
  Example:
22
- 'My\nheader' -> 'My\nheader\n──────'
19
+ 'My\nheader' -> ' My\n header\n ──────'
23
20
  """
24
21
 
25
22
  from meerschaum.utils.formatting import ANSI, UNICODE, colored
@@ -32,10 +29,15 @@ def make_header(
32
29
  if length > max_length:
33
30
  max_length = length
34
31
 
35
- s = message + "\n"
36
- for i in range(max_length):
37
- s += ruler
38
- return s
32
+ left_buffer = left_pad * ' '
33
+
34
+ return (
35
+ left_buffer
36
+ + message.replace('\n', '\n' + left_buffer)
37
+ + "\n"
38
+ + left_buffer
39
+ + (ruler * max_length)
40
+ )
39
41
 
40
42
 
41
43
  _tried_clear_command = None
meerschaum/utils/misc.py CHANGED
@@ -7,6 +7,7 @@ Miscellaneous functions go here
7
7
 
8
8
  from __future__ import annotations
9
9
  import sys
10
+ import functools
10
11
  from datetime import timedelta, datetime, timezone
11
12
  from meerschaum.utils.typing import (
12
13
  Union,
@@ -46,11 +47,11 @@ __pdoc__: Dict[str, bool] = {
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
@@ -311,7 +399,7 @@ def get_cols_lines(default_cols: int = 100, default_lines: int = 120) -> Tuple[i
311
399
  try:
312
400
  size = os.get_terminal_size()
313
401
  _cols, _lines = size.columns, size.lines
314
- except Exception as e:
402
+ except Exception:
315
403
  _cols, _lines = (
316
404
  int(os.environ.get('COLUMNS', str(default_cols))),
317
405
  int(os.environ.get('LINES', str(default_lines))),
@@ -367,7 +455,7 @@ def sorted_dict(d: Dict[Any, Any]) -> Dict[Any, Any]:
367
455
  """
368
456
  try:
369
457
  return {key: value for key, value in sorted(d.items(), key=lambda item: item[1])}
370
- except Exception as e:
458
+ except Exception:
371
459
  return d
372
460
 
373
461
  def flatten_pipes_dict(pipes_dict: PipesDict) -> List[Pipe]:
@@ -391,81 +479,13 @@ def flatten_pipes_dict(pipes_dict: PipesDict) -> List[Pipe]:
391
479
  return pipes_list
392
480
 
393
481
 
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
482
  def timed_input(
463
- seconds: int = 10,
464
- timeout_message: str = "",
465
- prompt: str = "",
466
- icon: bool = False,
467
- **kw
468
- ) -> Union[str, None]:
483
+ seconds: int = 10,
484
+ timeout_message: str = "",
485
+ prompt: str = "",
486
+ icon: bool = False,
487
+ **kw
488
+ ) -> Union[str, None]:
469
489
  """
470
490
  Accept user input only for a brief period of time.
471
491
 
@@ -515,52 +535,6 @@ def timed_input(
515
535
  signal.alarm(0) # cancel alarm
516
536
 
517
537
 
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
538
  def enforce_gevent_monkey_patch():
565
539
  """
566
540
  Check if gevent monkey patching is enabled, and if not, then apply patching.
@@ -634,10 +608,10 @@ def string_width(string: str, widest: bool = True) -> int:
634
608
  return _widest()
635
609
 
636
610
  def _pyinstaller_traverse_dir(
637
- directory: str,
638
- ignore_patterns: Iterable[str] = ('.pyc', 'dist', 'build', '.git', '.log'),
639
- include_dotfiles: bool = False
640
- ) -> list:
611
+ directory: str,
612
+ ignore_patterns: Iterable[str] = ('.pyc', 'dist', 'build', '.git', '.log'),
613
+ include_dotfiles: bool = False
614
+ ) -> list:
641
615
  """
642
616
  Recursively traverse a directory and return a list of its contents.
643
617
  """
@@ -677,6 +651,43 @@ def _pyinstaller_traverse_dir(
677
651
  return paths
678
652
 
679
653
 
654
+ def get_val_from_dict_path(d: Dict[Any, Any], path: Tuple[Any, ...]) -> Any:
655
+ """
656
+ Get a value from a dictionary with a tuple of keys.
657
+
658
+ Parameters
659
+ ----------
660
+ d: Dict[Any, Any]
661
+ The dictionary to search.
662
+
663
+ path: Tuple[Any, ...]
664
+ The path of keys to traverse.
665
+
666
+ Returns
667
+ -------
668
+ The value from the end of the path.
669
+ """
670
+ return functools.reduce(lambda di, key: di[key], path, d)
671
+
672
+
673
+ def set_val_in_dict_path(d: Dict[Any, Any], path: Tuple[Any, ...], val: Any) -> None:
674
+ """
675
+ Set a value in a dictionary with a tuple of keys.
676
+
677
+ Parameters
678
+ ----------
679
+ d: Dict[Any, Any]
680
+ The dictionary to search.
681
+
682
+ path: Tuple[Any, ...]
683
+ The path of keys to traverse.
684
+
685
+ val: Any
686
+ The value to set at the end of the path.
687
+ """
688
+ get_val_from_dict_path(d, path[:-1])[path[-1]] = val
689
+
690
+
680
691
  def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, Any]:
681
692
  """
682
693
  Recursively replace passwords in a dictionary.
@@ -717,7 +728,7 @@ def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, An
717
728
  from meerschaum.connectors.sql import SQLConnector
718
729
  try:
719
730
  uri_params = SQLConnector.parse_uri(v)
720
- except Exception as e:
731
+ except Exception:
721
732
  uri_params = None
722
733
  if not uri_params:
723
734
  continue
@@ -877,6 +888,7 @@ def dict_from_od(od: collections.OrderedDict) -> Dict[Any, Any]:
877
888
  _d[k] = dict_from_od(v)
878
889
  return _d
879
890
 
891
+
880
892
  def remove_ansi(s: str) -> str:
881
893
  """
882
894
  Remove ANSI escape characters from a string.
@@ -1153,7 +1165,7 @@ def items_str(
1153
1165
  return output
1154
1166
 
1155
1167
 
1156
- def interval_str(delta: Union[timedelta, int]) -> str:
1168
+ def interval_str(delta: Union[timedelta, int], round_unit: bool = False) -> str:
1157
1169
  """
1158
1170
  Return a human-readable string for a `timedelta` (or `int` minutes).
1159
1171
 
@@ -1162,20 +1174,57 @@ def interval_str(delta: Union[timedelta, int]) -> str:
1162
1174
  delta: Union[timedelta, int]
1163
1175
  The interval to print. If `delta` is an integer, assume it corresponds to minutes.
1164
1176
 
1177
+ round_unit: bool, default False
1178
+ If `True`, round the output to a single unit.
1179
+
1165
1180
  Returns
1166
1181
  -------
1167
1182
  A formatted string, fit for human eyes.
1168
1183
  """
1169
1184
  from meerschaum.utils.packages import attempt_import
1170
- if is_int(delta):
1185
+ if is_int(str(delta)) and not round_unit:
1171
1186
  return str(delta)
1172
- humanfriendly = attempt_import('humanfriendly')
1187
+
1188
+ humanfriendly = attempt_import('humanfriendly', lazy=False)
1173
1189
  delta_seconds = (
1174
1190
  delta.total_seconds()
1175
- if isinstance(delta, timedelta)
1191
+ if hasattr(delta, 'total_seconds')
1176
1192
  else (delta * 60)
1177
1193
  )
1178
- return humanfriendly.format_timespan(delta_seconds)
1194
+
1195
+ is_negative = delta_seconds < 0
1196
+ delta_seconds = abs(delta_seconds)
1197
+ replace_units = {}
1198
+
1199
+ if round_unit:
1200
+ if delta_seconds < 1:
1201
+ delta_seconds = round(delta_seconds, 2)
1202
+ elif delta_seconds < 60:
1203
+ delta_seconds = int(delta_seconds)
1204
+ elif delta_seconds < 3600:
1205
+ delta_seconds = int(delta_seconds / 60) * 60
1206
+ elif delta_seconds < 86400:
1207
+ delta_seconds = int(delta_seconds / 3600) * 3600
1208
+ elif delta_seconds < (86400 * 7):
1209
+ delta_seconds = int(delta_seconds / 86400) * 86400
1210
+ elif delta_seconds < (86400 * 7 * 4):
1211
+ delta_seconds = int(delta_seconds / (86400 * 7)) * (86400 * 7)
1212
+ elif delta_seconds < (86400 * 7 * 4 * 13):
1213
+ delta_seconds = int(delta_seconds / (86400 * 7 * 4)) * (86400 * 7)
1214
+ replace_units['weeks'] = 'months'
1215
+ else:
1216
+ delta_seconds = int(delta_seconds / (86400 * 364)) * (86400 * 364)
1217
+
1218
+ delta_str = humanfriendly.format_timespan(delta_seconds)
1219
+ if ',' in delta_str and round_unit:
1220
+ delta_str = delta_str.split(',')[0]
1221
+ elif ' and ' in delta_str and round_unit:
1222
+ delta_str = delta_str.split(' and ')[0]
1223
+
1224
+ for parsed_unit, replacement_unit in replace_units.items():
1225
+ delta_str = delta_str.replace(parsed_unit, replacement_unit)
1226
+
1227
+ return delta_str + (' ago' if is_negative else '')
1179
1228
 
1180
1229
 
1181
1230
  def is_docker_available() -> bool:
@@ -1406,7 +1455,7 @@ def separate_negation_values(
1406
1455
  If `None`, use the system default (`_`).
1407
1456
  """
1408
1457
  if negation_prefix is None:
1409
- from meerschaum.config.static import STATIC_CONFIG
1458
+ from meerschaum._internal.static import STATIC_CONFIG
1410
1459
  negation_prefix = STATIC_CONFIG['system']['fetch_pipes_keys']['negation_prefix']
1411
1460
  _in_vals, _ex_vals = [], []
1412
1461
  for v in vals:
@@ -1442,7 +1491,7 @@ def get_in_ex_params(params: Optional[Dict[str, Any]]) -> Dict[str, Tuple[List[A
1442
1491
  col: separate_negation_values(
1443
1492
  (
1444
1493
  val
1445
- if isinstance(val, (list, tuple))
1494
+ if isinstance(val, (list, tuple, set)) or hasattr(val, 'astype')
1446
1495
  else [val]
1447
1496
  )
1448
1497
  )
@@ -1610,6 +1659,36 @@ def safely_extract_tar(tarf: 'file', output_dir: Union[str, 'pathlib.Path']) ->
1610
1659
  return safe_extract(tarf, output_dir)
1611
1660
 
1612
1661
 
1662
+ def to_snake_case(name: str) -> str:
1663
+ """
1664
+ Return the given string in snake-case-style.
1665
+
1666
+ Parameters
1667
+ ----------
1668
+ name: str
1669
+ The input text to convert to snake case.
1670
+
1671
+ Returns
1672
+ -------
1673
+ A snake-case version of `name`.
1674
+
1675
+ Examples
1676
+ --------
1677
+ >>> to_snake_case("HelloWorld!")
1678
+ 'hello_world'
1679
+ >>> to_snake_case("This has spaces in it.")
1680
+ 'this_has_spaces_in_it'
1681
+ >>> to_snake_case("already_in_snake_case")
1682
+ 'already_in_snake_case'
1683
+ """
1684
+ import re
1685
+ name = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
1686
+ name = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', name)
1687
+ name = re.sub(r'[^\w\s]', '', name)
1688
+ name = re.sub(r'\s+', '_', name)
1689
+ return name.lower()
1690
+
1691
+
1613
1692
  ##################
1614
1693
  # Legacy imports #
1615
1694
  ##################
@@ -1756,6 +1835,24 @@ def json_serialize_datetime(dt: datetime) -> Union[str, None]:
1756
1835
  return serialize_datetime(dt)
1757
1836
 
1758
1837
 
1838
+ def replace_pipes_in_dict(*args, **kwargs):
1839
+ """
1840
+ Placeholder function to prevent breaking legacy behavior.
1841
+ See `meerschaum.utils.pipes.replace_pipes_in_dict`.
1842
+ """
1843
+ from meerschaum.utils.pipes import replace_pipes_in_dict
1844
+ return replace_pipes_in_dict(*args, **kwargs)
1845
+
1846
+
1847
+ def round_time(*args, **kwargs):
1848
+ """
1849
+ Placeholder function to prevent breaking legacy behavior.
1850
+ See `meerschaum.utils.dtypes.round_time`.
1851
+ """
1852
+ from meerschaum.utils.dtypes import round_time
1853
+ return round_time(*args, **kwargs)
1854
+
1855
+
1759
1856
  _current_module = sys.modules[__name__]
1760
1857
  __all__ = tuple(
1761
1858
  name
@@ -1763,4 +1860,5 @@ __all__ = tuple(
1763
1860
  if callable(obj)
1764
1861
  and name not in __pdoc__
1765
1862
  and getattr(obj, '__module__', None) == _current_module.__name__
1863
+ and not name.startswith('_')
1766
1864
  )