meerschaum 3.0.0rc2__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 (41) hide show
  1. meerschaum/_internal/shell/Shell.py +5 -4
  2. meerschaum/actions/bootstrap.py +1 -1
  3. meerschaum/actions/edit.py +6 -3
  4. meerschaum/actions/start.py +1 -1
  5. meerschaum/api/dash/callbacks/__init__.py +1 -0
  6. meerschaum/api/dash/callbacks/dashboard.py +19 -18
  7. meerschaum/api/dash/callbacks/jobs.py +11 -5
  8. meerschaum/api/dash/callbacks/pipes.py +106 -5
  9. meerschaum/api/dash/callbacks/settings/__init__.py +0 -1
  10. meerschaum/api/dash/callbacks/{settings/tokens.py → tokens.py} +1 -1
  11. meerschaum/api/dash/jobs.py +1 -1
  12. meerschaum/api/dash/pages/__init__.py +2 -1
  13. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  14. meerschaum/api/dash/pages/pipes.py +4 -3
  15. meerschaum/api/dash/pages/settings/__init__.py +0 -1
  16. meerschaum/api/dash/pages/{settings/tokens.py → tokens.py} +6 -8
  17. meerschaum/api/dash/pipes.py +131 -0
  18. meerschaum/api/dash/tokens.py +26 -29
  19. meerschaum/config/_default.py +5 -4
  20. meerschaum/config/_paths.py +1 -0
  21. meerschaum/config/_version.py +1 -1
  22. meerschaum/connectors/instance/_tokens.py +6 -2
  23. meerschaum/connectors/sql/_SQLConnector.py +14 -0
  24. meerschaum/connectors/sql/_pipes.py +57 -22
  25. meerschaum/connectors/sql/tables/__init__.py +237 -122
  26. meerschaum/core/Pipe/_attributes.py +5 -2
  27. meerschaum/core/Token/_Token.py +1 -1
  28. meerschaum/plugins/bootstrap.py +508 -3
  29. meerschaum/utils/_get_pipes.py +1 -1
  30. meerschaum/utils/dataframe.py +8 -2
  31. meerschaum/utils/dtypes/__init__.py +2 -3
  32. meerschaum/utils/dtypes/sql.py +11 -11
  33. meerschaum/utils/sql.py +1 -1
  34. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/METADATA +1 -1
  35. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/RECORD +41 -41
  36. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/WHEEL +0 -0
  37. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/entry_points.txt +0 -0
  38. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/licenses/LICENSE +0 -0
  39. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/licenses/NOTICE +0 -0
  40. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/top_level.txt +0 -0
  41. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/zip-safe +0 -0
@@ -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)
@@ -286,7 +286,6 @@ def get_dtypes(
286
286
  """
287
287
  If defined, return the `dtypes` dictionary defined in `meerschaum.Pipe.parameters`.
288
288
 
289
-
290
289
  Parameters
291
290
  ----------
292
291
  infer: bool, default True
@@ -310,7 +309,11 @@ def get_dtypes(
310
309
  dprint(f"Configured dtypes for {self}:")
311
310
  mrsm.pprint(configured_dtypes)
312
311
 
313
- remote_dtypes = self.infer_dtypes(persist=False, refresh=refresh, debug=debug)
312
+ remote_dtypes = (
313
+ self.infer_dtypes(persist=False, refresh=refresh, debug=debug)
314
+ if infer
315
+ else {}
316
+ )
314
317
  patched_dtypes = apply_patch_to_config((remote_dtypes or {}), (configured_dtypes or {}))
315
318
 
316
319
  dt_col = parameters.get('columns', {}).get('datetime', None)
@@ -120,7 +120,7 @@ class Token:
120
120
  Register the new token to the configured instance.
121
121
  """
122
122
  if self.user is None:
123
- raise ValueError("Cannot register a token with a user.")
123
+ return False, "Cannot register a token without a user."
124
124
 
125
125
  return self.instance_connector.register_token(self, debug=debug)
126
126