meerschaum 3.0.0rc1__py3-none-any.whl → 3.0.0rc3__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 (66) hide show
  1. meerschaum/_internal/arguments/_parser.py +2 -1
  2. meerschaum/_internal/docs/index.py +49 -2
  3. meerschaum/_internal/shell/Shell.py +5 -4
  4. meerschaum/_internal/static.py +8 -24
  5. meerschaum/actions/bootstrap.py +1 -1
  6. meerschaum/actions/edit.py +6 -3
  7. meerschaum/actions/start.py +1 -1
  8. meerschaum/actions/verify.py +5 -8
  9. meerschaum/api/__init__.py +2 -1
  10. meerschaum/api/dash/__init__.py +0 -2
  11. meerschaum/api/dash/callbacks/__init__.py +1 -0
  12. meerschaum/api/dash/callbacks/dashboard.py +20 -19
  13. meerschaum/api/dash/callbacks/jobs.py +11 -5
  14. meerschaum/api/dash/callbacks/pipes.py +106 -5
  15. meerschaum/api/dash/callbacks/settings/__init__.py +0 -1
  16. meerschaum/api/dash/callbacks/{settings/tokens.py → tokens.py} +1 -1
  17. meerschaum/api/dash/jobs.py +1 -1
  18. meerschaum/api/dash/pages/__init__.py +2 -1
  19. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  20. meerschaum/api/dash/pages/pipes.py +4 -3
  21. meerschaum/api/dash/pages/settings/__init__.py +0 -1
  22. meerschaum/api/dash/pages/{settings/tokens.py → tokens.py} +6 -8
  23. meerschaum/api/dash/pipes.py +131 -0
  24. meerschaum/api/dash/tokens.py +28 -31
  25. meerschaum/api/routes/_pipes.py +47 -37
  26. meerschaum/config/_default.py +13 -2
  27. meerschaum/config/_paths.py +1 -0
  28. meerschaum/config/_version.py +1 -1
  29. meerschaum/config/stack/__init__.py +9 -8
  30. meerschaum/connectors/api/_pipes.py +2 -18
  31. meerschaum/connectors/api/_tokens.py +2 -2
  32. meerschaum/connectors/instance/_tokens.py +10 -6
  33. meerschaum/connectors/sql/_SQLConnector.py +14 -0
  34. meerschaum/connectors/sql/_create_engine.py +3 -14
  35. meerschaum/connectors/sql/_pipes.py +175 -185
  36. meerschaum/connectors/sql/_sql.py +38 -20
  37. meerschaum/connectors/sql/tables/__init__.py +237 -122
  38. meerschaum/connectors/valkey/_pipes.py +44 -16
  39. meerschaum/core/Pipe/__init__.py +28 -5
  40. meerschaum/core/Pipe/_attributes.py +273 -46
  41. meerschaum/core/Pipe/_data.py +55 -17
  42. meerschaum/core/Pipe/_dtypes.py +19 -4
  43. meerschaum/core/Pipe/_edit.py +2 -0
  44. meerschaum/core/Pipe/_fetch.py +1 -1
  45. meerschaum/core/Pipe/_sync.py +90 -160
  46. meerschaum/core/Pipe/_verify.py +3 -3
  47. meerschaum/core/Token/_Token.py +4 -5
  48. meerschaum/plugins/bootstrap.py +508 -3
  49. meerschaum/utils/_get_pipes.py +1 -1
  50. meerschaum/utils/dataframe.py +385 -68
  51. meerschaum/utils/debug.py +15 -15
  52. meerschaum/utils/dtypes/__init__.py +387 -22
  53. meerschaum/utils/dtypes/sql.py +327 -31
  54. meerschaum/utils/misc.py +9 -68
  55. meerschaum/utils/packages/__init__.py +7 -21
  56. meerschaum/utils/packages/_packages.py +7 -2
  57. meerschaum/utils/schedule.py +1 -1
  58. meerschaum/utils/sql.py +8 -8
  59. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/METADATA +5 -17
  60. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/RECORD +66 -65
  61. meerschaum-3.0.0rc3.dist-info/licenses/NOTICE +2 -0
  62. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/WHEEL +0 -0
  63. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/entry_points.txt +0 -0
  64. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/licenses/LICENSE +0 -0
  65. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/top_level.txt +0 -0
  66. {meerschaum-3.0.0rc1.dist-info → meerschaum-3.0.0rc3.dist-info}/zip-safe +0 -0
