meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc1__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 (153) 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 +17 -1
  5. meerschaum/_internal/entry.py +6 -6
  6. meerschaum/_internal/shell/Shell.py +1 -1
  7. meerschaum/_internal/static.py +372 -0
  8. meerschaum/actions/api.py +12 -2
  9. meerschaum/actions/bootstrap.py +7 -7
  10. meerschaum/actions/edit.py +142 -18
  11. meerschaum/actions/register.py +137 -6
  12. meerschaum/actions/show.py +117 -29
  13. meerschaum/actions/stop.py +4 -1
  14. meerschaum/actions/sync.py +1 -1
  15. meerschaum/actions/tag.py +9 -8
  16. meerschaum/api/__init__.py +9 -2
  17. meerschaum/api/_events.py +39 -2
  18. meerschaum/api/_oauth2.py +118 -8
  19. meerschaum/api/_tokens.py +102 -0
  20. meerschaum/api/dash/__init__.py +0 -1
  21. meerschaum/api/dash/callbacks/custom.py +2 -2
  22. meerschaum/api/dash/callbacks/dashboard.py +102 -18
  23. meerschaum/api/dash/callbacks/plugins.py +0 -1
  24. meerschaum/api/dash/callbacks/register.py +1 -1
  25. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  26. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  27. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  28. meerschaum/api/dash/components.py +30 -8
  29. meerschaum/api/dash/keys.py +19 -93
  30. meerschaum/api/dash/pages/dashboard.py +1 -20
  31. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  32. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  33. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  34. meerschaum/api/dash/pipes.py +94 -59
  35. meerschaum/api/dash/sessions.py +12 -0
  36. meerschaum/api/dash/tokens.py +606 -0
  37. meerschaum/api/dash/websockets.py +1 -1
  38. meerschaum/api/dash/webterm.py +4 -0
  39. meerschaum/api/models/__init__.py +23 -3
  40. meerschaum/api/models/_actions.py +22 -0
  41. meerschaum/api/models/_pipes.py +85 -7
  42. meerschaum/api/models/_tokens.py +81 -0
  43. meerschaum/api/resources/templates/termpage.html +12 -0
  44. meerschaum/api/routes/__init__.py +1 -0
  45. meerschaum/api/routes/_actions.py +3 -4
  46. meerschaum/api/routes/_connectors.py +3 -7
  47. meerschaum/api/routes/_jobs.py +14 -35
  48. meerschaum/api/routes/_login.py +49 -12
  49. meerschaum/api/routes/_misc.py +5 -10
  50. meerschaum/api/routes/_pipes.py +134 -111
  51. meerschaum/api/routes/_plugins.py +38 -28
  52. meerschaum/api/routes/_tokens.py +236 -0
  53. meerschaum/api/routes/_users.py +47 -35
  54. meerschaum/api/routes/_version.py +3 -3
  55. meerschaum/config/__init__.py +43 -20
  56. meerschaum/config/_default.py +32 -5
  57. meerschaum/config/_edit.py +28 -24
  58. meerschaum/config/_environment.py +1 -1
  59. meerschaum/config/_patch.py +6 -6
  60. meerschaum/config/_paths.py +5 -1
  61. meerschaum/config/_read_config.py +65 -34
  62. meerschaum/config/_sync.py +6 -3
  63. meerschaum/config/_version.py +1 -1
  64. meerschaum/config/stack/__init__.py +24 -5
  65. meerschaum/config/static.py +18 -0
  66. meerschaum/connectors/_Connector.py +10 -4
  67. meerschaum/connectors/__init__.py +4 -20
  68. meerschaum/connectors/api/_APIConnector.py +34 -6
  69. meerschaum/connectors/api/_actions.py +2 -2
  70. meerschaum/connectors/api/_jobs.py +1 -1
  71. meerschaum/connectors/api/_login.py +33 -7
  72. meerschaum/connectors/api/_misc.py +2 -2
  73. meerschaum/connectors/api/_pipes.py +15 -14
  74. meerschaum/connectors/api/_plugins.py +2 -2
  75. meerschaum/connectors/api/_request.py +1 -1
  76. meerschaum/connectors/api/_tokens.py +146 -0
  77. meerschaum/connectors/api/_users.py +70 -58
  78. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  79. meerschaum/connectors/instance/__init__.py +10 -0
  80. meerschaum/connectors/instance/_pipes.py +442 -0
  81. meerschaum/connectors/instance/_plugins.py +151 -0
  82. meerschaum/connectors/instance/_tokens.py +296 -0
  83. meerschaum/connectors/instance/_users.py +181 -0
  84. meerschaum/connectors/parse.py +4 -1
  85. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  86. meerschaum/connectors/sql/_cli.py +12 -11
  87. meerschaum/connectors/sql/_create_engine.py +6 -154
  88. meerschaum/connectors/sql/_fetch.py +2 -18
  89. meerschaum/connectors/sql/_pipes.py +42 -31
  90. meerschaum/connectors/sql/_plugins.py +29 -0
  91. meerschaum/connectors/sql/_sql.py +8 -1
  92. meerschaum/connectors/sql/_users.py +29 -2
  93. meerschaum/connectors/sql/tables/__init__.py +1 -1
  94. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  95. meerschaum/connectors/valkey/_pipes.py +9 -10
  96. meerschaum/connectors/valkey/_plugins.py +2 -26
  97. meerschaum/core/Pipe/__init__.py +31 -14
  98. meerschaum/core/Pipe/_attributes.py +156 -58
  99. meerschaum/core/Pipe/_bootstrap.py +54 -24
  100. meerschaum/core/Pipe/_data.py +41 -1
  101. meerschaum/core/Pipe/_dtypes.py +29 -14
  102. meerschaum/core/Pipe/_edit.py +12 -4
  103. meerschaum/core/Pipe/_show.py +5 -5
  104. meerschaum/core/Pipe/_sync.py +48 -53
  105. meerschaum/core/Pipe/_verify.py +1 -1
  106. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  107. meerschaum/core/Plugin/__init__.py +1 -1
  108. meerschaum/core/Token/_Token.py +221 -0
  109. meerschaum/core/Token/__init__.py +12 -0
  110. meerschaum/core/User/_User.py +34 -8
  111. meerschaum/core/User/__init__.py +9 -1
  112. meerschaum/core/__init__.py +1 -0
  113. meerschaum/jobs/_Job.py +3 -2
  114. meerschaum/jobs/__init__.py +3 -2
  115. meerschaum/jobs/systemd.py +1 -1
  116. meerschaum/models/__init__.py +35 -0
  117. meerschaum/models/pipes.py +247 -0
  118. meerschaum/models/tokens.py +38 -0
  119. meerschaum/models/users.py +26 -0
  120. meerschaum/plugins/__init__.py +22 -7
  121. meerschaum/plugins/bootstrap.py +2 -1
  122. meerschaum/utils/_get_pipes.py +68 -27
  123. meerschaum/utils/daemon/Daemon.py +2 -1
  124. meerschaum/utils/daemon/__init__.py +30 -2
  125. meerschaum/utils/dataframe.py +95 -14
  126. meerschaum/utils/dtypes/__init__.py +91 -18
  127. meerschaum/utils/dtypes/sql.py +44 -0
  128. meerschaum/utils/formatting/__init__.py +1 -1
  129. meerschaum/utils/formatting/_pipes.py +5 -4
  130. meerschaum/utils/formatting/_shell.py +11 -9
  131. meerschaum/utils/misc.py +237 -80
  132. meerschaum/utils/packages/__init__.py +3 -6
  133. meerschaum/utils/packages/_packages.py +34 -32
  134. meerschaum/utils/pipes.py +181 -0
  135. meerschaum/utils/process.py +1 -1
  136. meerschaum/utils/prompt.py +3 -1
  137. meerschaum/utils/schedule.py +1 -0
  138. meerschaum/utils/sql.py +114 -37
  139. meerschaum/utils/typing.py +1 -4
  140. meerschaum/utils/venv/_Venv.py +2 -2
  141. meerschaum/utils/venv/__init__.py +5 -7
  142. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/METADATA +88 -80
  143. meerschaum-3.0.0rc1.dist-info/RECORD +282 -0
  144. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/WHEEL +1 -1
  145. meerschaum/api/models/_interfaces.py +0 -15
  146. meerschaum/api/models/_locations.py +0 -15
  147. meerschaum/api/models/_metrics.py +0 -15
  148. meerschaum/config/static/__init__.py +0 -186
  149. meerschaum-2.9.5.dist-info/RECORD +0 -263
  150. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  151. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
  152. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/top_level.txt +0 -0
  153. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/zip-safe +0 -0
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]:
@@ -460,12 +548,12 @@ def round_time(
460
548
 
461
549
 
462
550
  def timed_input(
463
- seconds: int = 10,
464
- timeout_message: str = "",
465
- prompt: str = "",
466
- icon: bool = False,
467
- **kw
468
- ) -> Union[str, None]:
551
+ seconds: int = 10,
552
+ timeout_message: str = "",
553
+ prompt: str = "",
554
+ icon: bool = False,
555
+ **kw
556
+ ) -> Union[str, None]:
469
557
  """
470
558
  Accept user input only for a brief period of time.
471
559
 
@@ -515,52 +603,6 @@ def timed_input(
515
603
  signal.alarm(0) # cancel alarm
516
604
 
517
605
 
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
606
  def enforce_gevent_monkey_patch():
565
607
  """
