meerschaum 2.9.5__py3-none-any.whl → 3.0.0__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 (200) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +198 -118
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +382 -95
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/js/terminado.js +3 -0
  68. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  69. meerschaum/api/resources/templates/termpage.html +13 -0
  70. meerschaum/api/routes/__init__.py +1 -0
  71. meerschaum/api/routes/_actions.py +3 -4
  72. meerschaum/api/routes/_connectors.py +3 -7
  73. meerschaum/api/routes/_jobs.py +26 -35
  74. meerschaum/api/routes/_login.py +120 -15
  75. meerschaum/api/routes/_misc.py +5 -10
  76. meerschaum/api/routes/_pipes.py +178 -143
  77. meerschaum/api/routes/_plugins.py +38 -28
  78. meerschaum/api/routes/_tokens.py +236 -0
  79. meerschaum/api/routes/_users.py +47 -35
  80. meerschaum/api/routes/_version.py +3 -3
  81. meerschaum/api/routes/_webterm.py +3 -3
  82. meerschaum/config/__init__.py +100 -30
  83. meerschaum/config/_default.py +132 -64
  84. meerschaum/config/_edit.py +38 -32
  85. meerschaum/config/_formatting.py +2 -0
  86. meerschaum/config/_patch.py +10 -8
  87. meerschaum/config/_paths.py +133 -13
  88. meerschaum/config/_read_config.py +87 -36
  89. meerschaum/config/_sync.py +6 -3
  90. meerschaum/config/_version.py +1 -1
  91. meerschaum/config/environment.py +262 -0
  92. meerschaum/config/stack/__init__.py +37 -15
  93. meerschaum/config/static.py +18 -0
  94. meerschaum/connectors/_Connector.py +11 -6
  95. meerschaum/connectors/__init__.py +41 -22
  96. meerschaum/connectors/api/_APIConnector.py +34 -6
  97. meerschaum/connectors/api/_actions.py +2 -2
  98. meerschaum/connectors/api/_jobs.py +12 -1
  99. meerschaum/connectors/api/_login.py +33 -7
  100. meerschaum/connectors/api/_misc.py +2 -2
  101. meerschaum/connectors/api/_pipes.py +23 -32
  102. meerschaum/connectors/api/_plugins.py +2 -2
  103. meerschaum/connectors/api/_request.py +1 -1
  104. meerschaum/connectors/api/_tokens.py +146 -0
  105. meerschaum/connectors/api/_users.py +70 -58
  106. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  107. meerschaum/connectors/instance/__init__.py +10 -0
  108. meerschaum/connectors/instance/_pipes.py +442 -0
  109. meerschaum/connectors/instance/_plugins.py +159 -0
  110. meerschaum/connectors/instance/_tokens.py +317 -0
  111. meerschaum/connectors/instance/_users.py +188 -0
  112. meerschaum/connectors/parse.py +5 -2
  113. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  114. meerschaum/connectors/sql/_cli.py +12 -11
  115. meerschaum/connectors/sql/_create_engine.py +12 -168
  116. meerschaum/connectors/sql/_fetch.py +2 -18
  117. meerschaum/connectors/sql/_pipes.py +295 -278
  118. meerschaum/connectors/sql/_plugins.py +29 -0
  119. meerschaum/connectors/sql/_sql.py +46 -21
  120. meerschaum/connectors/sql/_users.py +36 -2
  121. meerschaum/connectors/sql/tables/__init__.py +254 -122
  122. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  123. meerschaum/connectors/valkey/_pipes.py +60 -31
  124. meerschaum/connectors/valkey/_plugins.py +2 -26
  125. meerschaum/core/Pipe/__init__.py +115 -85
  126. meerschaum/core/Pipe/_attributes.py +425 -124
  127. meerschaum/core/Pipe/_bootstrap.py +54 -24
  128. meerschaum/core/Pipe/_cache.py +555 -0
  129. meerschaum/core/Pipe/_clear.py +0 -11
  130. meerschaum/core/Pipe/_data.py +96 -68
  131. meerschaum/core/Pipe/_deduplicate.py +0 -13
  132. meerschaum/core/Pipe/_delete.py +12 -21
  133. meerschaum/core/Pipe/_drop.py +11 -23
  134. meerschaum/core/Pipe/_dtypes.py +49 -19
  135. meerschaum/core/Pipe/_edit.py +14 -4
  136. meerschaum/core/Pipe/_fetch.py +1 -1
  137. meerschaum/core/Pipe/_index.py +8 -14
  138. meerschaum/core/Pipe/_show.py +5 -5
  139. meerschaum/core/Pipe/_sync.py +123 -204
  140. meerschaum/core/Pipe/_verify.py +4 -4
  141. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  142. meerschaum/core/Plugin/__init__.py +1 -1
  143. meerschaum/core/Token/_Token.py +220 -0
  144. meerschaum/core/Token/__init__.py +12 -0
  145. meerschaum/core/User/_User.py +35 -10
  146. meerschaum/core/User/__init__.py +9 -1
  147. meerschaum/core/__init__.py +1 -0
  148. meerschaum/jobs/_Executor.py +88 -4
  149. meerschaum/jobs/_Job.py +149 -38
  150. meerschaum/jobs/__init__.py +3 -2
  151. meerschaum/jobs/systemd.py +8 -3
  152. meerschaum/models/__init__.py +35 -0
  153. meerschaum/models/pipes.py +247 -0
  154. meerschaum/models/tokens.py +38 -0
  155. meerschaum/models/users.py +26 -0
  156. meerschaum/plugins/__init__.py +301 -88
  157. meerschaum/plugins/bootstrap.py +510 -4
  158. meerschaum/utils/_get_pipes.py +97 -30
  159. meerschaum/utils/daemon/Daemon.py +199 -43
  160. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  161. meerschaum/utils/daemon/RotatingFile.py +63 -36
  162. meerschaum/utils/daemon/StdinFile.py +53 -13
  163. meerschaum/utils/daemon/__init__.py +47 -6
  164. meerschaum/utils/daemon/_names.py +6 -3
  165. meerschaum/utils/dataframe.py +479 -81
  166. meerschaum/utils/debug.py +49 -19
  167. meerschaum/utils/dtypes/__init__.py +476 -34
  168. meerschaum/utils/dtypes/sql.py +369 -29
  169. meerschaum/utils/formatting/__init__.py +5 -2
  170. meerschaum/utils/formatting/_jobs.py +1 -1
  171. meerschaum/utils/formatting/_pipes.py +52 -50
  172. meerschaum/utils/formatting/_pprint.py +1 -0
  173. meerschaum/utils/formatting/_shell.py +44 -18
  174. meerschaum/utils/misc.py +268 -186
  175. meerschaum/utils/packages/__init__.py +25 -40
  176. meerschaum/utils/packages/_packages.py +42 -34
  177. meerschaum/utils/pipes.py +213 -0
  178. meerschaum/utils/process.py +2 -2
  179. meerschaum/utils/prompt.py +175 -144
  180. meerschaum/utils/schedule.py +2 -1
  181. meerschaum/utils/sql.py +134 -47
  182. meerschaum/utils/threading.py +42 -0
  183. meerschaum/utils/typing.py +1 -4
  184. meerschaum/utils/venv/_Venv.py +2 -2
  185. meerschaum/utils/venv/__init__.py +7 -7
  186. meerschaum/utils/warnings.py +19 -13
  187. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  188. meerschaum-3.0.0.dist-info/RECORD +289 -0
  189. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  190. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  191. meerschaum/api/models/_interfaces.py +0 -15
  192. meerschaum/api/models/_locations.py +0 -15
  193. meerschaum/api/models/_metrics.py +0 -15
  194. meerschaum/config/_environment.py +0 -145
  195. meerschaum/config/static/__init__.py +0 -186
  196. meerschaum-2.9.5.dist-info/RECORD +0 -263
  197. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  198. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  199. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  200. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.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.config.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)
