meerschaum 3.0.0rc3__py3-none-any.whl → 3.0.0rc7__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 (126) hide show
  1. meerschaum/_internal/arguments/_parser.py +14 -2
  2. meerschaum/_internal/cli/__init__.py +6 -0
  3. meerschaum/_internal/cli/daemons.py +103 -0
  4. meerschaum/_internal/cli/entry.py +220 -0
  5. meerschaum/_internal/cli/workers.py +434 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +113 -19
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +3 -1
  11. meerschaum/_internal/term/TermPageHandler.py +1 -2
  12. meerschaum/_internal/term/__init__.py +40 -6
  13. meerschaum/_internal/term/tools.py +33 -8
  14. meerschaum/actions/__init__.py +6 -4
  15. meerschaum/actions/api.py +39 -11
  16. meerschaum/actions/attach.py +1 -0
  17. meerschaum/actions/delete.py +4 -2
  18. meerschaum/actions/edit.py +27 -8
  19. meerschaum/actions/login.py +8 -8
  20. meerschaum/actions/register.py +13 -7
  21. meerschaum/actions/reload.py +22 -5
  22. meerschaum/actions/restart.py +14 -0
  23. meerschaum/actions/show.py +69 -4
  24. meerschaum/actions/start.py +135 -14
  25. meerschaum/actions/stop.py +36 -3
  26. meerschaum/actions/sync.py +6 -1
  27. meerschaum/api/__init__.py +35 -13
  28. meerschaum/api/_events.py +7 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +103 -97
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/pipes.py +136 -57
  34. meerschaum/api/dash/callbacks/register.py +9 -2
  35. meerschaum/api/dash/callbacks/tokens.py +2 -1
  36. meerschaum/api/dash/components.py +6 -7
  37. meerschaum/api/dash/keys.py +17 -1
  38. meerschaum/api/dash/pages/login.py +2 -2
  39. meerschaum/api/dash/pages/pipes.py +14 -4
  40. meerschaum/api/dash/pipes.py +186 -65
  41. meerschaum/api/dash/tokens.py +1 -1
  42. meerschaum/api/dash/webterm.py +14 -6
  43. meerschaum/api/models/_pipes.py +7 -1
  44. meerschaum/api/resources/static/js/terminado.js +3 -0
  45. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  46. meerschaum/api/resources/templates/termpage.html +1 -0
  47. meerschaum/api/routes/_jobs.py +23 -11
  48. meerschaum/api/routes/_login.py +73 -5
  49. meerschaum/api/routes/_pipes.py +6 -4
  50. meerschaum/api/routes/_webterm.py +3 -3
  51. meerschaum/config/__init__.py +60 -13
  52. meerschaum/config/_default.py +89 -61
  53. meerschaum/config/_edit.py +10 -8
  54. meerschaum/config/_formatting.py +2 -0
  55. meerschaum/config/_patch.py +4 -2
  56. meerschaum/config/_paths.py +127 -12
  57. meerschaum/config/_read_config.py +20 -10
  58. meerschaum/config/_version.py +1 -1
  59. meerschaum/config/environment.py +262 -0
  60. meerschaum/config/stack/__init__.py +7 -5
  61. meerschaum/connectors/_Connector.py +1 -2
  62. meerschaum/connectors/__init__.py +37 -2
  63. meerschaum/connectors/api/_APIConnector.py +1 -1
  64. meerschaum/connectors/api/_jobs.py +11 -0
  65. meerschaum/connectors/api/_pipes.py +7 -1
  66. meerschaum/connectors/instance/_plugins.py +9 -1
  67. meerschaum/connectors/instance/_tokens.py +20 -3
  68. meerschaum/connectors/instance/_users.py +8 -1
  69. meerschaum/connectors/parse.py +1 -1
  70. meerschaum/connectors/sql/_create_engine.py +3 -0
  71. meerschaum/connectors/sql/_pipes.py +98 -79
  72. meerschaum/connectors/sql/_users.py +8 -1
  73. meerschaum/connectors/sql/tables/__init__.py +20 -3
  74. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  75. meerschaum/connectors/valkey/_pipes.py +7 -5
  76. meerschaum/core/Pipe/__init__.py +62 -72
  77. meerschaum/core/Pipe/_attributes.py +66 -90
  78. meerschaum/core/Pipe/_cache.py +555 -0
  79. meerschaum/core/Pipe/_clear.py +0 -11
  80. meerschaum/core/Pipe/_data.py +0 -50
  81. meerschaum/core/Pipe/_deduplicate.py +0 -13
  82. meerschaum/core/Pipe/_delete.py +12 -21
  83. meerschaum/core/Pipe/_drop.py +11 -23
  84. meerschaum/core/Pipe/_dtypes.py +1 -1
  85. meerschaum/core/Pipe/_index.py +8 -14
  86. meerschaum/core/Pipe/_sync.py +12 -18
  87. meerschaum/core/Plugin/_Plugin.py +7 -1
  88. meerschaum/core/Token/_Token.py +1 -1
  89. meerschaum/core/User/_User.py +1 -2
  90. meerschaum/jobs/_Executor.py +88 -4
  91. meerschaum/jobs/_Job.py +135 -35
  92. meerschaum/jobs/systemd.py +7 -2
  93. meerschaum/plugins/__init__.py +277 -81
  94. meerschaum/utils/_get_pipes.py +30 -4
  95. meerschaum/utils/daemon/Daemon.py +195 -41
  96. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  97. meerschaum/utils/daemon/RotatingFile.py +63 -36
  98. meerschaum/utils/daemon/StdinFile.py +53 -13
  99. meerschaum/utils/daemon/__init__.py +18 -5
  100. meerschaum/utils/daemon/_names.py +6 -3
  101. meerschaum/utils/debug.py +34 -4
  102. meerschaum/utils/dtypes/__init__.py +5 -1
  103. meerschaum/utils/formatting/__init__.py +4 -1
  104. meerschaum/utils/formatting/_jobs.py +1 -1
  105. meerschaum/utils/formatting/_pipes.py +47 -46
  106. meerschaum/utils/formatting/_pprint.py +1 -0
  107. meerschaum/utils/formatting/_shell.py +16 -6
  108. meerschaum/utils/misc.py +18 -38
  109. meerschaum/utils/packages/__init__.py +15 -13
  110. meerschaum/utils/packages/_packages.py +1 -0
  111. meerschaum/utils/pipes.py +39 -7
  112. meerschaum/utils/process.py +1 -1
  113. meerschaum/utils/prompt.py +171 -144
  114. meerschaum/utils/sql.py +12 -2
  115. meerschaum/utils/threading.py +42 -0
  116. meerschaum/utils/venv/__init__.py +2 -0
  117. meerschaum/utils/warnings.py +19 -13
  118. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/METADATA +3 -1
  119. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/RECORD +125 -119
  120. meerschaum/config/_environment.py +0 -145
  121. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/WHEEL +0 -0
  122. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/entry_points.txt +0 -0
  123. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  124. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/NOTICE +0 -0
  125. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/top_level.txt +0 -0
  126. {meerschaum-3.0.0rc3.dist-info → meerschaum-3.0.0rc7.dist-info}/zip-safe +0 -0