566
608
  Check if gevent monkey patching is enabled, and if not, then apply patching.
@@ -634,10 +676,10 @@ def string_width(string: str, widest: bool = True) -> int:
634
676
  return _widest()
635
677
 
636
678
  def _pyinstaller_traverse_dir(
637
- directory: str,
638
- ignore_patterns: Iterable[str] = ('.pyc', 'dist', 'build', '.git', '.log'),
639
- include_dotfiles: bool = False
640
- ) -> list:
679
+ directory: str,
680
+ ignore_patterns: Iterable[str] = ('.pyc', 'dist', 'build', '.git', '.log'),
681
+ include_dotfiles: bool = False
682
+ ) -> list:
641
683
  """
642
684
  Recursively traverse a directory and return a list of its contents.
643
685
  """
@@ -677,6 +719,43 @@ def _pyinstaller_traverse_dir(
677
719
  return paths
678
720
 
679
721
 
722
+ def get_val_from_dict_path(d: Dict[Any, Any], path: Tuple[Any, ...]) -> Any:
723
+ """
724
+ Get a value from a dictionary with a tuple of keys.
725
+
726
+ Parameters
727
+ ----------
728
+ d: Dict[Any, Any]
729
+ The dictionary to search.
730
+
731
+ path: Tuple[Any, ...]
732
+ The path of keys to traverse.
733
+
734
+ Returns
735
+ -------
736
+ The value from the end of the path.
737
+ """
738
+ return functools.reduce(lambda di, key: di[key], path, d)
739
+
740
+
741
+ def set_val_in_dict_path(d: Dict[Any, Any], path: Tuple[Any, ...], val: Any) -> None:
742
+ """
743
+ Set a value in a dictionary with a tuple of keys.
744
+
745
+ Parameters
746
+ ----------
747
+ d: Dict[Any, Any]
748
+ The dictionary to search.
749
+
750
+ path: Tuple[Any, ...]
751
+ The path of keys to traverse.
752
+
753
+ val: Any
754
+ The value to set at the end of the path.
755
+ """
756
+ get_val_from_dict_path(d, path[:-1])[path[-1]] = val
757
+
758
+
680
759
  def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, Any]:
681
760
  """
682
761
  Recursively replace passwords in a dictionary.
@@ -717,7 +796,7 @@ def replace_password(d: Dict[str, Any], replace_with: str = '*') -> Dict[str, An
717
796
  from meerschaum.connectors.sql import SQLConnector
718
797
  try:
719
798
  uri_params = SQLConnector.parse_uri(v)
720
- except Exception as e:
799
+ except Exception:
721
800
  uri_params = None
722
801
  if not uri_params:
723
802
  continue
@@ -877,6 +956,7 @@ def dict_from_od(od: collections.OrderedDict) -> Dict[Any, Any]:
877
956
  _d[k] = dict_from_od(v)
878
957
  return _d
879
958
 
959
+
880
960
  def remove_ansi(s: str) -> str:
881
961
  """
882
962
  Remove ANSI escape characters from a string.
@@ -1153,7 +1233,7 @@ def items_str(
1153
1233
  return output
1154
1234
 
1155
1235
 
1156
- def interval_str(delta: Union[timedelta, int]) -> str:
1236
+ def interval_str(delta: Union[timedelta, int], round_unit: bool = False) -> str:
1157
1237
  """
1158
1238
  Return a human-readable string for a `timedelta` (or `int` minutes).
1159
1239
 
@@ -1162,20 +1242,57 @@ def interval_str(delta: Union[timedelta, int]) -> str:
1162
1242
  delta: Union[timedelta, int]
1163
1243
  The interval to print. If `delta` is an integer, assume it corresponds to minutes.
1164
1244
 
1245
+ round_unit: bool, default False
1246
+ If `True`, round the output to a single unit.
1247
+
1165
1248
  Returns
1166
1249
  -------
1167
1250
  A formatted string, fit for human eyes.
1168
1251
  """
1169
1252
  from meerschaum.utils.packages import attempt_import
1170
- if is_int(delta):
1253
+ if is_int(str(delta)) and not round_unit:
1171
1254
  return str(delta)
1172
- humanfriendly = attempt_import('humanfriendly')
1255
+
1256
+ humanfriendly = attempt_import('humanfriendly', lazy=False)
1173
1257
  delta_seconds = (
1174
1258
  delta.total_seconds()
1175
- if isinstance(delta, timedelta)
1259
+ if hasattr(delta, 'total_seconds')
1176
1260
  else (delta * 60)
1177
1261
  )
1178
- return humanfriendly.format_timespan(delta_seconds)
1262
+
1263
+ is_negative = delta_seconds < 0
1264
+ delta_seconds = abs(delta_seconds)
1265
+ replace_units = {}
1266
+
1267
+ if round_unit:
1268
+ if delta_seconds < 1:
1269
+ delta_seconds = round(delta_seconds, 2)
1270
+ elif delta_seconds < 60:
1271
+ delta_seconds = int(delta_seconds)
1272
+ elif delta_seconds < 3600:
1273
+ delta_seconds = int(delta_seconds / 60) * 60
1274
+ elif delta_seconds < 86400:
1275
+ delta_seconds = int(delta_seconds / 3600) * 3600
1276
+ elif delta_seconds < (86400 * 7):
1277
+ delta_seconds = int(delta_seconds / 86400) * 86400
1278
+ elif delta_seconds < (86400 * 7 * 4):
1279
+ delta_seconds = int(delta_seconds / (86400 * 7)) * (86400 * 7)
1280
+ elif delta_seconds < (86400 * 7 * 4 * 13):
1281
+ delta_seconds = int(delta_seconds / (86400 * 7 * 4)) * (86400 * 7)
1282
+ replace_units['weeks'] = 'months'
1283
+ else:
1284
+ delta_seconds = int(delta_seconds / (86400 * 364)) * (86400 * 364)
1285
+
1286
+ delta_str = humanfriendly.format_timespan(delta_seconds)
1287
+ if ',' in delta_str and round_unit:
1288
+ delta_str = delta_str.split(',')[0]
1289
+ elif ' and ' in delta_str and round_unit:
1290
+ delta_str = delta_str.split(' and ')[0]
1291
+
1292
+ for parsed_unit, replacement_unit in replace_units.items():
1293
+ delta_str = delta_str.replace(parsed_unit, replacement_unit)
1294
+
1295
+ return delta_str + (' ago' if is_negative else '')
1179
1296
 
1180
1297
 
1181
1298
  def is_docker_available() -> bool:
@@ -1406,7 +1523,7 @@ def separate_negation_values(
1406
1523
  If `None`, use the system default (`_`).
1407
1524
  """
1408
1525
  if negation_prefix is None:
1409
- from meerschaum.config.static import STATIC_CONFIG
1526
+ from meerschaum._internal.static import STATIC_CONFIG
1410
1527
  negation_prefix = STATIC_CONFIG['system']['fetch_pipes_keys']['negation_prefix']
1411
1528
  _in_vals, _ex_vals = [], []
1412
1529
  for v in vals:
@@ -1442,7 +1559,7 @@ def get_in_ex_params(params: Optional[Dict[str, Any]]) -> Dict[str, Tuple[List[A
1442
1559
  col: separate_negation_values(
1443
1560
  (
1444
1561
  val
1445
- if isinstance(val, (list, tuple))
1562
+ if isinstance(val, (list, tuple, set)) or hasattr(val, 'astype')
1446
1563
  else [val]
1447
1564
  )
1448
1565
  )
@@ -1610,6 +1727,36 @@ def safely_extract_tar(tarf: 'file', output_dir: Union[str, 'pathlib.Path']) ->
1610
1727
  return safe_extract(tarf, output_dir)
1611
1728
 
1612
1729
 
1730
+ def to_snake_case(name: str) -> str:
1731
+ """
1732
+ Return the given string in snake-case-style.
1733
+
1734
+ Parameters
1735
+ ----------
1736
+ name: str
1737
+ The input text to convert to snake case.
1738
+
1739
+ Returns
1740
+ -------
1741
+ A snake-case version of `name`.
1742
+
1743
+ Examples
1744
+ --------
1745
+ >>> to_snake_case("HelloWorld!")
1746
+ 'hello_world'
1747
+ >>> to_snake_case("This has spaces in it.")
1748
+ 'this_has_spaces_in_it'
1749
+ >>> to_snake_case("already_in_snake_case")
1750
+ 'already_in_snake_case'
1751
+ """
1752
+ import re
1753
+ name = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name)
1754
+ name = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', name)
1755
+ name = re.sub(r'[^\w\s]', '', name)
1756
+ name = re.sub(r'\s+', '_', name)
1757
+ return name.lower()
1758
+
1759
+
1613
1760
  ##################
1614
1761
  # Legacy imports #
1615
1762
  ##################
@@ -1756,6 +1903,15 @@ def json_serialize_datetime(dt: datetime) -> Union[str, None]:
1756
1903
  return serialize_datetime(dt)
1757
1904
 
1758
1905
 
1906
+ def replace_pipes_in_dict(*args, **kwargs):
1907
+ """
1908
+ Placeholder function to prevent breaking legacy behavior.
1909
+ See `meerschaum.utils.pipes.replace_pipes_in_dict`.
1910
+ """
1911
+ from meerschaum.utils.pipes import replace_pipes_in_dict
1912
+ return replace_pipes_in_dict(*args, **kwargs)
1913
+
1914
+
1759
1915
  _current_module = sys.modules[__name__]
1760
1916
  __all__ = tuple(
1761
1917
  name
@@ -1763,4 +1919,5 @@ __all__ = tuple(
1763
1919
  if callable(obj)
1764
1920
  and name not in __pdoc__
1765
1921
  and getattr(obj, '__module__', None) == _current_module.__name__
1922
+ and not name.startswith('_')
1766
1923
  )
@@ -755,7 +755,7 @@ def get_pip(
755
755
  import subprocess
756
756
  from meerschaum.utils.misc import wget
757
757
  from meerschaum.config._paths import CACHE_RESOURCES_PATH
758
- from meerschaum.config.static import STATIC_CONFIG
758
+ from meerschaum._internal.static import STATIC_CONFIG
759
759
  url = STATIC_CONFIG['system']['urls']['get-pip.py']
760
760
  dest = CACHE_RESOURCES_PATH / 'get-pip.py'
761
761
  try:
@@ -837,7 +837,7 @@ def pip_install(
837
837
 
838
838
  """
839
839
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
840
- from meerschaum.config.static import STATIC_CONFIG
840
+ from meerschaum._internal.static import STATIC_CONFIG
841
841
  from meerschaum.utils.warnings import warn
842
842
  if args is None:
843
843
  args = ['--upgrade'] if not _uninstall else []
@@ -1500,15 +1500,12 @@ def import_pandas(
1500
1500
  def import_rich(
1501
1501
  lazy: bool = True,
1502
1502
  debug: bool = False,
1503
- **kw : Any
1503
+ **kw: Any
1504
1504
  ) -> 'ModuleType':
1505
1505
  """
1506
1506
  Quality of life function for importing `rich`.
1507
1507
  """
1508
1508
  from meerschaum.utils.formatting import ANSI, UNICODE
1509
- if not ANSI and not UNICODE:
1510
- return None
1511
-
1512
1509
  ## need typing_extensions for `from rich import box`
1513
1510
  typing_extensions = attempt_import(
1514
1511
  'typing_extensions', lazy=False, debug=debug
@@ -54,23 +54,25 @@ packages: Dict[str, Dict[str, str]] = {
54
54
  'virtualenv' : 'virtualenv>=20.1.0',
55
55
  'attrs' : 'attrs>=24.2.0',
56
56
  'uv' : 'uv>=0.2.11',
57
+ 'pydantic' : 'pydantic>=2.11.7',
58
+ 'annotated-types' : 'annotated-types>=0.7.0',
57
59
  },
58
60
  '_internal' : {
59
61
  'apscheduler' : (
60
62
  f"{_MRSM_PACKAGE_ARCHIVES_PREFIX}"
61
- "APScheduler-4.0.0a5.post87+mrsm-py3-none-any.whl>=4.0.0a5"
63
+ "apscheduler-4.0.0a6.post8+mrsm-py3-none-any.whl>=4.0.0a6"
62
64
  ),
63
- 'dataclass_wizard' : 'dataclass-wizard>=0.28.0',
65
+ 'dataclass_wizard' : 'dataclass-wizard>=0.35.0',
64
66
  },
65
67
  'jobs': {
66
- 'dill' : 'dill>=0.3.3',
67
- 'daemon' : 'python-daemon>=0.2.3',
68
- 'watchfiles' : 'watchfiles>=0.21.0',
69
- 'psutil' : 'psutil>=5.8.0',
68
+ 'dill' : 'dill>=0.4.0',
69
+ 'daemon' : 'python-daemon>=3.1.2',
70
+ 'watchfiles' : 'watchfiles>=1.1.0',
71
+ 'psutil' : 'psutil>=7.0.0',
70
72
  },
71
73
  'drivers': {
72
74
  'cryptography' : 'cryptography>=38.0.1',
73
- 'psycopg' : 'psycopg[binary]>=3.2.1',
75
+ 'psycopg' : 'psycopg[binary]>=3.2.9',
74
76
  'pymysql' : 'PyMySQL>=0.9.0',
75
77
  'aiomysql' : 'aiomysql>=0.0.21',
76
78
  'sqlalchemy_cockroachdb' : 'sqlalchemy-cockroachdb>=2.0.0',
@@ -122,7 +124,7 @@ packages: Dict[str, Dict[str, str]] = {
122
124
  'mkdocs_section_index' : 'mkdocs-section-index>=0.3.3',
123
125
  'mkdocs_linkcheck' : 'mkdocs-linkcheck>=1.0.6',
124
126
  'mkdocs_redirects' : 'mkdocs-redirects>=1.0.4',
125
- 'jinja2' : 'jinja2==3.0.3',
127
+ 'jinja2' : 'jinja2>=3.1.6',
126
128
  },
127
129
  'gui': {
128
130
  'webview' : 'pywebview>=3.6.3',
@@ -137,43 +139,43 @@ packages: Dict[str, Dict[str, str]] = {
137
139
  },
138
140
  }
139
141
  packages['sql'] = {
140
- 'numpy' : 'numpy>=1.18.5',
141
- 'pandas' : 'pandas[parquet]>=2.0.1',
142
- 'pyarrow' : 'pyarrow>=16.1.0',
142
+ 'numpy' : 'numpy>=2.3.1',
143
+ 'pandas' : 'pandas[parquet]>=2.3.1',
144
+ 'pyarrow' : 'pyarrow>=20.0.0',
143
145
  'dask' : 'dask[complete]>=2024.12.1',
144
146
  'partd' : 'partd>=1.4.2',
145
147
  'pytz' : 'pytz',
146
- 'joblib' : 'joblib>=0.17.0',
147
- 'sqlalchemy' : 'SQLAlchemy>=2.0.5',
148
+ 'joblib' : 'joblib>=1.5.1',
149
+ 'sqlalchemy' : 'SQLAlchemy>=2.0.41',
148
150
  'geoalchemy' : 'GeoAlchemy2>=0.17.1',
149
- 'databases' : 'databases>=0.4.0',
150
- 'aiosqlite' : 'aiosqlite>=0.16.0',
151
- 'asyncpg' : 'asyncpg>=0.21.0',
151
+ 'databases' : 'databases>=0.9.0',
152
+ 'aiosqlite' : 'aiosqlite>=0.21.0',
153
+ 'asyncpg' : 'asyncpg>=0.30.0',
152
154
  }
153
155
  packages['sql'].update(packages['drivers'])
154
156
  packages['sql'].update(packages['core'])
155
157
  packages['sql'].update(packages['gis'])
156
158
  packages['dash'] = {
157
- 'flask_compress' : 'Flask-Compress>=1.10.1',
158
- 'dash' : 'dash>=2.6.2',
159
+ 'flask_compress' : 'Flask-Compress>=1.17.0',
160
+ 'dash' : 'dash>=3.1.1',
159
161
  'dash_bootstrap_components' : 'dash-bootstrap-components>=1.7.1',
160
162
  'dash_ace' : 'dash-ace>=0.2.1',
161
- 'dash_extensions' : 'dash-extensions>=1.0.4',
162
- 'dash_daq' : 'dash-daq>=0.5.0',
163
- 'terminado' : 'terminado>=0.12.1',
164
- 'tornado' : 'tornado>=6.1.0',
163
+ 'dash_extensions' : 'dash-extensions>=2.0.4',
164
+ 'dash_daq' : 'dash-daq>=0.6.0',
165
+ 'terminado' : 'terminado>=0.18.1',
166
+ 'tornado' : 'tornado>=6.5.1',
165
167
  }
166
168
  packages['api'] = {
167
- 'uvicorn' : 'uvicorn[standard]>=0.29.0',
168
- 'gunicorn' : 'gunicorn>=22.0.0',
169
- 'dotenv' : 'python-dotenv>=0.20.0',
170
- 'websockets' : 'websockets>=11.0.3',
171
- 'fastapi' : 'fastapi>=0.111.0',
172
- 'fastapi_login' : 'fastapi-login>=1.7.2',
173
- 'multipart' : 'python-multipart>=0.0.9',
174
- 'httpx' : 'httpx>=0.27.2',
175
- 'httpcore' : 'httpcore>=1.0.6',
176
- 'valkey' : 'valkey>=6.0.0',
169
+ 'uvicorn' : 'uvicorn[standard]>=0.35.0',
170
+ 'gunicorn' : 'gunicorn>=23.0.0',
171
+ 'dotenv' : 'python-dotenv>=1.1.1',
172
+ 'websockets' : 'websockets>=15.0.1',
173
+ 'fastapi' : 'fastapi>=0.116.0',
174
+ 'fastapi_login' : 'fastapi-login>=1.10.3',
175
+ 'multipart' : 'python-multipart>=0.0.20',
176
+ 'httpx' : 'httpx>=0.28.1',
177
+ 'httpcore' : 'httpcore>=1.0.9',
178
+ 'valkey' : 'valkey>=6.1.0',
177
179
  }
178
180
  packages['api'].update(packages['sql'])
179
181
  packages['api'].update(packages['formatting'])