meerschaum 3.0.0rc2__py3-none-any.whl → 3.0.0rc4__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 (47) 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/_events.py +5 -0
  6. meerschaum/api/dash/callbacks/__init__.py +1 -0
  7. meerschaum/api/dash/callbacks/dashboard.py +93 -115
  8. meerschaum/api/dash/callbacks/jobs.py +11 -5
  9. meerschaum/api/dash/callbacks/pipes.py +194 -14
  10. meerschaum/api/dash/callbacks/settings/__init__.py +0 -1
  11. meerschaum/api/dash/callbacks/{settings/tokens.py → tokens.py} +3 -2
  12. meerschaum/api/dash/components.py +6 -7
  13. meerschaum/api/dash/jobs.py +1 -1
  14. meerschaum/api/dash/keys.py +17 -1
  15. meerschaum/api/dash/pages/__init__.py +2 -1
  16. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  17. meerschaum/api/dash/pages/pipes.py +16 -5
  18. meerschaum/api/dash/pages/settings/__init__.py +0 -1
  19. meerschaum/api/dash/pages/{settings/tokens.py → tokens.py} +6 -8
  20. meerschaum/api/dash/pipes.py +219 -3
  21. meerschaum/api/dash/tokens.py +27 -30
  22. meerschaum/config/_default.py +5 -4
  23. meerschaum/config/_paths.py +1 -0
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/connectors/instance/_tokens.py +6 -2
  26. meerschaum/connectors/sql/_SQLConnector.py +14 -0
  27. meerschaum/connectors/sql/_pipes.py +63 -23
  28. meerschaum/connectors/sql/tables/__init__.py +254 -122
  29. meerschaum/core/Pipe/__init__.py +17 -1
  30. meerschaum/core/Pipe/_attributes.py +5 -2
  31. meerschaum/core/Token/_Token.py +1 -1
  32. meerschaum/plugins/bootstrap.py +508 -3
  33. meerschaum/utils/_get_pipes.py +31 -5
  34. meerschaum/utils/dataframe.py +8 -2
  35. meerschaum/utils/dtypes/__init__.py +2 -3
  36. meerschaum/utils/dtypes/sql.py +11 -11
  37. meerschaum/utils/formatting/_pprint.py +1 -0
  38. meerschaum/utils/pipes.py +6 -2
  39. meerschaum/utils/sql.py +1 -1
  40. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/METADATA +1 -1
  41. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/RECORD +47 -47
  42. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/WHEEL +0 -0
  43. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/entry_points.txt +0 -0
  44. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/licenses/LICENSE +0 -0
  45. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/licenses/NOTICE +0 -0
  46. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/top_level.txt +0 -0
  47. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/zip-safe +0 -0
@@ -7,19 +7,25 @@ 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
+ refresh: bool = False,
28
+ debug: bool = False,
23
29
  ) -> Union[Dict[str, 'sqlalchemy.Table'], bool]:
24
30
  """
25
31
  Create tables on the database and return the `sqlalchemy` tables.
@@ -29,10 +35,13 @@ def get_tables(
29
35
  mrsm_instance: Optional[Union[str, InstanceConnector]], default None
30
36
  The connector on which the tables reside.
31
37
 
32
- create: bool, default True:
38
+ create: Optional[bool], default None
33
39
  If `True`, create the tables if they don't exist.
34
40
 
35
- debug: Optional[bool], default None:
41
+ refresh: bool, default False
42
+ If `True`, invalidate and rebuild any cache.
43
+
44
+ debug: bool, default False
36
45
  Verbosity Toggle.
37
46
 
38
47
  Returns
@@ -42,7 +51,6 @@ def get_tables(
42
51
 
43
52
  """
44
53
  from meerschaum.utils.debug import dprint
45
- from meerschaum.utils.formatting import pprint
46
54
  from meerschaum.connectors.parse import parse_instance_keys
47
55
  from meerschaum.utils.packages import attempt_import
48
56
  from meerschaum.utils.sql import json_flavors
@@ -54,7 +62,7 @@ def get_tables(
54
62
  lazy=False,
55
63
  )
56
64
  if not sqlalchemy:
57
- error(f"Failed to import sqlalchemy. Is sqlalchemy installed?")
65
+ error("Failed to import sqlalchemy. Is sqlalchemy installed?")
58
66
 
59
67
  if mrsm_instance is None:
60
68
  conn = get_connector(debug=debug)
