meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc2__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 (158) 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 +19 -2
  5. meerschaum/_internal/docs/index.py +49 -2
  6. meerschaum/_internal/entry.py +6 -6
  7. meerschaum/_internal/shell/Shell.py +1 -1
  8. meerschaum/_internal/static.py +356 -0
  9. meerschaum/actions/api.py +12 -2
  10. meerschaum/actions/bootstrap.py +7 -7
  11. meerschaum/actions/edit.py +142 -18
  12. meerschaum/actions/register.py +137 -6
  13. meerschaum/actions/show.py +117 -29
  14. meerschaum/actions/stop.py +4 -1
  15. meerschaum/actions/sync.py +1 -1
  16. meerschaum/actions/tag.py +9 -8
  17. meerschaum/actions/verify.py +5 -8
  18. meerschaum/api/__init__.py +11 -3
  19. meerschaum/api/_events.py +39 -2
  20. meerschaum/api/_oauth2.py +118 -8
  21. meerschaum/api/_tokens.py +102 -0
  22. meerschaum/api/dash/__init__.py +0 -3
  23. meerschaum/api/dash/callbacks/custom.py +2 -2
  24. meerschaum/api/dash/callbacks/dashboard.py +103 -19
  25. meerschaum/api/dash/callbacks/plugins.py +0 -1
  26. meerschaum/api/dash/callbacks/register.py +1 -1
  27. meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
  28. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  29. meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
  30. meerschaum/api/dash/components.py +30 -8
  31. meerschaum/api/dash/keys.py +19 -93
  32. meerschaum/api/dash/pages/dashboard.py +1 -20
  33. meerschaum/api/dash/pages/settings/__init__.py +1 -0
  34. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  35. meerschaum/api/dash/pages/settings/tokens.py +55 -0
  36. meerschaum/api/dash/pipes.py +94 -59
  37. meerschaum/api/dash/sessions.py +12 -0
  38. meerschaum/api/dash/tokens.py +606 -0
  39. meerschaum/api/dash/websockets.py +1 -1
  40. meerschaum/api/dash/webterm.py +4 -0
  41. meerschaum/api/models/__init__.py +23 -3
  42. meerschaum/api/models/_actions.py +22 -0
  43. meerschaum/api/models/_pipes.py +85 -7
  44. meerschaum/api/models/_tokens.py +81 -0
  45. meerschaum/api/resources/templates/termpage.html +12 -0
  46. meerschaum/api/routes/__init__.py +1 -0
  47. meerschaum/api/routes/_actions.py +3 -4
  48. meerschaum/api/routes/_connectors.py +3 -7
  49. meerschaum/api/routes/_jobs.py +14 -35
  50. meerschaum/api/routes/_login.py +49 -12
  51. meerschaum/api/routes/_misc.py +5 -10
  52. meerschaum/api/routes/_pipes.py +173 -140
  53. meerschaum/api/routes/_plugins.py +38 -28
  54. meerschaum/api/routes/_tokens.py +236 -0
  55. meerschaum/api/routes/_users.py +47 -35
  56. meerschaum/api/routes/_version.py +3 -3
  57. meerschaum/config/__init__.py +43 -20
  58. meerschaum/config/_default.py +43 -6
  59. meerschaum/config/_edit.py +28 -24
  60. meerschaum/config/_environment.py +1 -1
  61. meerschaum/config/_patch.py +6 -6
  62. meerschaum/config/_paths.py +5 -1
  63. meerschaum/config/_read_config.py +65 -34
  64. meerschaum/config/_sync.py +6 -3
  65. meerschaum/config/_version.py +1 -1
  66. meerschaum/config/stack/__init__.py +31 -11
  67. meerschaum/config/static.py +18 -0
  68. meerschaum/connectors/_Connector.py +10 -4
  69. meerschaum/connectors/__init__.py +4 -20
  70. meerschaum/connectors/api/_APIConnector.py +34 -6
  71. meerschaum/connectors/api/_actions.py +2 -2
  72. meerschaum/connectors/api/_jobs.py +1 -1
  73. meerschaum/connectors/api/_login.py +33 -7
  74. meerschaum/connectors/api/_misc.py +2 -2
  75. meerschaum/connectors/api/_pipes.py +16 -31
  76. meerschaum/connectors/api/_plugins.py +2 -2
  77. meerschaum/connectors/api/_request.py +1 -1
  78. meerschaum/connectors/api/_tokens.py +146 -0
  79. meerschaum/connectors/api/_users.py +70 -58
  80. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  81. meerschaum/connectors/instance/__init__.py +10 -0
  82. meerschaum/connectors/instance/_pipes.py +442 -0
  83. meerschaum/connectors/instance/_plugins.py +151 -0
  84. meerschaum/connectors/instance/_tokens.py +296 -0
  85. meerschaum/connectors/instance/_users.py +181 -0
  86. meerschaum/connectors/parse.py +4 -1
  87. meerschaum/connectors/sql/_SQLConnector.py +8 -5
  88. meerschaum/connectors/sql/_cli.py +12 -11
  89. meerschaum/connectors/sql/_create_engine.py +9 -168
  90. meerschaum/connectors/sql/_fetch.py +2 -18
  91. meerschaum/connectors/sql/_pipes.py +156 -190
  92. meerschaum/connectors/sql/_plugins.py +29 -0
  93. meerschaum/connectors/sql/_sql.py +46 -21
  94. meerschaum/connectors/sql/_users.py +29 -2
  95. meerschaum/connectors/sql/tables/__init__.py +1 -1
  96. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
  97. meerschaum/connectors/valkey/_pipes.py +53 -26
  98. meerschaum/connectors/valkey/_plugins.py +2 -26
  99. meerschaum/core/Pipe/__init__.py +59 -19
  100. meerschaum/core/Pipe/_attributes.py +412 -90
  101. meerschaum/core/Pipe/_bootstrap.py +54 -24
  102. meerschaum/core/Pipe/_data.py +96 -18
  103. meerschaum/core/Pipe/_dtypes.py +48 -18
  104. meerschaum/core/Pipe/_edit.py +14 -4
  105. meerschaum/core/Pipe/_fetch.py +1 -1
  106. meerschaum/core/Pipe/_show.py +5 -5
  107. meerschaum/core/Pipe/_sync.py +118 -193
  108. meerschaum/core/Pipe/_verify.py +4 -4
  109. meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
  110. meerschaum/core/Plugin/__init__.py +1 -1
  111. meerschaum/core/Token/_Token.py +220 -0
  112. meerschaum/core/Token/__init__.py +12 -0
  113. meerschaum/core/User/_User.py +34 -8
  114. meerschaum/core/User/__init__.py +9 -1
  115. meerschaum/core/__init__.py +1 -0
  116. meerschaum/jobs/_Job.py +3 -2
  117. meerschaum/jobs/__init__.py +3 -2
  118. meerschaum/jobs/systemd.py +1 -1
  119. meerschaum/models/__init__.py +35 -0
  120. meerschaum/models/pipes.py +247 -0
  121. meerschaum/models/tokens.py +38 -0
  122. meerschaum/models/users.py +26 -0
  123. meerschaum/plugins/__init__.py +22 -7
  124. meerschaum/plugins/bootstrap.py +2 -1
  125. meerschaum/utils/_get_pipes.py +68 -27
  126. meerschaum/utils/daemon/Daemon.py +2 -1
  127. meerschaum/utils/daemon/__init__.py +30 -2
  128. meerschaum/utils/dataframe.py +473 -81
  129. meerschaum/utils/debug.py +15 -15
  130. meerschaum/utils/dtypes/__init__.py +473 -34
  131. meerschaum/utils/dtypes/sql.py +368 -28
  132. meerschaum/utils/formatting/__init__.py +1 -1
  133. meerschaum/utils/formatting/_pipes.py +5 -4
  134. meerschaum/utils/formatting/_shell.py +11 -9
  135. meerschaum/utils/misc.py +246 -148
  136. meerschaum/utils/packages/__init__.py +10 -27
  137. meerschaum/utils/packages/_packages.py +41 -34
  138. meerschaum/utils/pipes.py +181 -0
  139. meerschaum/utils/process.py +1 -1
  140. meerschaum/utils/prompt.py +3 -1
  141. meerschaum/utils/schedule.py +2 -1
  142. meerschaum/utils/sql.py +121 -44
  143. meerschaum/utils/typing.py +1 -4
  144. meerschaum/utils/venv/_Venv.py +2 -2
  145. meerschaum/utils/venv/__init__.py +5 -7
  146. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/METADATA +92 -96
  147. meerschaum-3.0.0rc2.dist-info/RECORD +283 -0
  148. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/WHEEL +1 -1
  149. meerschaum-3.0.0rc2.dist-info/licenses/NOTICE +2 -0
  150. meerschaum/api/models/_interfaces.py +0 -15
  151. meerschaum/api/models/_locations.py +0 -15
  152. meerschaum/api/models/_metrics.py +0 -15
  153. meerschaum/config/static/__init__.py +0 -186
  154. meerschaum-2.9.5.dist-info/RECORD +0 -263
  155. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  156. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  157. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/top_level.txt +0 -0
  158. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc2.dist-info}/zip-safe +0 -0
