meerschaum 2.6.0.dev1__py3-none-any.whl → 2.6.1__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 (36) hide show
  1. meerschaum/api/dash/pages/login.py +17 -17
  2. meerschaum/api/dash/pipes.py +13 -4
  3. meerschaum/api/routes/_pipes.py +162 -136
  4. meerschaum/config/_version.py +1 -1
  5. meerschaum/config/static/__init__.py +1 -0
  6. meerschaum/connectors/api/_APIConnector.py +1 -0
  7. meerschaum/connectors/api/_pipes.py +46 -13
  8. meerschaum/connectors/sql/_SQLConnector.py +4 -3
  9. meerschaum/connectors/sql/_fetch.py +4 -2
  10. meerschaum/connectors/sql/_pipes.py +496 -148
  11. meerschaum/connectors/sql/_sql.py +37 -16
  12. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -2
  13. meerschaum/connectors/valkey/_pipes.py +13 -5
  14. meerschaum/core/Pipe/__init__.py +20 -0
  15. meerschaum/core/Pipe/_attributes.py +179 -9
  16. meerschaum/core/Pipe/_clear.py +10 -8
  17. meerschaum/core/Pipe/_copy.py +2 -0
  18. meerschaum/core/Pipe/_data.py +57 -28
  19. meerschaum/core/Pipe/_deduplicate.py +30 -28
  20. meerschaum/core/Pipe/_dtypes.py +12 -2
  21. meerschaum/core/Pipe/_fetch.py +11 -9
  22. meerschaum/core/Pipe/_sync.py +24 -7
  23. meerschaum/core/Pipe/_verify.py +51 -48
  24. meerschaum/utils/dataframe.py +16 -8
  25. meerschaum/utils/dtypes/__init__.py +9 -1
  26. meerschaum/utils/dtypes/sql.py +32 -6
  27. meerschaum/utils/misc.py +8 -8
  28. meerschaum/utils/sql.py +485 -16
  29. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/METADATA +1 -1
  30. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/RECORD +36 -36
  31. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/LICENSE +0 -0
  32. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/NOTICE +0 -0
  33. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/WHEEL +0 -0
  34. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/entry_points.txt +0 -0
  35. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/top_level.txt +0 -0
  36. {meerschaum-2.6.0.dev1.dist-info → meerschaum-2.6.1.dist-info}/zip-safe +0 -0
@@ -17,7 +17,7 @@ from meerschaum.utils.warnings import warn
17
17
  ### database flavors that can use bulk insert
18
18
  _bulk_flavors = {'postgresql', 'timescaledb', 'citus'}
19
19
  ### flavors that do not support chunks
20
- _disallow_chunks_flavors = []
20
+ _disallow_chunks_flavors = ['duckdb']
21
21
  _max_chunks_flavors = {'sqlite': 1000}
22
22
  SKIP_READ_TRANSACTION_FLAVORS: list[str] = ['mssql']
23
23
 