@@ -10,19 +10,18 @@ import json
10
10
  from datetime import datetime, timezone
11
11
 
12
12
  import meerschaum as mrsm
13
- from meerschaum.connectors import Connector, make_connector
13
+ from meerschaum.connectors import InstanceConnector, make_connector
14
14
  from meerschaum.utils.typing import List, Dict, Any, Optional, Union
15
15
  from meerschaum.utils.warnings import dprint
16
16
 
17
17
 
18
18
  @make_connector
19
- class ValkeyConnector(Connector):
19
+ class ValkeyConnector(InstanceConnector):
20
20
  """
21
21
  Manage a Valkey instance.
22
22
 
23
23
  Build a `ValkeyConnector` from connection attributes or a URI string.
24
24
  """
25
- IS_INSTANCE: bool = True
26
25
  REQUIRED_ATTRIBUTES: List[str] = ['host']
27
26
  OPTIONAL_ATTRIBUTES: List[str] = [
28
27
  'port', 'username', 'password', 'db', 'socket_timeout',
@@ -80,7 +79,6 @@ class ValkeyConnector(Connector):
80
79
  get_plugin_user_id,
81
80
  get_plugin_username,
82
81
  get_plugin_attributes,
83
- get_plugins,
84
82
  delete_plugin,
85
83
  )
86
84
 
@@ -142,13 +140,13 @@ class ValkeyConnector(Connector):
142
140
 
143
141
  return uri
144
142
 
145
- def set(self, key: str, value: Any, **kwargs: Any) -> None:
143
+ def set(self, key: str, value: Any, **kwargs: Any) -> bool:
146
144
  """
147
145
  Set the `key` to `value`.
148
146
  """
149
147
  return self.client.set(key, value, **kwargs)
150
148
 
151
- def get(self, key: str) -> Union[str, None]:
149
+ def get(self, key: str, decode: bool = True) -> Union[str, None]:
152
150
  """
153
151
  Get the value for `key`.
154
152
  """
@@ -156,7 +154,7 @@ class ValkeyConnector(Connector):
156
154
  if val is None:
157
155
  return None
158
156
 
159
- return val.decode('utf-8')
157
+ return val.decode('utf-8') if decode else val
160
158
 
161
159
  def test_connection(self) -> bool:
162
160
  """