@@ -23,9 +23,22 @@ from meerschaum.api import (
23
23
  pipes,
24
24
  get_pipe,
25
25
  _get_pipes,
26
- manager,
27
26
  debug,
28
- no_auth,
27
+ ScopedAuth,
28
+ )
29
+ from meerschaum.models import (
30
+ ConnectorKeysModel,
31
+ MetricKeyModel,
32
+ LocationKeyModel,
33
+ InstanceKeysModel,
34
+ PipeModel,
35
+ PipeWithParametersModel,
36
+ PipesWithParametersDictModel,
37
+ )
38
+ from meerschaum.api.models import (
39
+ SuccessTupleResponseModel,
40
+ FetchPipesKeysResponseModel,
41
+ SyncPipeRequestModel,
29
42
  )
30
43
  from meerschaum.api._chunks import generate_chunks_cursor_token
31
44
  from meerschaum.utils.packages import attempt_import
@@ -35,6 +48,7 @@ from meerschaum.utils.misc import (
35
48
  is_pipe_registered,
36
49
  is_int,
37
50
  replace_pipes_in_dict,
51
+ string_to_dict,
38
52
  )
39
53
  from meerschaum.connectors.sql.tables import get_tables
40
54
 
@@ -49,6 +63,7 @@ MAX_RESPONSE_ROW_LIMIT: int = mrsm.get_config('system', 'api', 'data', 'max_resp
49
63
  @app.post(
50
64
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/register',
51
65
  tags=['Pipes: Attributes'],
66
+ response_model=SuccessTupleResponseModel,
52
67
  )
53
68
  def register_pipe(
54
69
  connector_keys: str,
@@ -56,10 +71,8 @@ def register_pipe(
56
71
  location_key: str,
57
72
  instance_keys: Optional[str] = None,
58
73
  parameters: Optional[Dict[str, Any]] = None,
59
- curr_user = (
60
- fastapi.Depends(manager) if not no_auth else None
61
- ),
62
- ):
74
+ curr_user = fastapi.Security(ScopedAuth(['pipes:write']), scopes=['pipes:write']),
75
+ ) -> mrsm.SuccessTuple:
63
76
  """
64
77
  Register a new pipe.
65
78
  """
@@ -69,7 +82,7 @@ def register_pipe(
69
82
  "The administrator for this server has not allowed pipe registration.\n\n"
70
83
  "Please contact the system administrator, or if you are running this server, "
71
84
  "open the configuration file with `edit config system` and search for 'permissions'."
72
- " Under the keys `api:permissions:registration`, " +
85
+ " Under the keys `api:permissions:registration`, " +
73
86
  "you can toggle various registration types."
74
87
  )
75
88
  pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
@@ -79,14 +92,16 @@ def register_pipe(
79
92
  )
80
93
  if parameters:
81
94
  pipe.parameters = parameters
82
- results = get_api_connector(instance_keys).register_pipe(pipe, debug=debug)
83
- pipes(instance_keys, refresh=True)
84
- return results
95
+
96
+ success, msg = get_api_connector(instance_keys).register_pipe(pipe, debug=debug)
97
+ _ = pipes(instance_keys, refresh=True)
98
+ return success, msg
85
99
 
86
100
 
87
101
  @app.patch(
88
102
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/edit',
89
103
  tags=['Pipes: Attributes'],
104
+ response_model=SuccessTupleResponseModel,
90
105
  )
91
106
  def edit_pipe(
92
107
  connector_keys: str,
@@ -95,10 +110,8 @@ def edit_pipe(
95
110
  parameters: dict,
96
111
  instance_keys: Optional[str] = None,
97
112
  patch: bool = False,
98
- curr_user = (
99
- fastapi.Depends(manager) if not no_auth else None
100
- ),
101
- ):
113
+ curr_user = fastapi.Security(ScopedAuth(['pipes:write'])),
114
+ ) -> mrsm.SuccessTuple:
102
115
  """
103
116
  Edit an existing pipe's parameters.
104
117
  """
@@ -111,53 +124,50 @@ def edit_pipe(
111
124
  " Under the keys `api:permissions:actions`, "
112
125
  "you can toggle non-admin actions."
113
126
  )
127
+
114
128
  pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
115
129
  if not is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
116
130
  raise fastapi.HTTPException(
117
131
  status_code=409, detail=f"{pipe} is not registered."
118
132
  )
133
+
119
134
  pipe.parameters = parameters
120
- results = get_api_connector(instance_keys).edit_pipe(pipe, patch=patch, debug=debug)
121
- pipes(instance_keys, refresh=True)
122
- return results
135
+ success, msg = get_api_connector(instance_keys).edit_pipe(pipe, patch=patch, debug=debug)
136
+ _ = pipes(instance_keys, refresh=True)
137
+ return success, msg
123
138
 
124
139
 
125
140
  @app.delete(
126
141
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/delete',
127
142
  tags=['Pipes: Attributes'],
143
+ response_model=SuccessTupleResponseModel,
128
144
  )
129
145
  def delete_pipe(
130
146
  connector_keys: str,
131
147
  metric_key: str,
132
148
  location_key: str,
133
149
  instance_keys: Optional[str] = None,
134
- curr_user = (
135
- fastapi.Depends(manager) if not no_auth else None
136
- ),
137
- ):
150
+ curr_user = fastapi.Security(ScopedAuth(['pipes:delete'])),
151
+ ) -> SuccessTuple:
138
152
  """
139
153
  Delete a Pipe (without dropping its table).
140
154
  """