@@ -131,23 +131,28 @@ def read(
131
131
  """
132
132
  if chunks is not None and chunks <= 0:
133
133
  return []
134
+
134
135
  from meerschaum.utils.sql import sql_item_name, truncate_item_name
135
136
  from meerschaum.utils.dtypes import are_dtypes_equal, coerce_timezone
136
137
  from meerschaum.utils.dtypes.sql import TIMEZONE_NAIVE_FLAVORS
137
138
  from meerschaum.utils.packages import attempt_import, import_pandas
138
139
  from meerschaum.utils.pool import get_pool
139
140
  from meerschaum.utils.dataframe import chunksize_to_npartitions, get_numeric_cols
141
+ from meerschaum.utils.misc import filter_arguments
140
142
  import warnings
141
143
  import traceback
142
144
  from decimal import Decimal
145
+
143
146
  pd = import_pandas()
144
147
  dd = None
148
+
145
149
  is_dask = 'dask' in pd.__name__
146
150
  pandas = attempt_import('pandas')
147
151
  is_dask = dd is not None
148
152
  npartitions = chunksize_to_npartitions(chunksize)
149
153
  if is_dask:
150
154
  chunksize = None
155
+
151
156
  schema = schema or self.schema
152
157
  utc_dt_cols = [
153
158
  col
@@ -158,7 +163,7 @@ def read(
158
163
  if dtype and utc_dt_cols and self.flavor in TIMEZONE_NAIVE_FLAVORS:
159
164
  dtype = dtype.copy()
160
165
  for col in utc_dt_cols:
161
- dtype[col] = 'datetime64[ns]'
166
+ dtype[col] = 'datetime64[us]'
162
167
 
163
168
  pool = get_pool(workers=workers)
164
169
  sqlalchemy = attempt_import("sqlalchemy", lazy=False)
@@ -222,26 +227,33 @@ def read(
222
227
  else format_sql_query_for_dask(str_query)
223
228
  )
224
229
 
230
+ def _get_chunk_args_kwargs(_chunk):
231
+ return filter_arguments(
232
+ chunk_hook,
233
+ _chunk,
234
+ workers=workers,
235
+ chunksize=chunksize,
236
+ debug=debug,
237
+ **kw
238
+ )
239
+
225
240
  chunk_list = []
226
241
  chunk_hook_results = []
227
242
  def _process_chunk(_chunk, _retry_on_failure: bool = True):
228
243
  if self.flavor in TIMEZONE_NAIVE_FLAVORS:
229
244
  for col in utc_dt_cols:
230
- _chunk[col] = coerce_timezone(_chunk[col], strip_timezone=False)
245
+ _chunk[col] = coerce_timezone(_chunk[col], strip_utc=False)
231
246
  if not as_hook_results:
232
247
  chunk_list.append(_chunk)
248
+
233
249
  if chunk_hook is None:
234
250
  return None
235
251
 
252
+ chunk_args, chunk_kwargs = _get_chunk_args_kwargs(_chunk)
253
+
236
254
  result = None
237
255
  try:
238
- result = chunk_hook(
239
- _chunk,
240
- workers=workers,
241
- chunksize=chunksize,
242
- debug=debug,
243
- **kw
244
- )
256
+ result = chunk_hook(*chunk_args, **chunk_kwargs)
245
257
  except Exception:
246
258
  result = False, traceback.format_exc()
247
259
  from meerschaum.utils.formatting import get_console
@@ -292,8 +304,16 @@ def read(
292
304
  self.engine,
293
305
  **read_sql_query_kwargs
294
306
  )
307
+
295
308
  to_return = (
296
- chunk_generator
309
+ (
310
+ chunk_generator
311
+ if not (as_hook_results or chunksize is None)
312
+ else (
313
+ _process_chunk(_chunk)
314
+ for _chunk in chunk_generator
315
+ )
316
+ )
297
317
  if as_iterator or chunksize is None
298
318
  else (
299
319
  list(pool.imap(_process_chunk, chunk_generator))
@@ -339,9 +359,8 @@ def read(
339
359
  try:
340
360
  for chunk in chunk_generator:
341
361
  if chunk_hook is not None:
342
- chunk_hook_results.append(
343
- chunk_hook(chunk, chunksize=chunksize, debug=debug, **kw)
344
- )
362
+ chunk_args, chunk_kwargs = _get_chunk_args_kwargs(chunk)
363
+ chunk_hook_results.append(chunk_hook(*chunk_args, **chunk_kwargs))
345
364
  chunk_list.append(chunk)
346
365
  read_chunks += 1
347
366
  if chunks is not None and read_chunks >= chunks:
@@ -356,9 +375,8 @@ def read(
356
375
  try:
357
376
  for chunk in chunk_generator:
358
377
  if chunk_hook is not None:
359
- chunk_hook_results.append(
360
- chunk_hook(chunk, chunksize=chunksize, debug=debug, **kw)
361
- )
378
+ chunk_args, chunk_kwargs = _get_chunk_args_kwargs(chunk)
379
+ chunk_hook_results.append(chunk_hook(*chunk_args, **chunk_kwargs))
362
380
  chunk_list.append(chunk)
363
381
  read_chunks += 1
364
382
  if chunks is not None and read_chunks >= chunks:
@@ -389,9 +407,8 @@ def read(
389
407
  ### call the hook on any missed chunks.
390
408
  if chunk_hook is not None and len(chunk_list) > len(chunk_hook_results):
391
409
  for c in chunk_list[len(chunk_hook_results):]:
392
- chunk_hook_results.append(
393
- chunk_hook(c, chunksize=chunksize, debug=debug, **kw)
394
- )
410
+ chunk_args, chunk_kwargs = _get_chunk_args_kwargs(c)
411
+ chunk_hook_results.append(chunk_hook(*chunk_args, **chunk_kwargs))
395
412
 
396
413
  ### chunksize is not None so must iterate
397
414
  if debug:
@@ -784,6 +801,7 @@ def to_sql(
784
801
  from meerschaum.utils.warnings import error, warn
785
802
  import warnings
786
803
  import functools
804
+ import traceback
787
805
 
788
806
  if name is None:
789
807
  error(f"Name must not be `None` to insert data into {self}.")
@@ -1057,7 +1075,7 @@ def to_sql(
1057
1075
  except Exception as e:
1058
1076
  if not silent:
1059
1077
  warn(str(e))
1060
- success, msg = False, str(e)
1078
+ success, msg = False, traceback.format_exc()
1061
1079
 
1062
1080
  end = time.perf_counter()
1063
1081
  if success:
@@ -7,19 +7,24 @@ Define SQLAlchemy tables
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
11
+ import pickle
12
+ import threading
13
+ import meerschaum as mrsm
10
14
  from meerschaum.utils.typing import Optional, Dict, Union, InstanceConnector, List
11
- from meerschaum.utils.warnings import error, warn
15
+ from meerschaum.utils.warnings import error, warn, dprint
12
16
 
13
17
  ### store a tables dict for each connector
14
18
  connector_tables = {}
19
+ _tables_locks = {}
15
20
 
16
21
  _sequence_flavors = {'duckdb', 'oracle'}
17
22
  _skip_index_names_flavors = {'mssql',}
18
23
 
19
24
  def get_tables(
20
25
  mrsm_instance: Optional[Union[str, InstanceConnector]] = None,
21
- create: bool = True,
22
- debug: Optional[bool] = None
26
+ create: Optional[bool] = None,
27
+ debug: bool = False,
23
28
  ) -> Union[Dict[str, 'sqlalchemy.Table'], bool]:
24
29
  """
25
30
  Create tables on the database and return the `sqlalchemy` tables.
@@ -29,10 +34,10 @@ def get_tables(
29
34
  mrsm_instance: Optional[Union[str, InstanceConnector]], default None
30
35
  The connector on which the tables reside.
31
36
 
32
- create: bool, default True:
37
+ create: Optional[bool], default None
33
38
  If `True`, create the tables if they don't exist.
34
39
 
35
- debug: Optional[bool], default None:
40
+ debug: bool, default False
36
41
  Verbosity Toggle.
37
42
 
38
43
  Returns
@@ -42,7 +47,6 @@ def get_tables(
42
47
 
43
48
  """
44
49
  from meerschaum.utils.debug import dprint
45
- from meerschaum.utils.formatting import pprint
46
50
  from meerschaum.connectors.parse import parse_instance_keys
47
51
  from meerschaum.utils.packages import attempt_import
48
52
  from meerschaum.utils.sql import json_flavors
@@ -54,7 +58,7 @@ def get_tables(
54
58
  lazy=False,
55
59
  )
56
60
  if not sqlalchemy:
57
- error(f"Failed to import sqlalchemy. Is sqlalchemy installed?")
61
+ error("Failed to import sqlalchemy. Is sqlalchemy installed?")
58
62
 
59
63
  if mrsm_instance is None:
60
64
  conn = get_connector(debug=debug)
@@ -63,149 +67,196 @@ def get_tables(
63
67
  else: ### NOTE: mrsm_instance MUST BE a SQL Connector for this to work!
64
68
  conn = mrsm_instance
65
69
 
66
- ### kind of a hack. Create the tables remotely
67
- from meerschaum.connectors.api import APIConnector
68
- if isinstance(conn, APIConnector):
69
- if create:
70
- return conn.create_metadata(debug=debug)
71
- return {}
70
+ cache_expired = _check_create_cache(conn, debug=debug) if conn.type == 'sql' else False
71
+ create = create or cache_expired
72
72
 
73
73
  ### Skip if the connector is not a SQL connector.
74
74
  if getattr(conn, 'type', None) != 'sql':
75
75
  return {}
76
76
 
77
- if conn not in connector_tables:
78
- if debug:
79
- dprint(f"Creating tables for connector '{conn}'.")
80
-
81
- id_type = sqlalchemy.Integer
82
- if conn.flavor in json_flavors:
83
- params_type = sqlalchemy.types.JSON
84
- else:
85
- params_type = sqlalchemy.types.Text
86
- id_names = ('user_id', 'plugin_id', 'pipe_id')
87
- sequences = {
88
- k: sqlalchemy.Sequence(k + '_seq')
89
- for k in id_names
90
- }
91
- id_col_args = { k: [k, id_type] for k in id_names }
92
- id_col_kw = { k: {'primary_key': True} for k in id_names }
93
- index_names = conn.flavor not in _skip_index_names_flavors
94
-
95
- if conn.flavor in _sequence_flavors:
96
- for k, args in id_col_args.items():
97
- args.append(sequences[k])
98
- for k, kw in id_col_kw.items():
99
- kw.update({'server_default': sequences[k].next_value()})
100
-
101
- _tables = {
102
- 'users': sqlalchemy.Table(
103
- 'mrsm_users',
104
- conn.metadata,
105
- sqlalchemy.Column(
106
- *id_col_args['user_id'],
107
- **id_col_kw['user_id'],
77
+ conn_key = str(conn)
78
+ if conn_key in connector_tables:
79
+ return connector_tables[conn_key]
80
+
81
+ fasteners = attempt_import('fasteners')
82
+ pickle_path = conn.get_metadata_cache_path(kind='pkl')
83
+ lock_path = pickle_path.with_suffix('.lock')
84
+ lock = fasteners.InterProcessLock(lock_path)
85
+
86
+ with lock:
87
+ if not cache_expired and pickle_path.exists():
88
+ try:
89
+ with open(pickle_path, 'rb') as f:
90
+ metadata = pickle.load(f)
91
+ metadata.bind = conn.engine
92
+ tables = {tbl.name.replace('mrsm_', ''): tbl for tbl in metadata.tables.values()}
93
+ connector_tables[conn_key] = tables
94
+ return tables
95
+ except Exception as e:
96
+ warn(f"Failed to load metadata from cache, rebuilding: {e}")
97
+
98
+ if conn_key not in _tables_locks:
99
+ _tables_locks[conn_key] = threading.Lock()
100
+
101
+ with _tables_locks[conn_key]:
102
+ if conn_key not in connector_tables:
103
+ if debug:
104
+ dprint(f"Building in-memory instance tables for '{conn}'.")
105
+
106
+ id_type = sqlalchemy.Integer
107
+ if conn.flavor in json_flavors:
108
+ from sqlalchemy.dialects.postgresql import JSONB
109
+ params_type = JSONB
110
+ else:
111
+ params_type = sqlalchemy.types.Text
112
+ id_names = ('user_id', 'plugin_id', 'pipe_id')
113
+ sequences = {
114
+ k: sqlalchemy.Sequence(k + '_seq')
115
+ for k in id_names
116
+ }
117
+ id_col_args = { k: [k, id_type] for k in id_names }
118
+ id_col_kw = { k: {'primary_key': True} for k in id_names }
119
+ index_names = conn.flavor not in _skip_index_names_flavors
120
+
121
+ if conn.flavor in _sequence_flavors:
122
+ for k, args in id_col_args.items():
123
+ args.append(sequences[k])
124
+ for k, kw in id_col_kw.items():
125
+ kw.update({'server_default': sequences[k].next_value()})
126
+
127
+ _tables = {
128
+ 'users': sqlalchemy.Table(
129
+ 'mrsm_users',
130
+ conn.metadata,
131
+ sqlalchemy.Column(
132
+ *id_col_args['user_id'],
133
+ **id_col_kw['user_id'],
134
+ ),
135
+ sqlalchemy.Column(
136
+ 'username',
137
+ sqlalchemy.String(256),
138
+ index = index_names,
139
+ nullable = False,
140
+ ),
141
+ sqlalchemy.Column('password_hash', sqlalchemy.String(1024)),
142
+ sqlalchemy.Column('email', sqlalchemy.String(256)),
143
+ sqlalchemy.Column('user_type', sqlalchemy.String(256)),
144
+ sqlalchemy.Column('attributes', params_type),
145
+ extend_existing = True,
108
146
  ),
109
- sqlalchemy.Column(
110
- 'username',
111
- sqlalchemy.String(256),
112
- index = index_names,
113
- nullable = False,
147
+ 'plugins': sqlalchemy.Table(
148
+ *([
149
+ 'mrsm_plugins',
150
+ conn.metadata,
151
+ sqlalchemy.Column(
152
+ *id_col_args['plugin_id'],
153
+ **id_col_kw['plugin_id'],
154
+ ),
155
+ sqlalchemy.Column(
156
+ 'plugin_name', sqlalchemy.String(256), index=index_names, nullable=False,
157
+ ),
158
+ sqlalchemy.Column('user_id', sqlalchemy.Integer, nullable=False),
159
+ sqlalchemy.Column('version', sqlalchemy.String(256)),
160
+ sqlalchemy.Column('attributes', params_type),
161
+ ] + ([
162
+ sqlalchemy.ForeignKeyConstraint(['user_id'], ['mrsm_users.user_id']),
163
+ ] if conn.flavor != 'duckdb' else [])),
164
+ extend_existing = True,
114
165
  ),
115
- sqlalchemy.Column('password_hash', sqlalchemy.String(1024)),
116
- sqlalchemy.Column('email', sqlalchemy.String(256)),
117
- sqlalchemy.Column('user_type', sqlalchemy.String(256)),
118
- sqlalchemy.Column('attributes', params_type),
119
- extend_existing = True,
120
- ),
121
- 'plugins': sqlalchemy.Table(
122
- *([
123
- 'mrsm_plugins',
166
+ 'temp_tables': sqlalchemy.Table(
167
+ 'mrsm_temp_tables',
124
168
  conn.metadata,
125
169
  sqlalchemy.Column(
126
- *id_col_args['plugin_id'],
127
- **id_col_kw['plugin_id'],
170
+ 'date_created',
171
+ sqlalchemy.DateTime,
172
+ index = True,
173
+ nullable = False,
128
174
  ),
129
175
  sqlalchemy.Column(
130
- 'plugin_name', sqlalchemy.String(256), index=index_names, nullable=False,
176
+ 'table',
177
+ sqlalchemy.String(256),
178
+ index = index_names,
179
+ nullable = False,
131
180
  ),
132
- sqlalchemy.Column('user_id', sqlalchemy.Integer, nullable=False),
133
- sqlalchemy.Column('version', sqlalchemy.String(256)),
134
- sqlalchemy.Column('attributes', params_type),
135
- ] + ([
136
- sqlalchemy.ForeignKeyConstraint(['user_id'], ['mrsm_users.user_id']),
137
- ] if conn.flavor != 'duckdb' else [])),
138
- extend_existing = True,
139
- ),
140
- 'temp_tables': sqlalchemy.Table(
141
- 'mrsm_temp_tables',
181
+ sqlalchemy.Column(
182
+ 'ready_to_drop',
183
+ sqlalchemy.DateTime,
184
+ index = False,
185
+ nullable = True,
186
+ ),
187
+ extend_existing = True,
188
+ ),
189
+ }
190
+
191
+ pipes_parameters_col = sqlalchemy.Column("parameters", params_type)
192
+ pipes_table_args = [
193
+ "mrsm_pipes",
142
194
  conn.metadata,
143
195
  sqlalchemy.Column(
144
- 'date_created',
145
- sqlalchemy.DateTime,
146
- index = True,
196
+ *id_col_args['pipe_id'],
197
+ **id_col_kw['pipe_id'],
198
+ ),
199
+ sqlalchemy.Column(
200
+ "connector_keys",
201
+ sqlalchemy.String(256),
202
+ index = index_names,
147
203
  nullable = False,
148
204
  ),
149
205
  sqlalchemy.Column(
150
- 'table',
206
+ "metric_key",
151
207
  sqlalchemy.String(256),
152
208
  index = index_names,
153
209
  nullable = False,
154
210
  ),
155
211
  sqlalchemy.Column(
156
- 'ready_to_drop',
157
- sqlalchemy.DateTime,
158
- index = False,
212
+ "location_key",
213
+ sqlalchemy.String(256),
214
+ index = index_names,
159
215
  nullable = True,
160
216
  ),
217
+ pipes_parameters_col,
218
+ ]
219
+ if conn.flavor in json_flavors:
220
+ pipes_table_args.append(
221
+ sqlalchemy.Index(
222
+ 'ix_mrsm_pipes_parameters_tags',
223
+ pipes_parameters_col['tags'],
224
+ postgresql_using='gin'
225
+ )
226
+ )
227
+ _tables['pipes'] = sqlalchemy.Table(
228
+ *pipes_table_args,
161
229
  extend_existing = True,
162
- ),
163
- }
164
-
165
- _tables['pipes'] = sqlalchemy.Table(
166
- "mrsm_pipes",
167
- conn.metadata,
168
- sqlalchemy.Column(
169
- *id_col_args['pipe_id'],
170
- **id_col_kw['pipe_id'],
171
- ),
172
- sqlalchemy.Column(
173
- "connector_keys",
174
- sqlalchemy.String(256),
175
- index = index_names,
176
- nullable = False,
177
- ),
178
- sqlalchemy.Column(
179
- "metric_key",
180
- sqlalchemy.String(256),
181
- index = index_names,
182
- nullable = False,
183
- ),
184
- sqlalchemy.Column(
185
- "location_key",
186
- sqlalchemy.String(256),
187
- index = index_names,
188
- nullable = True,
189
- ),
190
- sqlalchemy.Column("parameters", params_type),
191
- extend_existing = True,
192
- )
193
-
194
- ### store the table dict for reuse (per connector)
195
- connector_tables[conn] = _tables
196
- if create:
197
- create_schemas(
198
- conn,
199
- schemas = [conn.internal_schema],
200
- debug = debug,
201
230
  )
202
- create_tables(conn, tables=_tables)
203
231
 
204
- return connector_tables[conn]
232
+ ### store the table dict for reuse (per connector)
233
+ connector_tables[conn_key] = _tables
234
+
235
+ if debug:
236
+ dprint(f"Built in-memory tables for '{conn}'.")
237
+
238
+ if create:
239
+ if debug:
240
+ dprint(f"Creating tables for connector '{conn}'.")
241
+
242
+ create_schemas(
243
+ conn,
244
+ schemas = [conn.internal_schema],
245
+ debug = debug,
246
+ )
247
+ create_tables(conn, tables=_tables)
248
+
249
+ _write_create_cache(mrsm.get_connector(str(mrsm_instance)), debug=debug)
250
+
251
+ with open(pickle_path, 'wb') as f:
252
+ pickle.dump(conn.metadata, f)
253
+
254
+ connector_tables[conn_key] = _tables
255
+ return connector_tables[conn_key]
205
256
 
206
257
 
207
258
  def create_tables(
208
- conn: 'meerschaum.connectors.SQLConnector',
259
+ conn: mrsm.connectors.SQLConnector,
209
260
  tables: Optional[Dict[str, 'sqlalchemy.Table']] = None,
210
261
  ) -> bool:
211
262
  """
@@ -224,14 +275,13 @@ def create_tables(
224
275
 
225
276
 
226
277
  def create_schemas(
227
- conn: 'meerschaum.connectors.SQLConnector',
278
+ conn: mrsm.connectors.SQLConnector,
228
279
  schemas: List[str],
229
280
  debug: bool = False,
230
281
  ) -> bool:
231
282
  """
232
283
  Create the internal Meerschaum schema on the database.
233
284
  """
234
- from meerschaum._internal.static import STATIC_CONFIG
235
285
  from meerschaum.utils.packages import attempt_import
236
286
  from meerschaum.utils.sql import sql_item_name, NO_SCHEMA_FLAVORS, SKIP_IF_EXISTS_FLAVORS
237
287
  if conn.flavor in NO_SCHEMA_FLAVORS:
@@ -257,3 +307,68 @@ def create_schemas(
257
307
  except Exception as e:
258
308
  warn(f"Failed to create internal schema '{schema}':\n{e}")
259
309
  return all(successes.values())
310
+
311
+
312
+ def _check_create_cache(connector: mrsm.connectors.SQLConnector, debug: bool = False) -> bool:
313
+ """
314
+ Return `True` if the metadata cache is missing or expired.
315
+ """
316
+ import json
317
+ from datetime import datetime, timedelta
318
+ from meerschaum.utils.dtypes import get_current_timestamp
319
+
320
+ if connector.type != 'sql':
321
+ return False
322
+
323
+ path = connector.get_metadata_cache_path()
324
+ if not path.exists():
325
+ if debug:
326
+ dprint(f"Metadata cache doesn't exist for '{connector}'.")
327
+ return True
328
+
329
+ try:
330
+ with open(path, 'r', encoding='utf-8') as f:
331
+ metadata = json.load(f)
332
+ except Exception:
333
+ return True
334
+
335
+ created_str = metadata.get('created', None)
336
+ if not created_str:
337
+ return True
338
+
339
+ now = get_current_timestamp()
340
+ created = datetime.fromisoformat(created_str)
341
+
342
+ delta = now - created
343
+ threshold_minutes = (
344
+ mrsm.get_config('system', 'connectors', 'sql', 'instance', 'create_metadata_cache_minutes')
345
+ )
346
+ threshold_delta = timedelta(minutes=threshold_minutes)
347
+ if delta >= threshold_delta:
348
+ if debug:
349
+ dprint(f"Metadata cache expired for '{connector}'.")
350
+ return True
351
+
352
+ if debug:
353
+ dprint(f"Using cached metadata for '{connector}'.")
354
+
355
+ return False
356
+
357
+
358
+ def _write_create_cache(connector: mrsm.connectors.SQLConnector, debug: bool = False):
359
+ """
360
+ Write the current timestamp to the cache file.
361
+ """
362
+ if connector.type != 'sql':
363
+ return
364
+
365
+ import json
366
+ from meerschaum.utils.dtypes import get_current_timestamp, json_serialize_value
367
+
368
+ if debug:
369
+ dprint(f"Writing metadata cache for '{connector}'.")
370
+
371
+ path = connector.get_metadata_cache_path()
372
+ now = get_current_timestamp()
373
+ with open(path, 'w+', encoding='utf-8') as f:
374
+ json.dump({'created': now}, f, default=json_serialize_value)