@@ -146,8 +146,10 @@ def fetch_pipes_keys(
146
146
  location_keys: Optional[List[str]] = None,
147
147
  tags: Optional[List[str]] = None,
148
148
  params: Optional[Dict[str, Any]] = None,
149
- debug: bool = False
150
- ) -> List[Tuple[str, str, Optional[str]]]:
149
+ debug: bool = False,
150
+ ) -> List[
151
+ Tuple[str, str, Union[str, None], Dict[str, Any]]
152
+ ]:
151
153
  """
152
154
  Return a list of tuples corresponding to the parameters provided.
153
155
 
@@ -174,7 +176,7 @@ def fetch_pipes_keys(
174
176
 
175
177
  Returns
176
178
  -------
177
- A list of tuples of pipes' keys (connector_keys, metric_key, location_key).
179
+ A list of tuples of pipes' keys and parameters (connector_keys, metric_key, location_key, parameters).
178
180
  """
179
181
  from meerschaum.utils.packages import attempt_import
180
182
  from meerschaum.utils.misc import separate_negation_values
@@ -249,11 +251,18 @@ def fetch_pipes_keys(
249
251
  ) for key, val in _params.items()
250
252
  if not isinstance(val, (list, tuple)) and key in pipes_tbl.c
251
253
  ]
254
+ if self.flavor in json_flavors:
255
+ sqlalchemy_dialects = mrsm.attempt_import('sqlalchemy.dialects', lazy=False)
256
+ JSONB = sqlalchemy_dialects.postgresql.JSONB
257
+ else:
258
+ JSONB = sqlalchemy.String
259
+
252
260
  select_cols = (
253
261
  [
254
262
  pipes_tbl.c.connector_keys,
255
263
  pipes_tbl.c.metric_key,
256
264
  pipes_tbl.c.location_key,
265
+ pipes_tbl.c.parameters,
257
266
  ]
258
267
  )
259
268
 