141
- allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
142
- if not allow_actions:
143
- return False, (
144
- "The administrator for this server has not allowed actions.\n\n"
145
- "Please contact the system administrator, or if you are running this server, "
146
- "open the configuration file with `edit config system` and search for 'permissions'."
147
- " Under the keys `api:permissions:actions`, "
148
- "you can toggle non-admin actions."
149
- )
150
155
  pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
151
156
  if not is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
152
157
  raise fastapi.HTTPException(
153
158
  status_code=409, detail=f"{pipe} is not registered."
154
159
  )
155
- results = get_api_connector(instance_keys).delete_pipe(pipe, debug=debug)
156
- pipes(instance_keys, refresh=True)
157
- return results
160
+
161
+ success, msg = get_api_connector(instance_keys).delete_pipe(pipe, debug=debug)
162
+ _ = pipes(instance_keys, refresh=True)
163
+ return success, msg
158
164
 
159
165
 
160
- @app.get(pipes_endpoint + '/keys', tags=['Pipes: Attributes'])
166
+ @app.get(
167
+ pipes_endpoint + '/keys',
168
+ tags=['Pipes: Attributes'],
169
+ response_model=FetchPipesKeysResponseModel,
170
+ )
161
171
  async def fetch_pipes_keys(
162
172
  connector_keys: str = "[]",
163
173
  metric_keys: str = "[]",
@@ -165,10 +175,8 @@ async def fetch_pipes_keys(
165
175
  instance_keys: Optional[str] = None,
166
176
  tags: str = "[]",
167
177
  params: str = "{}",
168
- curr_user = (
169
- fastapi.Depends(manager) if not no_auth else None
170
- ),
171
- ):
178
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
179
+ ) -> FetchPipesKeysResponseModel:
172
180
  """
173
181
  Get a list of tuples of all registered pipes' keys.
174
182
  """
