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