@@ -271,14 +280,12 @@ def fetch_pipes_keys(
271
280
 
272
281
  ors, nands = [], []
273
282
  if self.flavor in json_flavors:
274
- from sqlalchemy.dialects import postgresql
275
283
  for _in_tags, _ex_tags in in_ex_tag_groups:
276
284
  if _in_tags:
277
285
  ors.append(
278
286
  sqlalchemy.and_(
279
- pipes_tbl.c['parameters'].cast(postgresql.JSONB).has_key('tags'),
280
287
  pipes_tbl.c['parameters']['tags'].cast(
281
- postgresql.JSONB
288
+ JSONB
282
289
  ).contains(_in_tags)
283
290
  )
284
291
  )
@@ -286,9 +293,8 @@ def fetch_pipes_keys(
286
293
  nands.append(
287
294
  sqlalchemy.not_(
288
295
  sqlalchemy.and_(
289
- pipes_tbl.c['parameters'].cast(postgresql.JSONB).has_key('tags'),
290
296
  pipes_tbl.c['parameters']['tags'].cast(
291
- postgresql.JSONB
297
+ JSONB
292
298
  ).contains([xt])
293
299
  )
294
300
  )
@@ -340,7 +346,7 @@ def fetch_pipes_keys(
340
346
  except Exception as e:
341
347
  error(str(e))
342
348
 
343
- return [(row[0], row[1], row[2]) for row in rows]
349
+ return rows
344
350
 
345
351
 
346
352
  def create_pipe_indices(
@@ -371,6 +377,9 @@ def create_indices(
371
377
  """
372
378
  Create a pipe's indices.
373
379
  """
380
+ if pipe.__dict__.get('_skip_check_indices', False):
381
+ return True
382
+
374
383
  if debug:
375
384
  dprint(f"Creating indices for {pipe}...")
376
385
 
@@ -380,7 +389,7 @@ def create_indices(
380
389
 
381
390
  cols_to_include = set((columns or []) + (indices or [])) or None
382
391
 
383
- _ = pipe.__dict__.pop('_columns_indices', None)
392
+ pipe._clear_cache_key('_columns_indices', debug=debug)
384
393
  ix_queries = {
385
394
  col: queries
386
395
  for col, queries in self.get_create_index_queries(pipe, debug=debug).items()
@@ -456,7 +465,7 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe) -> Dict[str, str]:
456
465
  -------
457
466
  A dictionary of index keys to column names.
458
467
  """
459
- from meerschaum.utils.sql import DEFAULT_SCHEMA_FLAVORS
468
+ from meerschaum.utils.sql import DEFAULT_SCHEMA_FLAVORS, truncate_item_name
460
469
  _parameters = pipe.parameters
461
470
  _index_template = _parameters.get('index_template', "IX_{schema_str}{target}_{column_names}")
462
471
  _schema = self.get_pipe_schema(pipe)
@@ -497,7 +506,7 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe) -> Dict[str, str]:
497
506
  continue
498
507
  seen_index_names[index_name] = ix
499
508
  return {
500
- ix: index_name
509
+ ix: truncate_item_name(index_name, flavor=self.flavor)
501
510
  for index_name, ix in seen_index_names.items()
502
511
  }
503
512
 
@@ -1498,7 +1507,7 @@ def get_pipe_attributes(
1498
1507
  """
1499
1508
  from meerschaum.connectors.sql.tables import get_tables
1500
1509
  from meerschaum.utils.packages import attempt_import
1501
- sqlalchemy = attempt_import('sqlalchemy')
1510
+ sqlalchemy = attempt_import('sqlalchemy', lazy=False)
1502
1511
 
1503
1512
  if pipe.get_id(debug=debug) is None:
1504
1513
  return {}
@@ -1509,16 +1518,16 @@ def get_pipe_attributes(
1509
1518
  q = sqlalchemy.select(pipes_tbl).where(pipes_tbl.c.pipe_id == pipe.id)
1510
1519
  if debug:
1511
1520
  dprint(q)
1512
- attributes = (
1513
- dict(self.exec(q, silent=True, debug=debug).first()._mapping)
1521
+ rows = (
1522
+ self.exec(q, silent=True, debug=debug).mappings().all()
1514
1523
  if self.flavor != 'duckdb'
1515
- else self.read(q, debug=debug).to_dict(orient='records')[0]
1524
+ else self.read(q, debug=debug).to_dict(orient='records')
1516
1525
  )
1517
- except Exception as e:
1518
- import traceback
1519
- traceback.print_exc()
1520
- warn(e)
1521
- print(pipe)
1526
+ if not rows:
1527
+ return {}
1528
+ attributes = dict(rows[0])
1529
+ except Exception:
1530
+ warn(traceback.format_exc())
1522
1531
  return {}
1523
1532
 
1524
1533
  ### handle non-PostgreSQL databases (text vs JSON)
@@ -1673,8 +1682,9 @@ def sync_pipe(
1673
1682
  UPDATE_QUERIES,
1674
1683
  get_reset_autoincrement_queries,
1675
1684
  )
1676
- from meerschaum.utils.dtypes import are_dtypes_equal
1685
+ from meerschaum.utils.dtypes import get_current_timestamp
1677
1686
  from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
1687
+ from meerschaum.utils.dataframe import get_special_cols
1678
1688
  from meerschaum import Pipe
1679
1689
  import time
1680
1690
  import copy
@@ -1686,6 +1696,7 @@ def sync_pipe(
1686
1696
 
1687
1697
  start = time.perf_counter()
1688
1698
  pipe_name = sql_item_name(pipe.target, self.flavor, schema=self.get_pipe_schema(pipe))
1699
+ dtypes = pipe.get_dtypes(debug=debug)
1689
1700
 
1690
1701
  if not pipe.temporary and not pipe.get_id(debug=debug):
1691
1702
  register_tuple = pipe.register(debug=debug)
@@ -1702,6 +1713,7 @@ def sync_pipe(
1702
1713
  df,
1703
1714
  chunksize=chunksize,
1704
1715
  safe_copy=kw.get('safe_copy', False),
1716
+ dtypes=dtypes,
1705
1717
  debug=debug,
1706
1718
  )
1707
1719
 
@@ -1714,35 +1726,18 @@ def sync_pipe(
1714
1726
  ### Check for new columns.
1715
1727
  add_cols_queries = self.get_add_columns_queries(pipe, df, debug=debug)
1716
1728
  if add_cols_queries:
1717
- _ = pipe.__dict__.pop('_columns_indices', None)
1718
- _ = pipe.__dict__.pop('_columns_types', None)
1729
+ pipe._clear_cache_key('_columns_types', debug=debug)
1730
+ pipe._clear_cache_key('_columns_indices', debug=debug)
1719
1731
  if not self.exec_queries(add_cols_queries, debug=debug):
1720
1732
  warn(f"Failed to add new columns to {pipe}.")
1721
1733
 
1722
1734
  alter_cols_queries = self.get_alter_columns_queries(pipe, df, debug=debug)
1723
1735
  if alter_cols_queries:
1724
- _ = pipe.__dict__.pop('_columns_indices', None)
1725
- _ = pipe.__dict__.pop('_columns_types', None)
1736
+ pipe._clear_cache_key('_columns_types', debug=debug)
1737
+ pipe._clear_cache_key('_columns_types', debug=debug)
1726
1738
  if not self.exec_queries(alter_cols_queries, debug=debug):
1727
1739
  warn(f"Failed to alter columns for {pipe}.")
1728
1740
 
1729
- ### NOTE: Oracle SQL < 23c (2023) and SQLite does not support booleans,
1730
- ### so infer bools and persist them to `dtypes`.
1731
- if self.flavor in ('oracle', 'sqlite', 'mysql', 'mariadb'):
1732
- pipe_dtypes = pipe.get_dtypes(infer=False, debug=debug)
1733
- new_bool_cols = {
1734
- col: 'bool[pyarrow]'
1735
- for col, typ in df.dtypes.items()
1736
- if col not in pipe_dtypes
1737
- and are_dtypes_equal(str(typ), 'bool')
1738
- }
1739
- pipe_dtypes.update(new_bool_cols)
1740
- pipe.dtypes = pipe_dtypes
1741
- if new_bool_cols and not pipe.temporary:
1742
- infer_bool_success, infer_bool_msg = pipe.edit(debug=debug)
1743
- if not infer_bool_success:
1744
- return infer_bool_success, infer_bool_msg
1745
-
1746
1741
  upsert = pipe.parameters.get('upsert', False) and (self.flavor + '-upsert') in UPDATE_QUERIES
1747
1742
  if upsert:
1748
1743
  check_existing = False
@@ -1771,7 +1766,7 @@ def sync_pipe(
1771
1766
  if 'name' in kw:
1772
1767
  kw.pop('name')
1773
1768
 
1774
- ### Insert new data into Pipe's table.
1769
+ ### Insert new data into the target table.
1775
1770
  unseen_kw = copy.deepcopy(kw)
1776
1771
  unseen_kw.update({
1777
1772
  'name': pipe.target,
@@ -1792,7 +1787,7 @@ def sync_pipe(
1792
1787
  is_new
1793
1788
  and primary_key
1794
1789
  and primary_key
1795
- not in pipe.dtypes
1790
+ not in dtypes
1796
1791
  and primary_key not in unseen_df.columns
1797
1792
  )
1798
1793
  )
@@ -1892,6 +1887,14 @@ def sync_pipe(
1892
1887
  label=('update' if not upsert else 'upsert'),
1893
1888
  )
1894
1889
  self._log_temporary_tables_creation(temp_target, create=(not pipe.temporary), debug=debug)
1890
+ update_dtypes = {
1891
+ **{
1892
+ col: str(typ)
1893
+ for col, typ in update_df.dtypes.items()
1894
+ },
1895
+ **get_special_cols(update_df)
1896
+ }
1897
+
1895
1898
  temp_pipe = Pipe(
1896
1899
  pipe.connector_keys.replace(':', '_') + '_', pipe.metric_key, pipe.location_key,
1897
1900
  instance=pipe.instance_keys,
@@ -1900,34 +1903,30 @@ def sync_pipe(
1900
1903
  for ix_key, ix in pipe.columns.items()
1901
1904
  if ix and ix in update_df.columns
1902
1905
  },
1903
- dtypes={
1904
- col: typ
1905
- for col, typ in pipe.dtypes.items()
1906
- if col in update_df.columns
1907
- },
1906
+ dtypes=update_dtypes,
1908
1907
  target=temp_target,
1909
1908
  temporary=True,
1910
1909
  enforce=False,
1911
1910
  static=True,
1912
1911
  autoincrement=False,
1912
+ cache=False,
1913
1913
  parameters={
1914
1914
  'schema': self.internal_schema,
1915
1915
  'hypertable': False,
1916
1916
  },
1917
1917
  )
1918
- temp_pipe.__dict__['_columns_types'] = {
1919
- col: get_db_type_from_pd_type(
1920
- pipe.dtypes.get(col, str(typ)),
1921
- self.flavor,
1922
- )
1923
- for col, typ in update_df.dtypes.items()
1918
+ _temp_columns_types = {
1919
+ col: get_db_type_from_pd_type(typ, self.flavor)
1920
+ for col, typ in update_dtypes.items()
1924
1921
  }
1925
- now_ts = time.perf_counter()
1926
- temp_pipe.__dict__['_columns_types_timestamp'] = now_ts
1927
- temp_pipe.__dict__['_skip_check_indices'] = True
1922
+ temp_pipe._cache_value('_columns_types', _temp_columns_types, memory_only=True, debug=debug)
1923
+ temp_pipe._cache_value('_skip_check_indices', True, memory_only=True, debug=debug)
1924
+ now_ts = get_current_timestamp('ms', as_int=True) / 1000
1925
+ temp_pipe._cache_value('_columns_types_timestamp', now_ts, memory_only=True, debug=debug)
1928
1926
  temp_success, temp_msg = temp_pipe.sync(update_df, check_existing=False, debug=debug)
1929
1927
  if not temp_success:
1930
1928
  return temp_success, temp_msg
1929
+
1931
1930
  existing_cols = pipe.get_columns_types(debug=debug)
1932
1931
  join_cols = [
1933
1932
  col
@@ -1950,6 +1949,8 @@ def sync_pipe(
1950
1949
  upsert=upsert,
1951
1950
  schema=self.get_pipe_schema(pipe),
1952
1951
  patch_schema=self.internal_schema,
1952
+ target_cols_types=pipe.get_columns_types(debug=debug),
1953
+ patch_cols_types=_temp_columns_types,
1953
1954
  datetime_col=(dt_col if dt_col in update_df.columns else None),
1954
1955
  identity_insert=(autoincrement and primary_key in update_df.columns),
1955
1956
  null_indices=pipe.null_indices,
@@ -2237,13 +2238,13 @@ def sync_pipe_inplace(
2237
2238
 
2238
2239
  add_cols_queries = self.get_add_columns_queries(pipe, new_cols, debug=debug)
2239
2240
  if add_cols_queries:
2240
- _ = pipe.__dict__.pop('_columns_types', None)
2241
- _ = pipe.__dict__.pop('_columns_indices', None)
2241
+ pipe._clear_cache_key('_columns_types', debug=debug)
2242
+ pipe._clear_cache_key('_columns_indices', debug=debug)
2242
2243
  self.exec_queries(add_cols_queries, debug=debug)
2243
2244
 
2244
2245
  alter_cols_queries = self.get_alter_columns_queries(pipe, new_cols, debug=debug)
2245
2246
  if alter_cols_queries:
2246
- _ = pipe.__dict__.pop('_columns_types', None)
2247
+ pipe._clear_cache_key('_columns_types', debug=debug)
2247
2248
  self.exec_queries(alter_cols_queries, debug=debug)
2248
2249
 
2249
2250
  insert_queries = [
@@ -2546,6 +2547,8 @@ def sync_pipe_inplace(
2546
2547
  upsert=upsert,
2547
2548
  schema=self.get_pipe_schema(pipe),
2548
2549
  patch_schema=internal_schema,
2550
+ target_cols_types=pipe.get_columns_types(debug=debug),
2551
+ patch_cols_types=delta_cols_types,
2549
2552
  datetime_col=pipe.columns.get('datetime', None),
2550
2553
  flavor=self.flavor,
2551
2554
  null_indices=pipe.null_indices,
@@ -3036,6 +3039,7 @@ def get_pipe_table(
3036
3039
  from meerschaum.utils.sql import get_sqlalchemy_table
3037
3040
  if not pipe.exists(debug=debug):
3038
3041
  return None
3042
+
3039
3043
  return get_sqlalchemy_table(
3040
3044
  pipe.target,
3041
3045
  connector=self,
@@ -3093,9 +3097,11 @@ def get_pipe_columns_types(
3093
3097
  pipe_table = self.get_pipe_table(pipe, debug=debug)
3094
3098
  if pipe_table is None:
3095
3099
  return {}
3100
+
3096
3101
  if debug:
3097
- dprint(f"Found columns:")
3102
+ dprint("Found columns:")
3098
3103
  mrsm.pprint(dict(pipe_table.columns))
3104
+
3099
3105
  for col in pipe_table.columns:
3100
3106
  table_columns[str(col.name)] = str(col.type)
3101
3107
  except Exception as e:
@@ -3127,6 +3133,7 @@ def get_pipe_columns_indices(
3127
3133
  """
3128
3134
  if pipe.__dict__.get('_skip_check_indices', False):
3129
3135
  return {}
3136
+
3130
3137
  from meerschaum.utils.sql import get_table_cols_indices
3131
3138
  return get_table_cols_indices(
3132
3139
  pipe.target,
@@ -3181,7 +3188,6 @@ def get_add_columns_queries(
3181
3188
  get_db_type_from_pd_type,
3182
3189
  )
3183
3190
  from meerschaum.utils.misc import flatten_list
3184
- table_obj = self.get_pipe_table(pipe, debug=debug)
3185
3191
  is_dask = 'dask' in df.__module__ if not isinstance(df, dict) else False
3186
3192
  if is_dask:
3187
3193
  df = df.partitions[0].compute()
@@ -3205,9 +3211,6 @@ def get_add_columns_queries(
3205
3211
  elif isinstance(val, str):
3206
3212
  df_cols_types[col] = 'str'
3207
3213
  db_cols_types = {
3208
- col: get_pd_type_from_db_type(str(typ.type))
3209
- for col, typ in table_obj.columns.items()
3210
- } if table_obj is not None else {
3211
3214
  col: get_pd_type_from_db_type(typ)
3212
3215
  for col, typ in get_table_cols_types(
3213
3216
  pipe.target,
@@ -3303,7 +3306,6 @@ def get_alter_columns_queries(
3303
3306
  get_db_type_from_pd_type,
3304
3307
  )
3305
3308
  from meerschaum.utils.misc import flatten_list, generate_password, items_str
3306
- table_obj = self.get_pipe_table(pipe, debug=debug)
3307
3309
  target = pipe.target
3308
3310
  session_id = generate_password(3)
3309
3311
  numeric_cols = (
@@ -3324,9 +3326,6 @@ def get_alter_columns_queries(
3324
3326
  else df
3325
3327
  )
3326
3328
  db_cols_types = {
3327
- col: get_pd_type_from_db_type(str(typ.type))
3328
- for col, typ in table_obj.columns.items()
3329
- } if table_obj is not None else {
3330
3329
  col: get_pd_type_from_db_type(typ)
3331
3330
  for col, typ in get_table_cols_types(
3332
3331
  pipe.target,
@@ -3335,7 +3334,7 @@ def get_alter_columns_queries(
3335
3334
  debug=debug,
3336
3335
  ).items()
3337
3336
  }
3338
- pipe_dtypes = pipe.dtypes
3337
+ pipe_dtypes = pipe.get_dtypes(debug=debug)
3339
3338
  pipe_bool_cols = [col for col, typ in pipe_dtypes.items() if are_dtypes_equal(str(typ), 'bool')]
3340
3339
  pd_db_df_aliases = {
3341
3340
  'int': 'bool',
@@ -3347,6 +3346,7 @@ def get_alter_columns_queries(
3347
3346
  pd_db_df_aliases.update({
3348
3347
  'int': 'numeric',
3349
3348
  'date': 'datetime',
3349
+ 'numeric': 'int',
3350
3350
  })
3351
3351
 
3352
3352
  altered_cols = {
@@ -3357,14 +3357,32 @@ def get_alter_columns_queries(
3357
3357
  }
3358
3358
 
3359
3359
  if debug and altered_cols:
3360
- dprint(f"Columns to be altered:")
3360
+ dprint("Columns to be altered:")
3361
3361
  mrsm.pprint(altered_cols)
3362
3362
 
3363
+ ### NOTE: Special columns (numerics, bools, etc.) are captured and cached upon detection.
3364
+ new_special_cols = pipe._get_cached_value('new_special_cols', debug=debug) or {}
3365
+ new_special_db_cols_types = {
3366
+ col: (db_cols_types.get(col, 'object'), typ)
3367
+ for col, typ in new_special_cols.items()
3368
+ }
3369
+ if debug:
3370
+ dprint("Cached new special columns:")
3371
+ mrsm.pprint(new_special_cols)
3372
+ dprint("New special columns db types:")
3373
+ mrsm.pprint(new_special_db_cols_types)
3374
+
3375
+ altered_cols.update(new_special_db_cols_types)
3376
+
3363
3377
  ### NOTE: Sometimes bools are coerced into ints or floats.
3364
3378
  altered_cols_to_ignore = set()
3365
3379
  for col, (db_typ, df_typ) in altered_cols.items():
3366
3380
  for db_alias, df_alias in pd_db_df_aliases.items():
3367
- if db_alias in db_typ.lower() and df_alias in df_typ.lower():
3381
+ if (
3382
+ db_alias in db_typ.lower()
3383
+ and df_alias in df_typ.lower()
3384
+ and col not in new_special_cols
3385
+ ):
3368
3386
  altered_cols_to_ignore.add(col)
3369
3387
 
3370
3388
  ### Oracle's bool handling sometimes mixes NUMBER and INT.
@@ -3387,7 +3405,7 @@ def get_alter_columns_queries(
3387
3405
  altered_cols_to_ignore.add(bool_col)
3388
3406
 
3389
3407
  if debug and altered_cols_to_ignore:
3390
- dprint(f"Ignoring the following altered columns (false positives).")
3408
+ dprint("Ignoring the following altered columns (false positives).")
3391
3409
  mrsm.pprint(altered_cols_to_ignore)
3392
3410
 
3393
3411
  for col in altered_cols_to_ignore:
@@ -3434,12 +3452,12 @@ def get_alter_columns_queries(
3434
3452
  + sql_item_name(target, self.flavor, self.get_pipe_schema(pipe))
3435
3453
  + " (\n"
3436
3454
  )
3437
- for col_name, col_obj in table_obj.columns.items():
3455
+ for col_name, col_typ in db_cols_types.items():
3438
3456
  create_query += (
3439
3457
  sql_item_name(col_name, self.flavor, None)
3440
3458
  + " "
3441
3459
  + (
3442
- str(col_obj.type)
3460
+ col_typ
3443
3461
  if col_name not in altered_cols
3444
3462
  else altered_cols_types[col_name]
3445
3463
  )
@@ -3453,12 +3471,12 @@ def get_alter_columns_queries(
3453
3471
  + ' ('
3454
3472
  + ', '.join([
3455
3473
  sql_item_name(col_name, self.flavor, None)
3456
- for col_name, _ in table_obj.columns.items()
3474
+ for col_name in db_cols_types
3457
3475
  ])
3458
3476
  + ')'
3459
3477
  + "\nSELECT\n"
3460
3478
  )
3461
- for col_name, col_obj in table_obj.columns.items():
3479
+ for col_name in db_cols_types:
3462
3480
  new_col_str = (
3463
3481
  sql_item_name(col_name, self.flavor, None)
3464
3482
  if col_name not in altered_cols
@@ -3471,6 +3489,7 @@ def get_alter_columns_queries(
3471
3489
  )
3472
3490
  )
3473
3491
  insert_query += new_col_str + ",\n"
3492
+
3474
3493
  insert_query = insert_query[:-2] + (
3475
3494
  f"\nFROM {sql_item_name(temp_table_name, self.flavor, self.get_pipe_schema(pipe))}"
3476
3495
  )
@@ -16,9 +16,15 @@ def get_users_pipe(self) -> mrsm.Pipe:
16
16
  """
17
17
  Return the internal metadata pipe for users management.
18
18
  """
19
- return mrsm.Pipe(
19
+ if '_users_pipe' in self.__dict__:
20
+ return self._users_pipe
21
+
22
+ cache_connector = self.__dict__.get('_cache_connector', None)
23
+ self._users_pipe = mrsm.Pipe(
20
24
  'mrsm', 'users',
21
25
  temporary=True,
26
+ cache=True,
27
+ cache_connector_keys=cache_connector,
22
28
  static=True,
23
29
  null_indices=False,
24
30
  enforce=False,
@@ -36,6 +42,7 @@ def get_users_pipe(self) -> mrsm.Pipe:
36
42
  'unique': 'username',
37
43
  },
38
44
  )
45
+ return self._users_pipe
39
46
 
40
47
 
41
48
  def register_user(
@@ -24,6 +24,7 @@ _skip_index_names_flavors = {'mssql',}
24
24
  def get_tables(
25
25
  mrsm_instance: Optional[Union[str, InstanceConnector]] = None,
26
26
  create: Optional[bool] = None,
27
+ refresh: bool = False,
27
28
  debug: bool = False,
28
29
  ) -> Union[Dict[str, 'sqlalchemy.Table'], bool]:
29
30
  """
@@ -37,6 +38,9 @@ def get_tables(
37
38
  create: Optional[bool], default None
38
39
  If `True`, create the tables if they don't exist.
39
40
 
41
+ refresh: bool, default False
42
+ If `True`, invalidate and rebuild any cache.
43
+
40
44
  debug: bool, default False
41
45
  Verbosity Toggle.
42
46
 
@@ -67,7 +71,15 @@ def get_tables(
67
71
  else: ### NOTE: mrsm_instance MUST BE a SQL Connector for this to work!
68
72
  conn = mrsm_instance
69
73
 
70
- cache_expired = _check_create_cache(conn, debug=debug) if conn.type == 'sql' else False
74
+ cache_expired = refresh or (
75
+ (
76
+ _check_create_cache(conn, debug=debug)
77
+ if conn.flavor != 'sqlite'
78
+ else True
79
+ )
80
+ if conn.type == 'sql'
81
+ else False
82
+ )
71
83
  create = create or cache_expired
72
84
 
73
85
  ### Skip if the connector is not a SQL connector.
@@ -75,6 +87,10 @@ def get_tables(
75
87
  return {}
76
88
 
77
89
  conn_key = str(conn)
90
+
91
+ if refresh:
92
+ _ = connector_tables.pop(conn_key, None)
93
+
78
94
  if conn_key in connector_tables:
79
95
  return connector_tables[conn_key]
80
96
 
@@ -248,8 +264,9 @@ def get_tables(
248
264
 
249
265
  _write_create_cache(mrsm.get_connector(str(mrsm_instance)), debug=debug)
250
266
 
251
- with open(pickle_path, 'wb') as f:
252
- pickle.dump(conn.metadata, f)
267
+ if conn.flavor != 'sqlite':
268
+ with open(pickle_path, 'wb') as f:
269
+ pickle.dump(conn.metadata, f)
253
270
 
254
271
  connector_tables[conn_key] = _tables
255
272
  return connector_tables[conn_key]
@@ -140,13 +140,13 @@ class ValkeyConnector(InstanceConnector):
140
140
 
141
141
  return uri
142
142
 
143
- def set(self, key: str, value: Any, **kwargs: Any) -> None:
143
+ def set(self, key: str, value: Any, **kwargs: Any) -> bool:
144
144
  """
145
145
  Set the `key` to `value`.
146
146
  """
147
147
  return self.client.set(key, value, **kwargs)
148
148
 
149
- def get(self, key: str) -> Union[str, None]:
149
+ def get(self, key: str, decode: bool = True) -> Union[str, None]:
150
150
  """
151
151
  Get the value for `key`.
152
152
  """
@@ -154,7 +154,7 @@ class ValkeyConnector(InstanceConnector):
154
154
  if val is None:
155
155
  return None
156
156
 
157
- return val.decode('utf-8')
157
+ return val.decode('utf-8') if decode else val
158
158
 
159
159
  def test_connection(self) -> bool:
160
160
  """
@@ -888,7 +888,9 @@ def fetch_pipes_keys(
888
888
  tags: Optional[List[str]] = None,
889
889
  params: Optional[Dict[str, Any]] = None,
890
890
  debug: bool = False
891
- ) -> Optional[List[Tuple[str, str, Optional[str]]]]:
891
+ ) -> List[
892
+ Tuple[str, str, Union[str, None], Dict[str, Any]]
893
+ ]:
892
894
  """
893
895
  Return the keys for the registered pipes.
894
896
  """
@@ -919,6 +921,7 @@ def fetch_pipes_keys(
919
921
  doc['connector_keys'],
920
922
  doc['metric_key'],
921
923
  doc['location_key'],
924
+ doc.get('parameters', {})
922
925
  )
923
926
  for doc in df.to_dict(orient='records')
924
927
  ]
@@ -929,9 +932,8 @@ def fetch_pipes_keys(
929
932
  in_ex_tag_groups = [separate_negation_values(tag_group) for tag_group in tag_groups]
930
933
 
931
934
  filtered_keys = []
932
- for ck, mk, lk in keys:
933
- pipe = mrsm.Pipe(ck, mk, lk, instance=self)
934
- pipe_tags = set(pipe.tags)
935
+ for ck, mk, lk, parameters in keys:
936
+ pipe_tags = set(parameters.get('tags', []))
935
937
 
936
938
  include_pipe = True
937
939
  for in_tags, ex_tags in in_ex_tag_groups:
@@ -943,6 +945,6 @@ def fetch_pipes_keys(
943
945
  continue
944
946
 
945
947
  if include_pipe:
946
- filtered_keys.append((ck, mk, lk))
948
+ filtered_keys.append((ck, mk, lk, parameters))
947
949
 
948
950
  return filtered_keys