@@ -187,41 +195,52 @@ async def get_pipes(
187
195
  connector_keys: str = "",
188
196
  metric_keys: str = "",
189
197
  location_keys: str = "",
190
- instance_keys: Optional[str] = None,
191
- curr_user=(
192
- fastapi.Depends(manager) if not no_auth else None
193
- ),
198
+ instance_keys: str = "",
199
+ curr_user=fastapi.Security(ScopedAuth(['pipes:read'])),
194
200
  debug: bool = False,
195
- ) -> Dict[str, Any]:
201
+ ) -> PipesWithParametersDictModel:
196
202
  """
197
203
  Get all registered Pipes with metadata, excluding parameters.
198
204
  """
199
- kw = {'debug': debug, 'mrsm_instance': get_api_connector(instance_keys)}
205
+ kw = {'debug': debug, 'mrsm_instance': get_api_connector(instance_keys or None)}
200
206
  if connector_keys != "":
201
207
  kw['connector_keys'] = connector_keys
202
208
  if metric_keys != "":
203
209
  kw['metric_keys'] = metric_keys
204
210
  if location_keys != "":
205
211
  kw['location_keys'] = location_keys
206
- return replace_pipes_in_dict(_get_pipes(**kw), lambda p: p.attributes)
212
+
213
+ pipes_dict = replace_pipes_in_dict(_get_pipes(**kw), lambda p: p.attributes)
214
+ for metrics in pipes_dict.values():
215
+ for locations in metrics.values():
216
+ if None in locations:
217
+ locations['None'] = locations.pop(None)
218
+ return pipes_dict
207
219
 