@@ -63,149 +71,209 @@ def get_tables(
63
71
  else: ### NOTE: mrsm_instance MUST BE a SQL Connector for this to work!
64
72
  conn = mrsm_instance
65
73
 
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 {}
74
+ cache_expired = refresh or (
75
+ (
76
+ _check_create_cache(conn, debug=debug)
77
+ if conn.flavor != 'sqlite'
78
+ else True
79
+ )
80
+ if conn.type == 'sql'
81
+ else False
82
+ )
83
+ create = create or cache_expired
72
84
 
73
85
  ### Skip if the connector is not a SQL connector.
74
86
  if getattr(conn, 'type', None) != 'sql':
75
87
  return {}
76
88
 
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'],
89
+ conn_key = str(conn)
90
+
91
+ if refresh:
92
+ _ = connector_tables.pop(conn_key, None)
93
+
94
+ if conn_key in connector_tables:
95
+ return connector_tables[conn_key]
96
+
97
+ fasteners = attempt_import('fasteners')
98
+ pickle_path = conn.get_metadata_cache_path(kind='pkl')
99
+ lock_path = pickle_path.with_suffix('.lock')
100
+ lock = fasteners.InterProcessLock(lock_path)
101
+
102
+ with lock:
103
+ if not cache_expired and pickle_path.exists():
104
+ try:
105
+ with open(pickle_path, 'rb') as f:
106
+ metadata = pickle.load(f)
107
+ metadata.bind = conn.engine
108
+ tables = {tbl.name.replace('mrsm_', ''): tbl for tbl in metadata.tables.values()}
109
+ connector_tables[conn_key] = tables
110
+ return tables
111
+ except Exception as e:
112
+ warn(f"Failed to load metadata from cache, rebuilding: {e}")
113
+
114
+ if conn_key not in _tables_locks:
115
+ _tables_locks[conn_key] = threading.Lock()
116
+
117
+ with _tables_locks[conn_key]:
118
+ if conn_key not in connector_tables:
119
+ if debug:
120
+ dprint(f"Building in-memory instance tables for '{conn}'.")
121
+
122
+ id_type = sqlalchemy.Integer
123
+ if conn.flavor in json_flavors:
124
+ from sqlalchemy.dialects.postgresql import JSONB
125
+ params_type = JSONB
126
+ else:
127
+ params_type = sqlalchemy.types.Text
128
+ id_names = ('user_id', 'plugin_id', 'pipe_id')
129
+ sequences = {
130
+ k: sqlalchemy.Sequence(k + '_seq')
131
+ for k in id_names
132
+ }
133
+ id_col_args = { k: [k, id_type] for k in id_names }
134
+ id_col_kw = { k: {'primary_key': True} for k in id_names }
135
+ index_names = conn.flavor not in _skip_index_names_flavors
136
+
137
+ if conn.flavor in _sequence_flavors:
138
+ for k, args in id_col_args.items():
139
+ args.append(sequences[k])
140
+ for k, kw in id_col_kw.items():
141
+ kw.update({'server_default': sequences[k].next_value()})
142
+
143
+ _tables = {
144
+ 'users': sqlalchemy.Table(
145
+ 'mrsm_users',
146
+ conn.metadata,
147
+ sqlalchemy.Column(
148
+ *id_col_args['user_id'],
149
+ **id_col_kw['user_id'],
150
+ ),
151
+ sqlalchemy.Column(
152
+ 'username',
153
+ sqlalchemy.String(256),
154
+ index = index_names,
155
+ nullable = False,
156
+ ),
157
+ sqlalchemy.Column('password_hash', sqlalchemy.String(1024)),
158
+ sqlalchemy.Column('email', sqlalchemy.String(256)),
159
+ sqlalchemy.Column('user_type', sqlalchemy.String(256)),
160
+ sqlalchemy.Column('attributes', params_type),
161
+ extend_existing = True,
108
162
  ),
109
- sqlalchemy.Column(
110
- 'username',
111
- sqlalchemy.String(256),
112
- index = index_names,
113
- nullable = False,
163
+ 'plugins': sqlalchemy.Table(
164
+ *([
165
+ 'mrsm_plugins',
166
+ conn.metadata,
167
+ sqlalchemy.Column(
168
+ *id_col_args['plugin_id'],
169
+ **id_col_kw['plugin_id'],
170
+ ),
171
+ sqlalchemy.Column(
172
+ 'plugin_name', sqlalchemy.String(256), index=index_names, nullable=False,
173
+ ),
174
+ sqlalchemy.Column('user_id', sqlalchemy.Integer, nullable=False),
175
+ sqlalchemy.Column('version', sqlalchemy.String(256)),
176
+ sqlalchemy.Column('attributes', params_type),
177
+ ] + ([
178
+ sqlalchemy.ForeignKeyConstraint(['user_id'], ['mrsm_users.user_id']),
179
+ ] if conn.flavor != 'duckdb' else [])),
180
+ extend_existing = True,
114
181
  ),
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',
182
+ 'temp_tables': sqlalchemy.Table(
183
+ 'mrsm_temp_tables',
124
184
  conn.metadata,
125
185
  sqlalchemy.Column(
126
- *id_col_args['plugin_id'],
127
- **id_col_kw['plugin_id'],
186
+ 'date_created',
187
+ sqlalchemy.DateTime,
188
+ index = True,
189
+ nullable = False,
128
190
  ),
129
191
  sqlalchemy.Column(
130
- 'plugin_name', sqlalchemy.String(256), index=index_names, nullable=False,
192
+ 'table',
193
+ sqlalchemy.String(256),
194
+ index = index_names,
195
+ nullable = False,
131
196
  ),
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',
197
+ sqlalchemy.Column(
198
+ 'ready_to_drop',
199
+ sqlalchemy.DateTime,
200
+ index = False,
201
+ nullable = True,
202
+ ),
203
+ extend_existing = True,
204
+ ),
205
+ }
206
+
207
+ pipes_parameters_col = sqlalchemy.Column("parameters", params_type)
208
+ pipes_table_args = [
209
+ "mrsm_pipes",
142
210
  conn.metadata,
143
211
  sqlalchemy.Column(
144
- 'date_created',
145
- sqlalchemy.DateTime,
146
- index = True,
212
+ *id_col_args['pipe_id'],
213
+ **id_col_kw['pipe_id'],
214
+ ),
215
+ sqlalchemy.Column(
216
+ "connector_keys",
217
+ sqlalchemy.String(256),
218
+ index = index_names,
147
219
  nullable = False,
148
220
  ),
149
221
  sqlalchemy.Column(
150
- 'table',
222
+ "metric_key",
151
223
  sqlalchemy.String(256),
152
224
  index = index_names,
153
225
  nullable = False,
154
226
  ),
155
227
  sqlalchemy.Column(
156
- 'ready_to_drop',
157
- sqlalchemy.DateTime,
158
- index = False,
228
+ "location_key",
229
+ sqlalchemy.String(256),
230
+ index = index_names,
159
231
  nullable = True,
160
232
  ),
233
+ pipes_parameters_col,
234
+ ]
235
+ if conn.flavor in json_flavors:
236
+ pipes_table_args.append(
237
+ sqlalchemy.Index(
238
+ 'ix_mrsm_pipes_parameters_tags',
239
+ pipes_parameters_col['tags'],
240
+ postgresql_using='gin'
241
+ )
242
+ )
243
+ _tables['pipes'] = sqlalchemy.Table(
244
+ *pipes_table_args,
161
245
  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
246
  )
202
- create_tables(conn, tables=_tables)
203
247
 
204
- return connector_tables[conn]
248
+ ### store the table dict for reuse (per connector)
249
+ connector_tables[conn_key] = _tables
250
+
251
+ if debug:
252
+ dprint(f"Built in-memory tables for '{conn}'.")
253
+
254
+ if create:
255
+ if debug:
256
+ dprint(f"Creating tables for connector '{conn}'.")
257
+
258
+ create_schemas(
259
+ conn,
260
+ schemas = [conn.internal_schema],
261
+ debug = debug,
262
+ )
263
+ create_tables(conn, tables=_tables)
264
+
265
+ _write_create_cache(mrsm.get_connector(str(mrsm_instance)), debug=debug)
266
+
267
+ if conn.flavor != 'sqlite':
268
+ with open(pickle_path, 'wb') as f:
269
+ pickle.dump(conn.metadata, f)
270
+
271
+ connector_tables[conn_key] = _tables
272
+ return connector_tables[conn_key]
205
273
 
206
274
 
207
275
  def create_tables(
208
- conn: 'meerschaum.connectors.SQLConnector',
276
+ conn: mrsm.connectors.SQLConnector,
209
277
  tables: Optional[Dict[str, 'sqlalchemy.Table']] = None,
210
278
  ) -> bool:
211
279
  """
@@ -224,14 +292,13 @@ def create_tables(
224
292
 
225
293
 
226
294
  def create_schemas(
227
- conn: 'meerschaum.connectors.SQLConnector',
295
+ conn: mrsm.connectors.SQLConnector,
228
296
  schemas: List[str],
229
297
  debug: bool = False,
230
298
  ) -> bool:
231
299
  """
232
300
  Create the internal Meerschaum schema on the database.
233
301
  """
234
- from meerschaum._internal.static import STATIC_CONFIG
235
302
  from meerschaum.utils.packages import attempt_import
236
303
  from meerschaum.utils.sql import sql_item_name, NO_SCHEMA_FLAVORS, SKIP_IF_EXISTS_FLAVORS
237
304
  if conn.flavor in NO_SCHEMA_FLAVORS:
@@ -257,3 +324,68 @@ def create_schemas(
257
324
  except Exception as e:
258
325
  warn(f"Failed to create internal schema '{schema}':\n{e}")
259
326
  return all(successes.values())
327
+
328
+
329
+ def _check_create_cache(connector: mrsm.connectors.SQLConnector, debug: bool = False) -> bool:
330
+ """
331
+ Return `True` if the metadata cache is missing or expired.
332
+ """
333
+ import json
334
+ from datetime import datetime, timedelta
335
+ from meerschaum.utils.dtypes import get_current_timestamp
336
+
337
+ if connector.type != 'sql':
338
+ return False
339
+
340
+ path = connector.get_metadata_cache_path()
341
+ if not path.exists():
342
+ if debug:
343
+ dprint(f"Metadata cache doesn't exist for '{connector}'.")
344
+ return True
345
+
346
+ try:
347
+ with open(path, 'r', encoding='utf-8') as f:
348
+ metadata = json.load(f)
349
+ except Exception:
350
+ return True
351
+
352
+ created_str = metadata.get('created', None)
353
+ if not created_str:
354
+ return True
355
+
356
+ now = get_current_timestamp()
357
+ created = datetime.fromisoformat(created_str)
358
+
359
+ delta = now - created
360
+ threshold_minutes = (
361
+ mrsm.get_config('system', 'connectors', 'sql', 'instance', 'create_metadata_cache_minutes')
362
+ )
363
+ threshold_delta = timedelta(minutes=threshold_minutes)
364
+ if delta >= threshold_delta:
365
+ if debug:
366
+ dprint(f"Metadata cache expired for '{connector}'.")
367
+ return True
368
+
369
+ if debug:
370
+ dprint(f"Using cached metadata for '{connector}'.")
371
+
372
+ return False
373
+
374
+
375
+ def _write_create_cache(connector: mrsm.connectors.SQLConnector, debug: bool = False):
376
+ """
377
+ Write the current timestamp to the cache file.
378
+ """
379
+ if connector.type != 'sql':
380
+ return
381
+
382
+ import json
383
+ from meerschaum.utils.dtypes import get_current_timestamp, json_serialize_value
384
+
385
+ if debug:
386
+ dprint(f"Writing metadata cache for '{connector}'.")
387
+
388
+ path = connector.get_metadata_cache_path()
389
+ now = get_current_timestamp()
390
+ with open(path, 'w+', encoding='utf-8') as f:
391
+ json.dump({'created': now}, f, default=json_serialize_value)
@@ -562,7 +562,7 @@ class Pipe:
562
562
  'connector_keys': self.connector_keys,
563
563
  'metric_key': self.metric_key,
564
564
  'location_key': self.location_key,
565
- 'parameters': self.parameters,
565
+ 'parameters': self._attributes.get('parameters', None),
566
566
  'instance_keys': self.instance_keys,
567
567
  }
568
568
 
@@ -598,3 +598,19 @@ class Pipe:
598
598
  if aliased_key is not None:
599
599
  key = aliased_key
600
600
  return getattr(self, key, None)
601
+
602
+ def __copy__(self):
603
+ """
604
+ Return a shallow copy of the current pipe.
605
+ """
606
+ return mrsm.Pipe(
607
+ self.connector_keys, self.metric_key, self.location_key,
608
+ instance=self.instance_keys,
609
+ parameters=self._attributes.get('parameters', None),
610
+ )
611
+
612
+ def __deepcopy__(self, memo):
613
+ """
614
+ Return a deep copy of the current pipe.
615
+ """
616
+ return self.__copy__()
@@ -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