meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc8__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 (117) 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 +435 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +115 -24
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +4 -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 +2 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +29 -0
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/register.py +9 -2
  34. meerschaum/api/dash/pages/login.py +2 -2
  35. meerschaum/api/dash/pipes.py +72 -36
  36. meerschaum/api/dash/webterm.py +14 -6
  37. meerschaum/api/models/_pipes.py +7 -1
  38. meerschaum/api/resources/static/js/terminado.js +3 -0
  39. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  40. meerschaum/api/resources/templates/termpage.html +1 -0
  41. meerschaum/api/routes/_jobs.py +23 -11
  42. meerschaum/api/routes/_login.py +73 -5
  43. meerschaum/api/routes/_pipes.py +6 -4
  44. meerschaum/api/routes/_webterm.py +3 -3
  45. meerschaum/config/__init__.py +60 -13
  46. meerschaum/config/_default.py +89 -61
  47. meerschaum/config/_edit.py +10 -8
  48. meerschaum/config/_formatting.py +2 -0
  49. meerschaum/config/_patch.py +4 -2
  50. meerschaum/config/_paths.py +127 -12
  51. meerschaum/config/_read_config.py +32 -12
  52. meerschaum/config/_version.py +1 -1
  53. meerschaum/config/environment.py +262 -0
  54. meerschaum/config/stack/__init__.py +7 -5
  55. meerschaum/connectors/_Connector.py +1 -2
  56. meerschaum/connectors/__init__.py +37 -2
  57. meerschaum/connectors/api/_APIConnector.py +1 -1
  58. meerschaum/connectors/api/_jobs.py +11 -0
  59. meerschaum/connectors/api/_pipes.py +7 -1
  60. meerschaum/connectors/instance/_plugins.py +9 -1
  61. meerschaum/connectors/instance/_tokens.py +20 -3
  62. meerschaum/connectors/instance/_users.py +8 -1
  63. meerschaum/connectors/parse.py +1 -1
  64. meerschaum/connectors/sql/_create_engine.py +3 -0
  65. meerschaum/connectors/sql/_pipes.py +93 -79
  66. meerschaum/connectors/sql/_users.py +8 -1
  67. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  68. meerschaum/connectors/valkey/_pipes.py +7 -5
  69. meerschaum/core/Pipe/__init__.py +45 -71
  70. meerschaum/core/Pipe/_attributes.py +66 -90
  71. meerschaum/core/Pipe/_cache.py +555 -0
  72. meerschaum/core/Pipe/_clear.py +0 -11
  73. meerschaum/core/Pipe/_data.py +0 -50
  74. meerschaum/core/Pipe/_deduplicate.py +0 -13
  75. meerschaum/core/Pipe/_delete.py +12 -21
  76. meerschaum/core/Pipe/_drop.py +11 -23
  77. meerschaum/core/Pipe/_dtypes.py +1 -1
  78. meerschaum/core/Pipe/_index.py +8 -14
  79. meerschaum/core/Pipe/_sync.py +12 -18
  80. meerschaum/core/Plugin/_Plugin.py +7 -1
  81. meerschaum/core/Token/_Token.py +1 -1
  82. meerschaum/core/User/_User.py +1 -2
  83. meerschaum/jobs/_Executor.py +88 -4
  84. meerschaum/jobs/_Job.py +146 -36
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +197 -42
  88. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  89. meerschaum/utils/daemon/RotatingFile.py +63 -36
  90. meerschaum/utils/daemon/StdinFile.py +53 -13
  91. meerschaum/utils/daemon/__init__.py +18 -5
  92. meerschaum/utils/daemon/_names.py +6 -3
  93. meerschaum/utils/debug.py +34 -4
  94. meerschaum/utils/dtypes/__init__.py +5 -1
  95. meerschaum/utils/formatting/__init__.py +4 -1
  96. meerschaum/utils/formatting/_jobs.py +1 -1
  97. meerschaum/utils/formatting/_pipes.py +47 -46
  98. meerschaum/utils/formatting/_shell.py +33 -9
  99. meerschaum/utils/misc.py +22 -38
  100. meerschaum/utils/packages/__init__.py +15 -13
  101. meerschaum/utils/packages/_packages.py +1 -0
  102. meerschaum/utils/pipes.py +33 -5
  103. meerschaum/utils/process.py +1 -1
  104. meerschaum/utils/prompt.py +172 -143
  105. meerschaum/utils/sql.py +12 -2
  106. meerschaum/utils/threading.py +42 -0
  107. meerschaum/utils/venv/__init__.py +2 -0
  108. meerschaum/utils/warnings.py +19 -13
  109. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/zip-safe +0 -0