@@ -134,7 +134,7 @@ def read(
134
134
  pd = import_pandas()
135
135
  dd = None
136
136
  is_dask = 'dask' in pd.__name__
137
- pd = attempt_import('pandas')
137
+ pandas = attempt_import('pandas')
138
138
  is_dask = dd is not None
139
139
  npartitions = chunksize_to_npartitions(chunksize)
140
140
  if is_dask:
@@ -498,6 +498,8 @@ def exec(
498
498
  commit: Optional[bool] = None,
499
499
  close: Optional[bool] = None,
500
500
  with_connection: bool = False,
501
+ _connection=None,
502
+ _transaction=None,
501
503
  **kw: Any
502
504
  ) -> Union[
503
505
  sqlalchemy.engine.result.resultProxy,
@@ -508,7 +510,7 @@ def exec(
508
510
  ]:
509
511
  """
510
512
  Execute SQL code and return the `sqlalchemy` result, e.g. when calling stored procedures.
511
-
513
+
512
514
  If inserting data, please use bind variables to avoid SQL injection!
513
515
 
514
516
  Parameters
@@ -565,15 +567,24 @@ def exec(
565
567
  if not hasattr(query, 'compile'):
566
568
  query = sqlalchemy.text(query)
567
569
 
568
- connection = self.get_connection()
570
+ connection = _connection if _connection is not None else self.get_connection()
569
571
 
570
572
  try:
571
- transaction = connection.begin() if _commit else None
572
- except sqlalchemy.exc.InvalidRequestError:
573
+ transaction = (
574
+ _transaction
575
+ if _transaction is not None else (
576
+ connection.begin()
577
+ if _commit
578
+ else None
579
+ )
580
+ )
581
+ except sqlalchemy.exc.InvalidRequestError as e:
582
+ if _connection is not None or _transaction is not None:
583
+ raise e
573
584
  connection = self.get_connection(rebuild=True)
574
585
  transaction = connection.begin()
575
586
 
576
- if transaction is not None and not transaction.is_active:
587
+ if transaction is not None and not transaction.is_active and _transaction is not None:
577
588
  connection = self.get_connection(rebuild=True)
578
589
  transaction = connection.begin() if _commit else None
579
590
 
@@ -708,6 +719,8 @@ def to_sql(
708
719
  debug: bool = False,
709
720
  as_tuple: bool = False,
710
721
  as_dict: bool = False,
722
+ _connection=None,
723
+ _transaction=None,
711
724
  **kw
712
725
  ) -> Union[bool, SuccessTuple]:
713
726
  """
@@ -782,6 +795,7 @@ def to_sql(
782
795
  from meerschaum.utils.dtypes.sql import (
783
796
  NUMERIC_PRECISION_FLAVORS,
784
797
  PD_TO_SQLALCHEMY_DTYPES_FLAVORS,
798
+ get_db_type_from_pd_type,
785
799
  )
786
800
  from meerschaum.connectors.sql._create_engine import flavor_configs
787
801
  from meerschaum.utils.packages import attempt_import, import_pandas
@@ -849,6 +863,8 @@ def to_sql(
849
863
  to_sql_kw.update({
850
864
  'parallel': True,
851
865
  })
866
+ elif _connection is not None:
867
+ to_sql_kw['con'] = _connection
852
868
 
853
869
  if_exists_str = "IF EXISTS" if self.flavor in DROP_IF_EXISTS_FLAVORS else ""
854
870
  if self.flavor == 'oracle':
@@ -869,19 +885,24 @@ def to_sql(
869
885
  elif are_dtypes_equal(str(typ), 'int'):
870
886
  dtype[col] = sqlalchemy.types.INTEGER
871
887
  to_sql_kw['dtype'] = dtype
872
- elif self.flavor == 'mssql':
873
- pass
874
- ### TODO clean this up
875
- # dtype = to_sql_kw.get('dtype', {})
876
- # for col, typ in df.dtypes.items():
877
- # if are_dtypes_equal(str(typ), 'bool'):
878
- # dtype[col] = sqlalchemy.types.INTEGER
879
- # to_sql_kw['dtype'] = dtype
880
888
  elif self.flavor == 'duckdb':
881
889
  dtype = to_sql_kw.get('dtype', {})
882
890
  dt_cols = [col for col, typ in df.dtypes.items() if are_dtypes_equal(str(typ), 'datetime')]
883
891
  for col in dt_cols:
884
892
  df[col] = coerce_timezone(df[col], strip_utc=False)
893
+ elif self.flavor == 'mssql':
894
+ dtype = to_sql_kw.get('dtype', {})
895
+ dt_cols = [col for col, typ in df.dtypes.items() if are_dtypes_equal(str(typ), 'datetime')]
896
+ new_dtype = {}
897
+ for col in dt_cols:
898
+ if col in dtype:
899
+ continue
900
+ dt_typ = get_db_type_from_pd_type(str(df.dtypes[col]), self.flavor, as_sqlalchemy=True)
901
+ if col not in dtype:
902
+ new_dtype[col] = dt_typ
903
+
904
+ dtype.update(new_dtype)
905
+ to_sql_kw['dtype'] = dtype
885
906
 
886
907
  ### Check for JSON columns.
887
908
  if self.flavor not in json_flavors:
@@ -916,7 +937,7 @@ def to_sql(
916
937
 
917
938
  try:
918
939
  with warnings.catch_warnings():
919
- warnings.filterwarnings('ignore', 'case sensitivity issues')
940
+ warnings.filterwarnings('ignore')
920
941
  df.to_sql(**to_sql_kw)
921
942
  success = True
922
943
  except Exception as e:
@@ -408,6 +408,7 @@ class ValkeyConnector(Connector):
408
408
  -------
409
409
  A list of dictionaries, where all keys and values are strings.
410
410
  """
411
+ from meerschaum.utils.dtypes import coerce_timezone
411
412
  table_name = self.quote_table(table)
412
413
  datetime_column_key = self.get_datetime_column_key(table)
413
414
  datetime_column = self.get(datetime_column_key)
@@ -424,10 +425,10 @@ class ValkeyConnector(Connector):
424
425
  dateutil_parser = mrsm.attempt_import('dateutil.parser')
425
426
 
426
427
  if isinstance(begin, str):
427
- begin = dateutil_parser.parse(begin)
428
+ begin = coerce_timezone(dateutil_parser.parse(begin))
428
429
 
429
430
  if isinstance(end, str):
430
- end = dateutil_parser.parse(end)
431
+ end = coerce_timezone(dateutil_parser.parse(end))
431
432
 
432
433
  begin_ts = (
433
434
  (
@@ -409,6 +409,7 @@ def get_pipe_data(
409
409
  return None
410
410
 
411
411
  from meerschaum.utils.dataframe import query_df, parse_df_datetimes
412
+ from meerschaum.utils.dtypes import are_dtypes_equal
412
413
 
413
414
  valkey_dtypes = pipe.parameters.get('valkey', {}).get('dtypes', {})
414
415
  dt_col = pipe.columns.get('datetime', None)
@@ -442,13 +443,14 @@ def get_pipe_data(
442
443
  ignore_dt_cols = [
443
444
  col
444
445
  for col, dtype in pipe.dtypes.items()
445
- if 'datetime' not in str(dtype)
446
+ if not are_dtypes_equal(str(dtype), 'datetime')
446
447
  ]
447
448
 
448
449
  df = parse_df_datetimes(
449
450
  docs,
450
451
  ignore_cols=ignore_dt_cols,
451
452
  chunksize=kwargs.get('chunksize', None),
453
+ strip_timezone=(pipe.tzinfo is None),
452
454
  debug=debug,
453
455
  )
454
456
  for col, typ in valkey_dtypes.items():
@@ -501,6 +503,7 @@ def sync_pipe(
501
503
  -------
502
504
  A `SuccessTuple` indicating success.
503
505
  """
506
+ from meerschaum.utils.dtypes import are_dtypes_equal
504
507
  dt_col = pipe.columns.get('datetime', None)
505
508
  indices = [col for col in pipe.columns.values() if col]
506
509
  table_name = self.quote_table(pipe.target)
@@ -508,6 +511,7 @@ def sync_pipe(
508
511
  if is_dask:
509
512
  df = df.compute()
510
513
  upsert = pipe.parameters.get('upsert', False)
514
+ static = pipe.parameters.get('static', False)
511
515
 
512
516
  def _serialize_indices_docs(_docs):
513
517
  return [
@@ -526,7 +530,11 @@ def sync_pipe(
526
530
 
527
531
  valkey_dtypes = pipe.parameters.get('valkey', {}).get('dtypes', {})
528
532
  new_dtypes = {
529
- str(key): str(val)
533
+ str(key): (
534
+ str(val)
535
+ if not are_dtypes_equal(str(val), 'datetime')
536
+ else 'datetime64[ns, UTC]'
537
+ )
530
538
  for key, val in df.dtypes.items()
531
539
  if str(key) not in valkey_dtypes
532
540
  }
@@ -539,7 +547,7 @@ def sync_pipe(
539
547
  new_dtypes[col] = 'string'
540
548
  df[col] = df[col].astype('string')
541
549
 
542
- if new_dtypes:
550
+ if new_dtypes and not static:
543
551
  valkey_dtypes.update(new_dtypes)
544
552
  if 'valkey' not in pipe.parameters:
545
553
  pipe.parameters['valkey'] = {}
@@ -625,7 +633,7 @@ def get_pipe_columns_types(
625
633
 
626
634
  from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
627
635
  return {
628
- col: get_db_type_from_pd_type(typ)
636
+ col: get_db_type_from_pd_type(typ, flavor='postgresql')
629
637
  for col, typ in pipe.parameters.get('valkey', {}).get('dtypes', {}).items()
630
638
  }
631
639
 
@@ -733,7 +741,7 @@ def get_sync_time(
733
741
  return (
734
742
  int(dt_val)
735
743
  if are_dtypes_equal(dt_typ, 'int')
736
- else dateutil_parser.parse(str(dt_val)).replace(tzinfo=None)
744
+ else dateutil_parser.parse(str(dt_val))
737
745
  )
738
746
  except Exception as e:
739
747
  warn(f"Failed to parse sync time for {pipe}:\n{e}")
@@ -92,6 +92,7 @@ class Pipe:
92
92
  _get_data_as_iterator,
93
93
  get_chunk_interval,
94
94
  get_chunk_bounds,
95
+ parse_date_bounds,
95
96
  )
96
97
  from ._register import register
97
98
  from ._attributes import (
@@ -101,8 +102,13 @@ class Pipe:
101
102
  indices,
102
103
  indexes,
103
104
  dtypes,
105
+ autoincrement,
106
+ upsert,
107
+ static,
108
+ tzinfo,
104
109
  get_columns,
105
110
  get_columns_types,
111
+ get_columns_indices,
106
112
  get_indices,
107
113
  tags,
108
114
  get_id,
@@ -154,6 +160,8 @@ class Pipe:
154
160
  instance: Optional[Union[str, InstanceConnector]] = None,
155
161
  temporary: bool = False,
156
162
  upsert: Optional[bool] = None,
163
+ autoincrement: Optional[bool] = None,
164
+ static: Optional[bool] = None,
157
165
  mrsm_instance: Optional[Union[str, InstanceConnector]] = None,
158
166
  cache: bool = False,
159
167
  debug: bool = False,
@@ -205,6 +213,12 @@ class Pipe:
205
213
  upsert: Optional[bool], default None
206
214
  If `True`, set `upsert` to `True` in the parameters.
207
215
 
216
+ autoincrement: Optional[bool], default None
217
+ If `True`, set `autoincrement` in the parameters.
218
+
219
+ static: Optional[bool], default None
220
+ If `True`, set `static` in the parameters.
221
+
208
222
  temporary: bool, default False
209
223
  If `True`, prevent instance tables (pipes, users, plugins) from being created.
210
224
 
@@ -299,6 +313,12 @@ class Pipe:
299
313
  if isinstance(upsert, bool):
300
314
  self._attributes['parameters']['upsert'] = upsert
301
315
 
316
+ if isinstance(autoincrement, bool):
317
+ self._attributes['parameters']['autoincrement'] = autoincrement
318
+
319
+ if isinstance(static, bool):
320
+ self._attributes['parameters']['static'] = static
321
+
302
322
  ### NOTE: The parameters dictionary is {} by default.
303
323
  ### A Pipe may be registered without parameters, then edited,
304
324
  ### or a Pipe may be registered with parameters set in-memory first.
@@ -7,9 +7,14 @@ Fetch and manipulate Pipes' attributes
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
11
+ from datetime import timezone
12
+
13
+ import meerschaum as mrsm
10
14
  from meerschaum.utils.typing import Tuple, Dict, SuccessTuple, Any, Union, Optional, List
11
15
  from meerschaum.utils.warnings import warn
12
16
 
17
+
13
18
  @property
14
19
  def attributes(self) -> Dict[str, Any]:
15
20
  """
@@ -50,6 +55,13 @@ def parameters(self) -> Optional[Dict[str, Any]]:
50
55
  """
51
56
  if 'parameters' not in self.attributes:
52
57
  self.attributes['parameters'] = {}
58
+ _parameters = self.attributes['parameters']
59
+ dt_col = _parameters.get('columns', {}).get('datetime', None)
60
+ dt_typ = _parameters.get('dtypes', {}).get(dt_col, None) if dt_col else None
61
+ if dt_col and not dt_typ:
62
+ if 'dtypes' not in _parameters:
63
+ self.attributes['parameters']['dtypes'] = {}
64
+ self.attributes['parameters']['dtypes'][dt_col] = 'datetime'
53
65
  return self.attributes['parameters']
54
66
 
55
67
 
@@ -84,7 +96,7 @@ def columns(self, _columns: Union[Dict[str, str], List[str]]) -> None:
84
96
  """
85
97
  if isinstance(_columns, (list, tuple)):
86
98
  _columns = {col: col for col in _columns}
87
- if not isinstance(columns, dict):
99
+ if not isinstance(_columns, dict):
88
100
  warn(f"{self}.columns must be a dictionary, received {type(_columns)}.")
89
101
  return
90
102
  self.parameters['columns'] = _columns
@@ -108,7 +120,7 @@ def indices(self) -> Union[Dict[str, Union[str, List[str]]], None]:
108
120
  if not isinstance(_indices, dict):
109
121
  _indices = {}
110
122
  self.parameters[indices_key] = _indices
111
- unique_cols = (
123
+ unique_cols = list(set((
112
124
  [dt_col]
113
125
  if dt_col
114
126
  else []
@@ -116,7 +128,7 @@ def indices(self) -> Union[Dict[str, Union[str, List[str]]], None]:
116
128
  col
117
129
  for col_ix, col in _columns.items()
118
130
  if col_ix != 'datetime'
119
- ]
131
+ ]))
120
132
  return {
121
133
  **({'unique': unique_cols} if len(unique_cols) > 1 else {}),
122
134
  **_columns,
@@ -203,6 +215,80 @@ def dtypes(self, _dtypes: Dict[str, Any]) -> None:
203
215
  self.parameters['dtypes'] = _dtypes
204
216
 
205
217
 
218
+ @property
219
+ def upsert(self) -> bool:
220
+ """
221
+ Return whether `upsert` is set for the pipe.
222
+ """
223
+ if 'upsert' not in self.parameters:
224
+ self.parameters['upsert'] = False
225
+ return self.parameters['upsert']
226
+
227
+
228
+ @upsert.setter
229
+ def upsert(self, _upsert: bool) -> None:
230
+ """
231
+ Set the `upsert` parameter for the pipe.
232
+ """
233
+ self.parameters['upsert'] = _upsert
234
+
235
+
236
+ @property
237
+ def static(self) -> bool:
238
+ """
239
+ Return whether `static` is set for the pipe.
240
+ """
241
+ if 'static' not in self.parameters:
242
+ self.parameters['static'] = False
243
+ return self.parameters['static']
244
+
245
+
246
+ @static.setter
247
+ def static(self, _static: bool) -> None:
248
+ """
249
+ Set the `static` parameter for the pipe.
250
+ """
251
+ self.parameters['static'] = _static
252
+
253
+
254
+ @property
255
+ def autoincrement(self) -> bool:
256
+ """
257
+ Return the `autoincrement` parameter for the pipe.
258
+ """
259
+ if 'autoincrement' not in self.parameters:
260
+ self.parameters['autoincrement'] = False
261
+
262
+ return self.parameters['autoincrement']
263
+
264
+
265
+ @autoincrement.setter
266
+ def autoincrement(self, _autoincrement: bool) -> None:
267
+ """
268
+ Set the `autoincrement` parameter for the pipe.
269
+ """
270
+ self.parameters['autoincrement'] = _autoincrement
271
+
272
+
273
+ @property
274
+ def tzinfo(self) -> Union[None, timezone]:
275
+ """
276
+ Return `timezone.utc` if the pipe is timezone-aware.
277
+ """
278
+ dt_col = self.columns.get('datetime', None)
279
+ if not dt_col:
280
+ return None
281
+
282
+ dt_typ = str(self.dtypes.get(dt_col, 'datetime64[ns, UTC]'))
283
+ if 'utc' in dt_typ.lower() or dt_typ == 'datetime':
284
+ return timezone.utc
285
+
286
+ if dt_typ == 'datetime64[ns]':
287
+ return None
288
+
289
+ return None
290
+
291
+
206
292
  def get_columns(self, *args: str, error: bool = False) -> Union[str, Tuple[str]]:
207
293
  """
208
294
  Check if the requested columns are defined.
@@ -248,12 +334,19 @@ def get_columns(self, *args: str, error: bool = False) -> Union[str, Tuple[str]]
248
334
  return tuple(col_names)
249
335
 
250
336
 
251
- def get_columns_types(self, debug: bool = False) -> Union[Dict[str, str], None]:
337
+ def get_columns_types(
338
+ self,
339
+ refresh: bool = False,
340
+ debug: bool = False,
341
+ ) -> Union[Dict[str, str], None]:
252
342
  """
253
343
  Get a dictionary of a pipe's column names and their types.
254
344
 
255
345
  Parameters
256
346
  ----------
347
+ refresh: bool, default False
348
+ If `True`, invalidate the cache and fetch directly from the instance connector.
349
+
257
350
  debug: bool, default False:
258
351
  Verbosity toggle.
259
352
 
@@ -265,17 +358,91 @@ def get_columns_types(self, debug: bool = False) -> Union[Dict[str, str], None]:
265
358
  --------
266
359
  >>> pipe.get_columns_types()
267
360
  {
268
- 'dt': 'TIMESTAMP WITHOUT TIMEZONE',
361
+ 'dt': 'TIMESTAMP WITH TIMEZONE',
269
362
  'id': 'BIGINT',
270
363
  'val': 'DOUBLE PRECISION',
271
364
  }
272
365
  >>>
273
366
  """
274
- from meerschaum.utils.venv import Venv
367
+ import time
275
368
  from meerschaum.connectors import get_connector_plugin
369
+ from meerschaum.config.static import STATIC_CONFIG
370
+ from meerschaum.utils.warnings import dprint
276
371
 
277
- with Venv(get_connector_plugin(self.instance_connector)):
278
- return self.instance_connector.get_pipe_columns_types(self, debug=debug)
372
+ now = time.perf_counter()
373
+ cache_seconds = STATIC_CONFIG['pipes']['static_schema_cache_seconds']
374
+ static = self.parameters.get('static', False)
375
+ if not static:
376
+ refresh = True
377
+ if refresh:
378
+ _ = self.__dict__.pop('_columns_types_timestamp', None)
379
+ _ = self.__dict__.pop('_columns_types', None)
380
+ _columns_types = self.__dict__.get('_columns_types', None)
381
+ if _columns_types:
382
+ columns_types_timestamp = self.__dict__.get('_columns_types_timestamp', None)
383
+ if columns_types_timestamp is not None:
384
+ delta = now - columns_types_timestamp
385
+ if delta < cache_seconds:
386
+ if debug:
387
+ dprint(
388
+ f"Returning cached `columns_types` for {self} "
389
+ f"({round(delta, 2)} seconds old)."
390
+ )
391
+ return _columns_types
392
+
393
+ with mrsm.Venv(get_connector_plugin(self.instance_connector)):
394
+ _columns_types = (
395
+ self.instance_connector.get_pipe_columns_types(self, debug=debug)
396
+ if hasattr(self.instance_connector, 'get_pipe_columns_types')
397
+ else None
398
+ )
399
+
400
+ self.__dict__['_columns_types'] = _columns_types
401
+ self.__dict__['_columns_types_timestamp'] = now
402
+ return _columns_types or {}
403
+
404
+
405
+ def get_columns_indices(
406
+ self,
407
+ debug: bool = False,
408
+ refresh: bool = False,
409
+ ) -> Dict[str, List[Dict[str, str]]]:
410
+ """
411
+ Return a dictionary mapping columns to index information.
412
+ """
413
+ import time
414
+ from meerschaum.connectors import get_connector_plugin
415
+ from meerschaum.config.static import STATIC_CONFIG
416
+ from meerschaum.utils.warnings import dprint
417
+
418
+ now = time.perf_counter()
419
+ exists_timeout_seconds = STATIC_CONFIG['pipes']['exists_timeout_seconds']
420
+ if refresh:
421
+ _ = self.__dict__.pop('_columns_indices_timestamp', None)
422
+ _ = self.__dict__.pop('_columns_indices', None)
423
+ _columns_indices = self.__dict__.get('_columns_indices', None)
424
+ if _columns_indices:
425
+ columns_indices_timestamp = self.__dict__.get('_columns_indices_timestamp', None)
426
+ if columns_indices_timestamp is not None:
427
+ delta = now - columns_indices_timestamp
428
+ if delta < exists_timeout_seconds:
429
+ if debug:
430
+ dprint(
431
+ f"Returning cached `columns_indices` for {self} "
432
+ f"({round(delta, 2)} seconds old)."
433
+ )
434
+ return _columns_indices
435
+
436
+ with mrsm.Venv(get_connector_plugin(self.instance_connector)):
437
+ _columns_indices = (
438
+ self.instance_connector.get_pipe_columns_indices(self, debug=debug)
439
+ if hasattr(self.instance_connector, 'get_pipe_columns_indices')
440
+ else None
441
+ )
442
+
443
+ self.__dict__['_columns_indices'] = _columns_indices
444
+ self.__dict__['_columns_indices_timestamp'] = now
445
+ return _columns_indices or {}
279
446
 
280
447
 
281
448
  def get_id(self, **kw: Any) -> Union[int, None]:
@@ -289,7 +456,10 @@ def get_id(self, **kw: Any) -> Union[int, None]:
289
456
  from meerschaum.connectors import get_connector_plugin
290
457
 
291
458
  with Venv(get_connector_plugin(self.instance_connector)):
292
- return self.instance_connector.get_pipe_id(self, **kw)
459
+ if hasattr(self.instance_connector, 'get_pipe_id'):
460
+ return self.instance_connector.get_pipe_id(self, **kw)
461
+
462
+ return None
293
463
 
294
464
 
295
465
  @property
@@ -58,12 +58,14 @@ def clear(
58
58
  from meerschaum.utils.venv import Venv
59
59
  from meerschaum.connectors import get_connector_plugin
60
60
 
61
+ begin, end = self.parse_date_bounds(begin, end)
62
+
61
63
  if self.cache_pipe is not None:
62
64
  success, msg = self.cache_pipe.clear(
63
- begin = begin,
64
- end = end,
65
- params = params,
66
- debug = debug,
65
+ begin=begin,
66
+ end=end,
67
+ params=params,
68
+ debug=debug,
67
69
  **kwargs
68
70
  )
69
71
  if not success:
@@ -72,9 +74,9 @@ def clear(
72
74
  with Venv(get_connector_plugin(self.instance_connector)):
73
75
  return self.instance_connector.clear_pipe(
74
76
  self,
75
- begin = begin,
76
- end = end,
77
- params = params,
78
- debug = debug,
77
+ begin=begin,
78
+ end=end,
79
+ params=params,
80
+ debug=debug,
79
81
  **kwargs
80
82
  )
@@ -55,6 +55,8 @@ def copy_to(
55
55
  if str(instance_keys) == self.instance_keys:
56
56
  return False, f"Cannot copy {self} to instance '{instance_keys}'."
57
57
 
58
+ begin, end = self.parse_date_bounds(begin, end)
59
+
58
60
  new_pipe = mrsm.Pipe(
59
61
  self.connector_keys,
60
62
  self.metric_key,