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.
- meerschaum/_internal/shell/Shell.py +5 -4
- meerschaum/actions/bootstrap.py +1 -1
- meerschaum/actions/edit.py +6 -3
- meerschaum/actions/start.py +1 -1
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/dashboard.py +19 -18
- meerschaum/api/dash/callbacks/jobs.py +11 -5
- meerschaum/api/dash/callbacks/pipes.py +106 -5
- meerschaum/api/dash/callbacks/settings/__init__.py +0 -1
- meerschaum/api/dash/callbacks/{settings/tokens.py → tokens.py} +1 -1
- meerschaum/api/dash/jobs.py +1 -1
- meerschaum/api/dash/pages/__init__.py +2 -1
- meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
- meerschaum/api/dash/pages/pipes.py +4 -3
- meerschaum/api/dash/pages/settings/__init__.py +0 -1
- meerschaum/api/dash/pages/{settings/tokens.py → tokens.py} +6 -8
- meerschaum/api/dash/pipes.py +131 -0
- meerschaum/api/dash/tokens.py +26 -29
- meerschaum/config/_default.py +5 -4
- meerschaum/config/_paths.py +1 -0
- meerschaum/config/_version.py +1 -1
- meerschaum/connectors/instance/_tokens.py +6 -2
- meerschaum/connectors/sql/_SQLConnector.py +14 -0
- meerschaum/connectors/sql/_pipes.py +57 -22
- meerschaum/connectors/sql/tables/__init__.py +237 -122
- meerschaum/core/Pipe/_attributes.py +5 -2
- meerschaum/core/Token/_Token.py +1 -1
- meerschaum/plugins/bootstrap.py +508 -3
- meerschaum/utils/_get_pipes.py +1 -1
- meerschaum/utils/dataframe.py +8 -2
- meerschaum/utils/dtypes/__init__.py +2 -3
- meerschaum/utils/dtypes/sql.py +11 -11
- meerschaum/utils/sql.py +1 -1
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/METADATA +1 -1
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/RECORD +41 -41
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/WHEEL +0 -0
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/entry_points.txt +0 -0
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/licenses/NOTICE +0 -0
- {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc3.dist-info}/top_level.txt +0 -0
- {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 =
|
22
|
-
debug:
|
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
|
37
|
+
create: Optional[bool], default None
|
33
38
|
If `True`, create the tables if they don't exist.
|
34
39
|
|
35
|
-
debug:
|
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(
|
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
|
-
|
67
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
conn.
|
105
|
-
|
106
|
-
|
107
|
-
|
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.
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
116
|
-
|
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
|
-
|
127
|
-
|
170
|
+
'date_created',
|
171
|
+
sqlalchemy.DateTime,
|
172
|
+
index = True,
|
173
|
+
nullable = False,
|
128
174
|
),
|
129
175
|
sqlalchemy.Column(
|
130
|
-
'
|
176
|
+
'table',
|
177
|
+
sqlalchemy.String(256),
|
178
|
+
index = index_names,
|
179
|
+
nullable = False,
|
131
180
|
),
|
132
|
-
sqlalchemy.Column(
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
'
|
145
|
-
|
146
|
-
|
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
|
-
|
206
|
+
"metric_key",
|
151
207
|
sqlalchemy.String(256),
|
152
208
|
index = index_names,
|
153
209
|
nullable = False,
|
154
210
|
),
|
155
211
|
sqlalchemy.Column(
|
156
|
-
|
157
|
-
sqlalchemy.
|
158
|
-
index =
|
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
|
-
|
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:
|
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:
|
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 =
|
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)
|
meerschaum/core/Token/_Token.py
CHANGED
@@ -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
|
-
|
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
|
|