208
220
 
209
221
  @app.get(pipes_endpoint + '/{connector_keys}', tags=['Pipes: Attributes'])
210
222
  async def get_pipes_by_connector(
211
223
  connector_keys: str,
212
224
  instance_keys: Optional[str] = None,
213
- curr_user = (
214
- fastapi.Depends(manager) if not no_auth else None
215
- ),
225
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
216
226
  ) -> Dict[str, Any]:
217
227
  """
218
228
  Get all registered Pipes by connector_keys with metadata, excluding parameters.
219
229
  """
220
- if connector_keys not in pipes(instance_keys):
230
+ if connector_keys not in pipes(instance_keys, refresh=True):
221
231
  raise fastapi.HTTPException(
222
232
  status_code=404, detail=f"Connector '{connector_keys}' not found."
223
233
  )
224
- return replace_pipes_in_dict(pipes(instance_keys)[connector_keys], lambda p: p.attributes)
234
+
235
+ metrics = replace_pipes_in_dict(
236
+ pipes(instance_keys, refresh=False)[connector_keys],
237
+ lambda p: p.attributes
238
+ )
239
+ for locations in metrics.values():
240
+ if None in locations:
241
+ locations['None'] = locations.pop(None)
242
+
243
+ return metrics
225
244
 
226
245
 
227
246
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}', tags=['Pipes: Attributes'])
@@ -229,28 +248,33 @@ async def get_pipes_by_connector_and_metric(
229
248
  connector_keys: str,
230
249
  metric_key: str,
231
250
  instance_keys: Optional[str] = None,
232
- curr_user = (
233
- fastapi.Depends(manager) if not no_auth else None
234
- ),
251
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
235
252
  ):
236
253
  """
237
- Get all registered Pipes by connector_keys and metric_key with metadata, excluding parameters.
254
+ Get all registered Pipes by `connector_keys` and `metric_key` with metadata, excluding parameters.
238
255
  """
239
- if connector_keys not in pipes(instance_keys):
256
+ if connector_keys not in pipes(instance_keys, refresh=True):
240
257
  raise fastapi.HTTPException(
241
258
  status_code=404,
242
259
  detail=f"Connector '{connector_keys}' not found.",
243
260
  )
244
- if metric_key not in pipes(instance_keys)[connector_keys]:
261
+
262
+ if metric_key not in pipes(instance_keys, refresh=False)[connector_keys]:
245
263
  raise fastapi.HTTPException(
246
264
  status_code=404,
247
265
  detail=f"Metric '{metric_key}' not found.",
248
266
  )