@@ -146,7 +146,7 @@ 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
149
+ debug: bool = False,
150
150
  ) -> List[
151
151
  Tuple[str, str, Union[str, None], Dict[str, Any]]
152
152
  ]:
@@ -251,16 +251,18 @@ def fetch_pipes_keys(
251
251
  ) for key, val in _params.items()
252
252
  if not isinstance(val, (list, tuple)) and key in pipes_tbl.c
253
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
+
254
260
  select_cols = (
255
261
  [
256
262
  pipes_tbl.c.connector_keys,
257
263
  pipes_tbl.c.metric_key,
258
264
  pipes_tbl.c.location_key,
259
- (
260
- pipes_tbl.c.parameters['tags']
261
- if self.flavor in json_flavors
262
- else pipes_tbl.c.parameters
263
- ),
265
+ pipes_tbl.c.parameters,
264
266
  ]
265
267
  )
266
268
 
@@ -278,13 +280,12 @@ def fetch_pipes_keys(
278
280
 
279
281
  ors, nands = [], []
280
282
  if self.flavor in json_flavors:
281
- from sqlalchemy.dialects import postgresql
282
283
  for _in_tags, _ex_tags in in_ex_tag_groups:
283
284
  if _in_tags:
284
285
  ors.append(
285
286
  sqlalchemy.and_(
286
287
  pipes_tbl.c['parameters']['tags'].cast(
287
- postgresql.JSONB
288
+ JSONB
288
289
  ).contains(_in_tags)
289
290
  )
290
291
  )
@@ -293,7 +294,7 @@ def fetch_pipes_keys(
293
294
  sqlalchemy.not_(
294
295
  sqlalchemy.and_(
295
296
  pipes_tbl.c['parameters']['tags'].cast(
296
- postgresql.JSONB
297
+ JSONB
297
298
  ).contains([xt])
298
299
  )
299
300
  )
@@ -376,6 +377,9 @@ def create_indices(
376
377
  """
377
378
  Create a pipe's indices.
378
379
  """
380
+ if pipe.__dict__.get('_skip_check_indices', False):
381
+ return True
382
+
379
383
  if debug:
380
384
  dprint(f"Creating indices for {pipe}...")
381
385
 
@@ -385,7 +389,7 @@ def create_indices(
385
389
 
386
390
  cols_to_include = set((columns or []) + (indices or [])) or None
387
391
 
388
- _ = pipe.__dict__.pop('_columns_indices', None)
392
+ pipe._clear_cache_key('_columns_indices', debug=debug)
389
393
  ix_queries = {
390
394
  col: queries
391
395
  for col, queries in self.get_create_index_queries(pipe, debug=debug).items()
@@ -461,7 +465,7 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe) -> Dict[str, str]:
461
465
  -------
462
466
  A dictionary of index keys to column names.
463
467
  """
464
- from meerschaum.utils.sql import DEFAULT_SCHEMA_FLAVORS
468
+ from meerschaum.utils.sql import DEFAULT_SCHEMA_FLAVORS, truncate_item_name
465
469
  _parameters = pipe.parameters
466
470
  _index_template = _parameters.get('index_template', "IX_{schema_str}{target}_{column_names}")
467
471
  _schema = self.get_pipe_schema(pipe)
@@ -502,7 +506,7 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe) -> Dict[str, str]:
502
506
  continue
503
507
  seen_index_names[index_name] = ix
504
508
  return {
505
- ix: index_name
509
+ ix: truncate_item_name(index_name, flavor=self.flavor)
506
510
  for index_name, ix in seen_index_names.items()
507
511
  }
508
512
 
@@ -1503,7 +1507,7 @@ def get_pipe_attributes(
1503
1507
  """
1504
1508
  from meerschaum.connectors.sql.tables import get_tables
1505
1509
  from meerschaum.utils.packages import attempt_import
1506
- sqlalchemy = attempt_import('sqlalchemy')
1510
+ sqlalchemy = attempt_import('sqlalchemy', lazy=False)
1507
1511
 
1508
1512
  if pipe.get_id(debug=debug) is None:
1509
1513
  return {}
@@ -1514,16 +1518,16 @@ def get_pipe_attributes(
1514
1518
  q = sqlalchemy.select(pipes_tbl).where(pipes_tbl.c.pipe_id == pipe.id)
1515
1519
  if debug:
1516
1520
  dprint(q)
1517
- attributes = (
1518
- dict(self.exec(q, silent=True, debug=debug).first()._mapping)
1521
+ rows = (
1522
+ self.exec(q, silent=True, debug=debug).mappings().all()
1519
1523
  if self.flavor != 'duckdb'
1520
- else self.read(q, debug=debug).to_dict(orient='records')[0]
1524
+ else self.read(q, debug=debug).to_dict(orient='records')
1521
1525
  )
1522
- except Exception as e:
1523
- import traceback
1524
- traceback.print_exc()
1525
- warn(e)
1526
- print(pipe)
1526
+ if not rows:
1527
+ return {}
1528
+ attributes = dict(rows[0])
1529
+ except Exception:
1530
+ warn(traceback.format_exc())
1527
1531
  return {}
1528
1532
 
1529
1533
  ### handle non-PostgreSQL databases (text vs JSON)
@@ -1678,8 +1682,9 @@ def sync_pipe(
1678
1682
  UPDATE_QUERIES,
1679
1683
  get_reset_autoincrement_queries,
1680
1684
  )
1681
- from meerschaum.utils.dtypes import are_dtypes_equal
1685
+ from meerschaum.utils.dtypes import get_current_timestamp
1682
1686
  from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
1687
+ from meerschaum.utils.dataframe import get_special_cols
1683
1688
  from meerschaum import Pipe
1684
1689
  import time
1685
1690
  import copy
@@ -1691,6 +1696,7 @@ def sync_pipe(
1691
1696
 
1692
1697
  start = time.perf_counter()
1693
1698
  pipe_name = sql_item_name(pipe.target, self.flavor, schema=self.get_pipe_schema(pipe))
1699
+ dtypes = pipe.get_dtypes(debug=debug)
1694
1700
 
1695
1701
  if not pipe.temporary and not pipe.get_id(debug=debug):
1696
1702
  register_tuple = pipe.register(debug=debug)
@@ -1707,6 +1713,7 @@ def sync_pipe(
1707
1713
  df,
1708
1714
  chunksize=chunksize,
1709
1715
  safe_copy=kw.get('safe_copy', False),
1716
+ dtypes=dtypes,
1710
1717
  debug=debug,
1711
1718
  )
1712
1719
 
@@ -1719,35 +1726,18 @@ def sync_pipe(
1719
1726
  ### Check for new columns.
1720
1727
  add_cols_queries = self.get_add_columns_queries(pipe, df, debug=debug)
1721
1728
  if add_cols_queries:
1722
- _ = pipe.__dict__.pop('_columns_indices', None)
1723
- _ = pipe.__dict__.pop('_columns_types', None)
1729
+ pipe._clear_cache_key('_columns_types', debug=debug)
1730
+ pipe._clear_cache_key('_columns_indices', debug=debug)
1724
1731
  if not self.exec_queries(add_cols_queries, debug=debug):
1725
1732
  warn(f"Failed to add new columns to {pipe}.")
1726
1733
 
1727
1734
  alter_cols_queries = self.get_alter_columns_queries(pipe, df, debug=debug)
1728
1735
  if alter_cols_queries:
1729
- _ = pipe.__dict__.pop('_columns_indices', None)
1730
- _ = pipe.__dict__.pop('_columns_types', None)
1736
+ pipe._clear_cache_key('_columns_types', debug=debug)
1737
+ pipe._clear_cache_key('_columns_types', debug=debug)
1731
1738
  if not self.exec_queries(alter_cols_queries, debug=debug):
1732
1739
  warn(f"Failed to alter columns for {pipe}.")
1733
1740
 
1734
- ### NOTE: Oracle SQL < 23c (2023) and SQLite does not support booleans,
1735
- ### so infer bools and persist them to `dtypes`.
1736
- if self.flavor in ('oracle', 'sqlite', 'mysql', 'mariadb'):
1737
- pipe_dtypes = pipe.get_dtypes(infer=False, debug=debug)
1738
- new_bool_cols = {
1739
- col: 'bool[pyarrow]'
1740
- for col, typ in df.dtypes.items()
1741
- if col not in pipe_dtypes
1742
- and are_dtypes_equal(str(typ), 'bool')
1743
- }
1744
- pipe_dtypes.update(new_bool_cols)
1745
- pipe.dtypes = pipe_dtypes
1746
- if new_bool_cols and not pipe.temporary:
1747
- infer_bool_success, infer_bool_msg = pipe.edit(debug=debug)
1748
- if not infer_bool_success:
1749
- return infer_bool_success, infer_bool_msg
1750
-
1751
1741
  upsert = pipe.parameters.get('upsert', False) and (self.flavor + '-upsert') in UPDATE_QUERIES
1752
1742
  if upsert:
1753
1743
  check_existing = False
@@ -1776,7 +1766,7 @@ def sync_pipe(
1776
1766
  if 'name' in kw:
1777
1767
  kw.pop('name')
1778
1768
 
1779
- ### Insert new data into Pipe's table.
1769
+ ### Insert new data into the target table.
1780
1770
  unseen_kw = copy.deepcopy(kw)
1781
1771
  unseen_kw.update({
1782
1772
  'name': pipe.target,
@@ -1797,7 +1787,7 @@ def sync_pipe(
1797
1787
  is_new
1798
1788
  and primary_key
1799
1789
  and primary_key
1800
- not in pipe.dtypes
1790
+ not in dtypes
1801
1791
  and primary_key not in unseen_df.columns
1802
1792
  )
1803
1793
  )
@@ -1897,6 +1887,14 @@ def sync_pipe(
1897
1887
  label=('update' if not upsert else 'upsert'),
1898
1888
  )
1899
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
+
1900
1898
  temp_pipe = Pipe(
1901
1899
  pipe.connector_keys.replace(':', '_') + '_', pipe.metric_key, pipe.location_key,
1902
1900
  instance=pipe.instance_keys,
@@ -1905,34 +1903,30 @@ def sync_pipe(
1905
1903
  for ix_key, ix in pipe.columns.items()
1906
1904
  if ix and ix in update_df.columns
1907
1905
  },
1908
- dtypes={
1909
- col: typ
1910
- for col, typ in pipe.dtypes.items()
1911
- if col in update_df.columns
1912
- },
1906
+ dtypes=update_dtypes,
1913
1907
  target=temp_target,
1914
1908
  temporary=True,
1915
1909
  enforce=False,
1916
1910
  static=True,
1917
1911
  autoincrement=False,
1912
+ cache=False,
1918
1913
  parameters={
1919
1914
  'schema': self.internal_schema,
1920
1915
  'hypertable': False,
1921
1916
  },
1922
1917
  )
1923
- temp_pipe.__dict__['_columns_types'] = {
1924
- col: get_db_type_from_pd_type(
1925
- pipe.dtypes.get(col, str(typ)),
1926
- self.flavor,
1927
- )
1928
- 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()
1929
1921
  }
1930
- now_ts = time.perf_counter()
1931
- temp_pipe.__dict__['_columns_types_timestamp'] = now_ts
1932
- 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)
1933
1926
  temp_success, temp_msg = temp_pipe.sync(update_df, check_existing=False, debug=debug)
1934
1927
  if not temp_success:
1935
1928
  return temp_success, temp_msg
1929
+
1936
1930
  existing_cols = pipe.get_columns_types(debug=debug)
1937
1931
  join_cols = [
1938
1932
  col
@@ -1955,6 +1949,8 @@ def sync_pipe(
1955
1949
  upsert=upsert,
1956
1950
  schema=self.get_pipe_schema(pipe),
1957
1951
  patch_schema=self.internal_schema,
1952
+ target_cols_types=pipe.get_columns_types(debug=debug),
1953
+ patch_cols_types=_temp_columns_types,
1958
1954
  datetime_col=(dt_col if dt_col in update_df.columns else None),
1959
1955
  identity_insert=(autoincrement and primary_key in update_df.columns),
1960
1956
  null_indices=pipe.null_indices,
@@ -2242,13 +2238,13 @@ def sync_pipe_inplace(
2242
2238
 
2243
2239
  add_cols_queries = self.get_add_columns_queries(pipe, new_cols, debug=debug)
2244
2240
  if add_cols_queries:
2245
- _ = pipe.__dict__.pop('_columns_types', None)
2246
- _ = pipe.__dict__.pop('_columns_indices', None)
2241
+ pipe._clear_cache_key('_columns_types', debug=debug)
2242
+ pipe._clear_cache_key('_columns_indices', debug=debug)
2247
2243
  self.exec_queries(add_cols_queries, debug=debug)
2248
2244
 
2249
2245
  alter_cols_queries = self.get_alter_columns_queries(pipe, new_cols, debug=debug)
2250
2246
  if alter_cols_queries:
2251
- _ = pipe.__dict__.pop('_columns_types', None)
2247
+ pipe._clear_cache_key('_columns_types', debug=debug)
2252
2248
  self.exec_queries(alter_cols_queries, debug=debug)
2253
2249
 
2254
2250
  insert_queries = [
@@ -2551,6 +2547,8 @@ def sync_pipe_inplace(
2551
2547
  upsert=upsert,
2552
2548
  schema=self.get_pipe_schema(pipe),
2553
2549
  patch_schema=internal_schema,
2550
+ target_cols_types=pipe.get_columns_types(debug=debug),
2551
+ patch_cols_types=delta_cols_types,
2554
2552
  datetime_col=pipe.columns.get('datetime', None),
2555
2553
  flavor=self.flavor,
2556
2554
  null_indices=pipe.null_indices,
@@ -3041,6 +3039,7 @@ def get_pipe_table(
3041
3039
  from meerschaum.utils.sql import get_sqlalchemy_table
3042
3040
  if not pipe.exists(debug=debug):
3043
3041
  return None
3042
+
3044
3043
  return get_sqlalchemy_table(
3045
3044
  pipe.target,
3046
3045
  connector=self,
@@ -3098,9 +3097,11 @@ def get_pipe_columns_types(
3098
3097
  pipe_table = self.get_pipe_table(pipe, debug=debug)
3099
3098
  if pipe_table is None:
3100
3099
  return {}
3100
+
3101
3101
  if debug:
3102
- dprint(f"Found columns:")
3102
+ dprint("Found columns:")
3103
3103
  mrsm.pprint(dict(pipe_table.columns))
3104
+
3104
3105
  for col in pipe_table.columns:
3105
3106
  table_columns[str(col.name)] = str(col.type)
3106
3107
  except Exception as e:
@@ -3132,6 +3133,7 @@ def get_pipe_columns_indices(
3132
3133
  """
3133
3134
  if pipe.__dict__.get('_skip_check_indices', False):
3134
3135
  return {}
3136
+
3135
3137
  from meerschaum.utils.sql import get_table_cols_indices
3136
3138
  return get_table_cols_indices(
3137
3139
  pipe.target,
@@ -3186,7 +3188,6 @@ def get_add_columns_queries(
3186
3188
  get_db_type_from_pd_type,
3187
3189
  )
3188
3190
  from meerschaum.utils.misc import flatten_list
3189
- table_obj = self.get_pipe_table(pipe, debug=debug)
3190
3191
  is_dask = 'dask' in df.__module__ if not isinstance(df, dict) else False
3191
3192
  if is_dask:
3192
3193
  df = df.partitions[0].compute()
@@ -3210,9 +3211,6 @@ def get_add_columns_queries(
3210
3211
  elif isinstance(val, str):
3211
3212
  df_cols_types[col] = 'str'
3212
3213
  db_cols_types = {
3213
- col: get_pd_type_from_db_type(str(typ.type))
3214
- for col, typ in table_obj.columns.items()
3215
- } if table_obj is not None else {
3216
3214
  col: get_pd_type_from_db_type(typ)
3217
3215
  for col, typ in get_table_cols_types(
3218
3216
  pipe.target,
@@ -3308,7 +3306,6 @@ def get_alter_columns_queries(
3308
3306
  get_db_type_from_pd_type,
3309
3307
  )
3310
3308
  from meerschaum.utils.misc import flatten_list, generate_password, items_str
3311
- table_obj = self.get_pipe_table(pipe, debug=debug)
3312
3309
  target = pipe.target
3313
3310
  session_id = generate_password(3)
3314
3311
  numeric_cols = (
@@ -3329,9 +3326,6 @@ def get_alter_columns_queries(
3329
3326
  else df
3330
3327
  )
3331
3328
  db_cols_types = {
3332
- col: get_pd_type_from_db_type(str(typ.type))
3333
- for col, typ in table_obj.columns.items()
3334
- } if table_obj is not None else {
3335
3329
  col: get_pd_type_from_db_type(typ)
3336
3330
  for col, typ in get_table_cols_types(
3337
3331
  pipe.target,
@@ -3340,7 +3334,7 @@ def get_alter_columns_queries(
3340
3334
  debug=debug,
3341
3335
  ).items()
3342
3336
  }
3343
- pipe_dtypes = pipe.dtypes
3337
+ pipe_dtypes = pipe.get_dtypes(debug=debug)
3344
3338
  pipe_bool_cols = [col for col, typ in pipe_dtypes.items() if are_dtypes_equal(str(typ), 'bool')]
3345
3339
  pd_db_df_aliases = {
3346
3340
  'int': 'bool',
@@ -3352,6 +3346,7 @@ def get_alter_columns_queries(
3352
3346
  pd_db_df_aliases.update({
3353
3347
  'int': 'numeric',
3354
3348
  'date': 'datetime',
3349
+ 'numeric': 'int',
3355
3350
  })
3356
3351
 
3357
3352
  altered_cols = {
@@ -3362,14 +3357,32 @@ def get_alter_columns_queries(
3362
3357
  }
3363
3358
 
3364
3359
  if debug and altered_cols:
3365
- dprint(f"Columns to be altered:")
3360
+ dprint("Columns to be altered:")
3366
3361
  mrsm.pprint(altered_cols)
3367
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
+
3368
3377
  ### NOTE: Sometimes bools are coerced into ints or floats.
3369
3378
  altered_cols_to_ignore = set()
3370
3379
  for col, (db_typ, df_typ) in altered_cols.items():
3371
3380
  for db_alias, df_alias in pd_db_df_aliases.items():
3372
- 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
+ ):
3373
3386
  altered_cols_to_ignore.add(col)
3374
3387
 
3375
3388
  ### Oracle's bool handling sometimes mixes NUMBER and INT.
@@ -3392,7 +3405,7 @@ def get_alter_columns_queries(
3392
3405
  altered_cols_to_ignore.add(bool_col)
3393
3406
 
3394
3407
  if debug and altered_cols_to_ignore:
3395
- dprint(f"Ignoring the following altered columns (false positives).")
3408
+ dprint("Ignoring the following altered columns (false positives).")
3396
3409
  mrsm.pprint(altered_cols_to_ignore)
3397
3410
 
3398
3411
  for col in altered_cols_to_ignore:
@@ -3439,12 +3452,12 @@ def get_alter_columns_queries(
3439
3452
  + sql_item_name(target, self.flavor, self.get_pipe_schema(pipe))
3440
3453
  + " (\n"
3441
3454
  )
3442
- for col_name, col_obj in table_obj.columns.items():
3455
+ for col_name, col_typ in db_cols_types.items():
3443
3456
  create_query += (
3444
3457
  sql_item_name(col_name, self.flavor, None)
3445
3458
  + " "
3446
3459
  + (
3447
- str(col_obj.type)
3460
+ col_typ
3448
3461
  if col_name not in altered_cols
3449
3462
  else altered_cols_types[col_name]
3450
3463
  )
@@ -3458,12 +3471,12 @@ def get_alter_columns_queries(
3458
3471
  + ' ('
3459
3472
  + ', '.join([
3460
3473
  sql_item_name(col_name, self.flavor, None)
3461
- for col_name, _ in table_obj.columns.items()
3474
+ for col_name in db_cols_types
3462
3475
  ])
3463
3476
  + ')'
3464
3477
  + "\nSELECT\n"
3465
3478
  )
3466
- for col_name, col_obj in table_obj.columns.items():
3479
+ for col_name in db_cols_types:
3467
3480
  new_col_str = (
3468
3481
  sql_item_name(col_name, self.flavor, None)
3469
3482
  if col_name not in altered_cols
@@ -3476,6 +3489,7 @@ def get_alter_columns_queries(
3476
3489
  )
3477
3490
  )
3478
3491
  insert_query += new_col_str + ",\n"
3492
+
3479
3493
  insert_query = insert_query[:-2] + (
3480
3494
  f"\nFROM {sql_item_name(temp_table_name, self.flavor, self.get_pipe_schema(pipe))}"
3481
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(
@@ -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