249
- return replace_pipes_in_dict(
250
- pipes(instance_keys)[connector_keys][metric_key],
267
+
268
+ locations = replace_pipes_in_dict(
269
+ pipes(instance_keys, refresh=False)[connector_keys][metric_key],
251
270
  lambda p: p.attributes
252
271
  )
253
272
 
273
+ if None in locations:
274
+ locations['None'] = locations.pop(None)
275
+
276
+ return locations
277
+
254
278
 
255
279
  @app.get(
256
280
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}',
@@ -261,29 +285,30 @@ async def get_pipe_by_connector_and_metric_and_location(
261
285
  metric_key: str,
262
286
  location_key: str,
263
287
  instance_keys: Optional[str] = None,
264
- curr_user = (
265
- fastapi.Depends(manager) if not no_auth else None
266
- ),
288
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
267
289
  ):
268
290
  """
269
291
  Get a specific Pipe with metadata, excluding parameters.
270
292
  """
271
- if connector_keys not in pipes(instance_keys):
293
+ if connector_keys not in pipes(instance_keys, refresh=True):
272
294
  raise fastapi.HTTPException(
273
295
  status_code=404,
274
296
  detail=f"Connector '{connector_keys}' not found.",
275
297
  )
276
- if metric_key not in pipes(instance_keys)[connector_keys]:
298
+
299
+ if metric_key not in pipes(instance_keys, refresh=False)[connector_keys]:
277
300
  raise fastapi.HTTPException(status_code=404, detail=f"Metric '{metric_key}' not found.")
301
+
278
302
  if location_key in ('[None]', 'None', 'null'):
279
303
  location_key = None
280
- if location_key not in pipes(instance_keys)[connector_keys][metric_key]:
304
+
305
+ if location_key not in pipes(instance_keys, refresh=False)[connector_keys][metric_key]:
281
306
  raise fastapi.HTTPException(
282
307
  status_code=404,
283
308
  detail=f"location_key '{location_key}' not found."
284
309
  )
285
310
 
286
- return pipes(instance_keys)[connector_keys][metric_key][location_key].attributes
311
+ return pipes(instance_keys, refresh=False)[connector_keys][metric_key][location_key].attributes
287
312
 
288
313
 
289
314
  @app.get(
@@ -299,9 +324,7 @@ def get_sync_time(
299
324
  remote: bool = False,
300
325
  round_down: bool = True,
301
326
  instance_keys: Optional[str] = None,
302
- curr_user = (
303
- fastapi.Depends(manager) if not no_auth else None
304
- ),
327
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
305
328
  ) -> Union[str, int, None]:
306
329
  """
307
330
  Get a Pipe's latest datetime value.
@@ -323,31 +346,74 @@ def get_sync_time(
323
346
  @app.post(
324
347
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/data',
325
348
  tags=['Pipes: Data'],
349
+ response_model=SuccessTupleResponseModel,
350
+ openapi_extra={
351
+ 'requestBody': {
352
+ 'content': {
353
+ 'application/json': {
354
+ 'example': [
355
+ {
356
+ 'timestamp': '2026-01-01',
357
+ 'id': 1,
358
+ 'value': 100.1,
359
+ },
360
+ {
361
+ 'timestamp': '2026-01-02',
362
+ 'id': 1,
363
+ 'value': 200.2,
364
+ },
365
+ ],
366
+ },
367
+ 'text/plain': {
368
+ 'example': 'a:1,b:2',
369
+ },
370
+ },
371
+ 'required': True,
372
+ },
373
+ },
326
374
  )
327
- def sync_pipe(
375
+ async def sync_pipe(
328
376
  connector_keys: str,
329
377
  metric_key: str,
330
378
  location_key: str,
331
- data: Union[List[Dict[Any, Any]], Dict[Any, Any]],
379
+ request: fastapi.Request,
332
380
  instance_keys: Optional[str] = None,
333
381
  check_existing: bool = True,
334
382
  blocking: bool = True,
335
383
  force: bool = False,
336
384
  workers: Optional[int] = None,
337
385
  columns: Optional[str] = None,
338
- curr_user = (
339
- fastapi.Depends(manager) if not no_auth else None
340
- ),
386
+ curr_user = fastapi.Security(ScopedAuth(['pipes:write'])),
341
387
  debug: bool = False,
342
- ) -> List[Union[bool, str]]:
388
+ ) -> mrsm.SuccessTuple:
343
389
  """
344
390
  Add data to an existing Pipe.
345
391
  See [`meerschaum.Pipe.sync`](https://docs.meerschaum.io/meerschaum.html#Pipe.sync).
346
392
  """
393
+ body = await request.body()
394
+ try:
395
+ data = json.loads(body)
396
+ except (json.JSONDecodeError, UnicodeDecodeError):
397
+ data = body.decode('utf-8', errors='replace')
398
+
399
+ if not data:
400
+ return True, "No data to sync."
401
+
402
+ if isinstance(data, str) and data.strip() and not data.lstrip()[0] not in ('{', '['):
403
+ try:
404
+ lines = data.splitlines()
405
+ data = [string_to_dict(line) for line in lines]
406
+ except Exception:
407
+ data = None
408
+
347
409
  if not data:
348
- return [True, "No data to sync."]
349
- pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
350
- if pipe.target in ('mrsm_users', 'mrsm_plugins', 'mrsm_pipes'):
410
+ raise fastapi.HTTPException(
411
+ status=400,
412
+ detail="Cannot sync given data.",
413
+ )
414
+
415
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys, refresh=True)
416
+ if pipe.target in ('mrsm_users', 'mrsm_plugins', 'mrsm_pipes', 'mrsm_tokens'):
351
417
  raise fastapi.HTTPException(
352
418
  status_code=409,
353
419
  detail=f"Cannot sync data to protected table '{pipe.target}'.",
@@ -364,7 +430,7 @@ def sync_pipe(
364
430
  force=force,
365
431
  workers=workers,
366
432
  )
367
- return list((success, msg))
433
+ return success, msg
368
434
 
369
435
 
370
436
  @app.get(
@@ -387,9 +453,7 @@ def get_pipe_data(
387
453
  date_unit: str = 'us',
388
454
  double_precision: int = 15,
389
455
  geometry_format: str = 'wkb_hex',
390
- curr_user = (
391
- fastapi.Depends(manager) if not no_auth else None
392
- ),
456
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
393
457
  ) -> str:
394
458
  """
395
459
  Get a pipe's data, applying any filtering.
@@ -525,6 +589,7 @@ def get_pipe_chunk_bounds(
525
589
  end: Union[str, int, None] = None,
526
590
  bounded: bool = True,
527
591
  chunk_interval_minutes: Union[int, None] = None,
592
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
528
593
  ) -> List[List[Union[str, int, None]]]:
529
594
  """
530
595
  Return a list of request boundaries between `begin` and `end` (or the pipe's sync times).
@@ -558,37 +623,28 @@ def get_pipe_chunk_bounds(
558
623
  @app.delete(
559
624
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/drop',
560
625
  tags=['Pipes: Data'],
626
+ response_model=SuccessTupleResponseModel,
561
627
  )
562
628
  def drop_pipe(
563
629
  connector_keys: str,
564
630
  metric_key: str,
565
631
  location_key: str,
566
632
  instance_keys: Optional[str] = None,
567
- curr_user = (
568
- fastapi.Depends(manager) if not no_auth else None
569
- ),
570
- ):
633
+ curr_user = fastapi.Security(ScopedAuth(['pipes:drop'])),
634
+ ) -> mrsm.SuccessTuple:
571
635
  """
572
636
  Drop a pipe's target table.
573
637
  """
574
- allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
575
- if not allow_actions:
576
- return False, (
577
- "The administrator for this server has not allowed actions.\n\n"
578
- "Please contact the system administrator, or if you are running this server, "
579
- "open the configuration file with `edit config system` and search for 'permissions'."
580
- " Under the keys `api:permissions:actions`, " +
581
- "you can toggle non-admin actions."
582
- )
583
- pipe_object = get_pipe(connector_keys, metric_key, location_key, instance_keys)
584
- results = get_api_connector(instance_keys=instance_keys).drop_pipe(pipe_object, debug=debug)
585
- pipes(instance_keys, refresh=True)
586
- return results
638
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
639
+ success, msg = pipe.drop(debug=debug)
640
+ _ = pipes(instance_keys, refresh=True)
641
+ return success, msg
587
642
 
588
643
 
589
644
  @app.delete(
590
645
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/clear',
591
646
  tags=['Pipes: Data'],
647
+ response_model=SuccessTupleResponseModel,
592
648
  )
593
649
  def clear_pipe(
594
650
  connector_keys: str,
@@ -598,10 +654,8 @@ def clear_pipe(
598
654
  begin: Union[str, int, None] = None,
599
655
  end: Union[str, int, None] = None,
600
656
  params: Optional[str] = None,
601
- curr_user = (
602
- fastapi.Depends(manager) if not no_auth else None
603
- ),
604
- ):
657
+ curr_user = fastapi.Security(ScopedAuth(['pipes:delete'])),
658
+ ) -> SuccessTupleResponseModel:
605
659
  """
606
660
  Delete rows from a pipe's target table.
607
661
  """
@@ -613,21 +667,13 @@ def clear_pipe(
613
667
  _params = json.loads(params)
614
668
  except Exception:
615
669
  _params = None
670
+
616
671
  if not isinstance(_params, dict):
617
672
  raise fastapi.HTTPException(
618
673
  status_code=409,
619
674
  detail="Params must be a valid JSON-encoded dictionary.",
620
675
  )
621
676
 
622
- allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
623
- if not allow_actions:
624
- return False, (
625
- "The administrator for this server has not allowed actions.\n\n"
626
- "Please contact the system administrator, or if you are running this server, "
627
- "open the configuration file with `edit config system` and search for 'permissions'."
628
- " Under the keys `api:permissions:actions`, " +
629
- "you can toggle non-admin actions."
630
- )
631
677
  pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
632
678
  begin, end = pipe.parse_date_bounds(begin, end)
633
679
  results = get_api_connector(instance_keys=instance_keys).clear_pipe(
@@ -637,7 +683,7 @@ def clear_pipe(
637
683
  params=_params,
638
684
  debug=debug,
639
685
  )
640
- pipes(instance_keys, refresh=True)
686
+ _ = pipes(instance_keys, refresh=True)
641
687
  return results
642
688
 
643
689
 
@@ -653,9 +699,7 @@ def get_pipe_csv(
653
699
  begin: Union[str, int, None] = None,
654
700
  end: Union[str, int, None] = None,
655
701
  params: Optional[str] = None,
656
- curr_user = (
657
- fastapi.Depends(manager) if not no_auth else None
658
- ),
702
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
659
703
  ) -> str:
660
704
  """
661
705
  Get a pipe's data as a CSV file. Optionally set query boundaries.
@@ -720,9 +764,7 @@ def get_pipe_id(
720
764
  metric_key: str,
721
765
  location_key: str,
722
766
  instance_keys: Optional[str] = None,
723
- curr_user = (
724
- fastapi.Depends(manager) if not no_auth else None
725
- ),
767
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
726
768
  ) -> Union[int, str]:
727
769
  """
728
770
  Get a pipe's ID.
@@ -742,9 +784,7 @@ def get_pipe_attributes(
742
784
  metric_key: str,
743
785
  location_key: str,
744
786
  instance_keys: Optional[str] = None,
745
- curr_user=(
746
- fastapi.Depends(manager) if not no_auth else None
747
- ),
787
+ curr_user=fastapi.Security(ScopedAuth(['pipes:read'])),
748
788
  ) -> Dict[str, Any]:
749
789
  """Get a pipe's attributes."""
750
790
  return get_pipe(
@@ -765,9 +805,7 @@ def get_pipe_exists(
765
805
  metric_key: str,
766
806
  location_key: str,
767
807
  instance_keys: Optional[str] = None,
768
- curr_user = (
769
- fastapi.Depends(manager) if not no_auth else None
770
- ),
808
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
771
809
  ) -> bool:
772
810
  """Determine whether a pipe's target table exists."""
773
811
  return get_pipe(connector_keys, metric_key, location_key, instance_keys).exists(debug=debug)
@@ -776,9 +814,7 @@ def get_pipe_exists(
776
814
  @app.post(endpoints['metadata'], tags=['Misc'])
777
815
  def create_metadata(
778
816
  instance_keys: Optional[str] = None,
779
- curr_user = (
780
- fastapi.Depends(manager) if not no_auth else None
781
- ),
817
+ curr_user = fastapi.Security(ScopedAuth(['actions:execute'])),
782
818
  ) -> bool:
783
819
  """Create pipe instance metadata tables."""
784
820
  conn = get_api_connector(instance_keys)
@@ -804,9 +840,7 @@ def get_pipe_rowcount(
804
840
  end: Union[str, int, None] = None,
805
841
  params: Optional[Dict[str, Any]] = None,
806
842
  remote: bool = False,
807
- curr_user = (
808
- fastapi.Depends(manager) if not no_auth else None
809
- ),
843
+ curr_user = fastapi.Security(ScopedAuth(['pipes:read'])),
810
844
  ) -> int:
811
845
  """
812
846
  Return a pipe's rowcount.
@@ -852,9 +886,7 @@ def get_pipe_columns_types(
852
886
  metric_key: str,
853
887
  location_key: str,
854
888
  instance_keys: Optional[str] = None,
855
- curr_user=(
856
- fastapi.Depends(manager) if not no_auth else None
857
- ),
889
+ curr_user=fastapi.Security(ScopedAuth(['pipes:read'])),
858
890
  ) -> Dict[str, str]:
859
891
  """
860
892
  Return a dictionary of column names and types.
@@ -868,7 +900,12 @@ def get_pipe_columns_types(
868
900
  }
869
901
  ```
870
902
  """
871
- return get_pipe(connector_keys, metric_key, location_key, instance_keys).dtypes
903
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
904
+ columns_types = (
905
+ pipe.get_columns_types(debug=debug)
906
+ or pipe.get_dtypes(refresh=True, debug=debug)
907
+ )
908
+ return columns_types
872
909
 
873
910
 
874
911
  @app.get(
@@ -880,9 +917,7 @@ def get_pipe_columns_indices(
880
917
  metric_key: str,
881
918
  location_key: str,
882
919
  instance_keys: Optional[str] = None,
883
- curr_user=(
884
- fastapi.Depends(manager) if not no_auth else None
885
- ),
920
+ curr_user=fastapi.Security(ScopedAuth(['pipes:read'])),
886
921
  ) -> Dict[str, List[Dict[str, str]]]:
887
922
  """
888
923
  Return a dictionary of column names and related indices.
@@ -934,9 +969,7 @@ def get_pipe_index_names(
934
969
  metric_key: str,
935
970
  location_key: str,
936
971
  instance_keys: Optional[str] = None,
937
- curr_user=(
938
- fastapi.Depends(manager) if not no_auth else None
939
- ),
972
+ curr_user=fastapi.Security(ScopedAuth(['pipes:read'])),
940
973
  ) -> Dict[str, Union[str, Dict[str, str], List[Dict[str, str]]]]:
941
974
  """
942
975
  Return a dictionary of index keys and index names.