meerschaum 2.7.9__py3-none-any.whl → 2.8.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 (61) hide show
  1. meerschaum/_internal/arguments/_parser.py +17 -5
  2. meerschaum/_internal/term/TermPageHandler.py +1 -1
  3. meerschaum/_internal/term/__init__.py +1 -1
  4. meerschaum/actions/api.py +36 -10
  5. meerschaum/actions/copy.py +3 -1
  6. meerschaum/actions/index.py +1 -1
  7. meerschaum/actions/show.py +7 -7
  8. meerschaum/actions/sync.py +5 -1
  9. meerschaum/actions/verify.py +14 -1
  10. meerschaum/api/__init__.py +77 -41
  11. meerschaum/api/_exceptions.py +18 -0
  12. meerschaum/api/dash/__init__.py +4 -2
  13. meerschaum/api/dash/callbacks/dashboard.py +30 -1
  14. meerschaum/api/dash/components.py +2 -2
  15. meerschaum/api/dash/webterm.py +23 -4
  16. meerschaum/api/models/_pipes.py +8 -8
  17. meerschaum/api/resources/static/css/dash.css +2 -2
  18. meerschaum/api/resources/templates/termpage.html +5 -1
  19. meerschaum/api/routes/__init__.py +15 -12
  20. meerschaum/api/routes/_connectors.py +30 -28
  21. meerschaum/api/routes/_index.py +16 -7
  22. meerschaum/api/routes/_misc.py +30 -22
  23. meerschaum/api/routes/_pipes.py +244 -148
  24. meerschaum/api/routes/_plugins.py +58 -47
  25. meerschaum/api/routes/_users.py +39 -31
  26. meerschaum/api/routes/_version.py +8 -10
  27. meerschaum/api/routes/_webterm.py +2 -2
  28. meerschaum/config/_default.py +10 -0
  29. meerschaum/config/_version.py +1 -1
  30. meerschaum/config/static/__init__.py +5 -2
  31. meerschaum/connectors/api/_APIConnector.py +4 -3
  32. meerschaum/connectors/api/_login.py +21 -17
  33. meerschaum/connectors/api/_pipes.py +1 -0
  34. meerschaum/connectors/api/_request.py +9 -10
  35. meerschaum/connectors/sql/_cli.py +11 -3
  36. meerschaum/connectors/sql/_instance.py +1 -1
  37. meerschaum/connectors/sql/_pipes.py +77 -57
  38. meerschaum/connectors/sql/_sql.py +26 -9
  39. meerschaum/core/Pipe/__init__.py +2 -0
  40. meerschaum/core/Pipe/_attributes.py +13 -2
  41. meerschaum/core/Pipe/_data.py +85 -0
  42. meerschaum/core/Pipe/_deduplicate.py +6 -8
  43. meerschaum/core/Pipe/_sync.py +63 -30
  44. meerschaum/core/Pipe/_verify.py +242 -77
  45. meerschaum/core/User/__init__.py +2 -6
  46. meerschaum/jobs/_Job.py +1 -1
  47. meerschaum/jobs/__init__.py +15 -0
  48. meerschaum/utils/dataframe.py +2 -0
  49. meerschaum/utils/dtypes/sql.py +26 -0
  50. meerschaum/utils/formatting/_pipes.py +1 -1
  51. meerschaum/utils/misc.py +11 -7
  52. meerschaum/utils/packages/_packages.py +1 -1
  53. meerschaum/utils/sql.py +6 -2
  54. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/METADATA +4 -4
  55. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/RECORD +61 -60
  56. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/LICENSE +0 -0
  57. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/NOTICE +0 -0
  58. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/WHEEL +0 -0
  59. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/entry_points.txt +0 -0
  60. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/top_level.txt +0 -0
  61. {meerschaum-2.7.9.dist-info → meerschaum-2.8.0.dist-info}/zip-safe +0 -0
@@ -11,8 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import io
13
13
  import json
14
- from decimal import Decimal
15
- import datetime
14
+ from datetime import datetime, timedelta
16
15
 
17
16
  import meerschaum as mrsm
18
17
  from meerschaum.utils.typing import Any, Optional, Dict, Union, List
@@ -27,20 +26,15 @@ from meerschaum.api import (
27
26
  manager,
28
27
  debug,
29
28
  no_auth,
30
- private,
31
29
  )
32
- from meerschaum import Pipe
33
- from meerschaum.api.models import MetaPipe
34
- from meerschaum.utils.packages import attempt_import, import_pandas
35
- from meerschaum.utils.dataframe import get_numeric_cols, to_json, parse_df_datetimes
36
- from meerschaum.utils.dtypes import are_dtypes_equal
30
+ from meerschaum.utils.packages import attempt_import
31
+ from meerschaum.utils.dataframe import to_json
37
32
  from meerschaum.utils.misc import (
38
33
  is_pipe_registered,
39
- round_time,
40
34
  is_int,
41
35
  replace_pipes_in_dict,
42
36
  )
43
- import meerschaum.core.User
37
+ from meerschaum.connectors.sql.tables import get_tables
44
38
 
45
39
  fastapi_responses = attempt_import('fastapi.responses', lazy=False)
46
40
  StreamingResponse = fastapi_responses.StreamingResponse
@@ -48,13 +42,16 @@ dateutil_parser = attempt_import('dateutil.parser', lazy=False)
48
42
  pipes_endpoint = endpoints['pipes']
49
43
  pd = attempt_import('pandas', lazy=False)
50
44
 
45
+ MAX_RESPONSE_ROW_LIMIT: int = mrsm.get_config('system', 'api', 'data', 'max_response_row_limit')
46
+
51
47
 
52
48
  @app.post(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/register', tags=['Pipes'])
53
49
  def register_pipe(
54
50
  connector_keys: str,
55
51
  metric_key: str,
56
52
  location_key: str,
57
- parameters: dict,
53
+ instance_keys: Optional[str] = None,
54
+ parameters: Optional[Dict[str, Any]] = None,
58
55
  curr_user = (
59
56
  fastapi.Depends(manager) if not no_auth else None
60
57
  ),
@@ -62,25 +59,24 @@ def register_pipe(
62
59
  """
63
60
  Register a new pipe.
64
61
  """
65
- from meerschaum.config import get_config
66
- allow_pipes = get_config('system', 'api', 'permissions', 'registration', 'pipes', patch=True)
62
+ allow_pipes = mrsm.get_config('system', 'api', 'permissions', 'registration', 'pipes')
67
63
  if not allow_pipes:
68
64
  return False, (
69
- "The administrator for this server has not allowed pipe registration.\n\n" +
70
- "Please contact the system administrator, or if you are running this server, " +
71
- "open the configuration file with `edit config system` and search for 'permissions'." +
65
+ "The administrator for this server has not allowed pipe registration.\n\n"
66
+ "Please contact the system administrator, or if you are running this server, "
67
+ "open the configuration file with `edit config system` and search for 'permissions'."
72
68
  " Under the keys `api:permissions:registration`, " +
73
69
  "you can toggle various registration types."
74
70
  )
75
- pipe_object = get_pipe(connector_keys, metric_key, location_key)
76
- if is_pipe_registered(pipe_object, pipes(refresh=True)):
71
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
72
+ if is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
77
73
  raise fastapi.HTTPException(
78
- status_code=409, detail=f"{pipe_object} already registered."
74
+ status_code=409, detail=f"{pipe} already registered."
79
75
  )
80
- pipe_object.parameters = parameters
81
- results = get_api_connector().register_pipe(pipe_object, debug=debug)
82
- pipes(refresh=True)
83
-
76
+ if parameters:
77
+ pipe.parameters = parameters
78
+ results = get_api_connector(instance_keys).register_pipe(pipe, debug=debug)
79
+ pipes(instance_keys, refresh=True)
84
80
  return results
85
81
 
86
82
 
@@ -89,6 +85,7 @@ def delete_pipe(
89
85
  connector_keys: str,
90
86
  metric_key: str,
91
87
  location_key: str,
88
+ instance_keys: Optional[str] = None,
92
89
  curr_user = (
93
90
  fastapi.Depends(manager) if not no_auth else None
94
91
  ),
@@ -96,52 +93,50 @@ def delete_pipe(
96
93
  """
97
94
  Delete a Pipe (without dropping its table).
98
95
  """
99
- from meerschaum.config import get_config
100
- allow_actions = get_config('system', 'api', 'permissions', 'actions', 'non_admin')
96
+ allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
101
97
  if not allow_actions:
102
98
  return False, (
103
- "The administrator for this server has not allowed actions.\n\n" +
104
- "Please contact the system administrator, or if you are running this server, " +
105
- "open the configuration file with `edit config system` and search for 'permissions'." +
106
- " Under the keys `api:permissions:actions`, " +
99
+ "The administrator for this server has not allowed actions.\n\n"
100
+ "Please contact the system administrator, or if you are running this server, "
101
+ "open the configuration file with `edit config system` and search for 'permissions'."
102
+ " Under the keys `api:permissions:actions`, "
107
103
  "you can toggle non-admin actions."
108
104
  )
109
- pipe = get_pipe(connector_keys, metric_key, location_key)
110
- if not is_pipe_registered(pipe, pipes(refresh=True)):
105
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
106
+ if not is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
111
107
  raise fastapi.HTTPException(
112
108
  status_code=409, detail=f"{pipe} is not registered."
113
109
  )
114
- results = get_api_connector().delete_pipe(pipe, debug=debug)
115
- pipes(refresh=True)
116
-
110
+ results = get_api_connector(instance_keys).delete_pipe(pipe, debug=debug)
111
+ pipes(instance_keys, refresh=True)
117
112
  return results
118
113
 
119
114
 
120
115
  @app.delete(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/drop', tags=['Pipes'])
121
116
  def drop_pipe(
122
- connector_keys: str,
123
- metric_key: str,
124
- location_key: str,
125
- curr_user = (
126
- fastapi.Depends(manager) if not no_auth else None
127
- ),
128
- ):
129
- """
130
- Dropping a pipes' table (without deleting its registration).
131
- """
132
- from meerschaum.config import get_config
133
- allow_actions = get_config('system', 'api', 'permissions', 'actions', 'non_admin')
117
+ connector_keys: str,
118
+ metric_key: str,
119
+ location_key: str,
120
+ instance_keys: Optional[str] = None,
121
+ curr_user = (
122
+ fastapi.Depends(manager) if not no_auth else None
123
+ ),
124
+ ):
125
+ """
126
+ Drop a pipes' underlying target table.
127
+ """
128
+ allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
134
129
  if not allow_actions:
135
130
  return False, (
136
- "The administrator for this server has not allowed actions.\n\n" +
137
- "Please contact the system administrator, or if you are running this server, " +
138
- "open the configuration file with `edit config system` and search for 'permissions'." +
131
+ "The administrator for this server has not allowed actions.\n\n"
132
+ "Please contact the system administrator, or if you are running this server, "
133
+ "open the configuration file with `edit config system` and search for 'permissions'."
139
134
  " Under the keys `api:permissions:actions`, " +
140
135
  "you can toggle non-admin actions."
141
136
  )
142
- pipe_object = get_pipe(connector_keys, metric_key, location_key)
143
- results = get_api_connector().drop_pipe(pipe_object, debug=debug)
144
- pipes(refresh=True)
137
+ pipe_object = get_pipe(connector_keys, metric_key, location_key, instance_keys)
138
+ results = get_api_connector(instance_keys=instance_keys).drop_pipe(pipe_object, debug=debug)
139
+ pipes(instance_keys, refresh=True)
145
140
  return results
146
141
 
147
142
 
@@ -152,33 +147,32 @@ def edit_pipe(
152
147
  metric_key: str,
153
148
  location_key: str,
154
149
  parameters: dict,
150
+ instance_keys: Optional[str] = None,
155
151
  patch: bool = False,
156
152
  curr_user = (
157
153
  fastapi.Depends(manager) if not no_auth else None
158
154
  ),
159
155
  ):
160
156
  """
161
- Edit an existing pipe.
157
+ Edit an existing pipe's parameters.
162
158
  """
163
- from meerschaum.config import get_config
164
- allow_pipes = get_config('system', 'api', 'permissions', 'actions', 'non_admin')
165
- if not allow_pipes:
159
+ allow_actions = mrsm.get_config('system', 'api', 'permissions', 'actions', 'non_admin')
160
+ if not allow_actions:
166
161
  return False, (
167
- "The administrator for this server has not allowed actions.\n\n" +
168
- "Please contact the system administrator, or if you are running this server, " +
169
- "open the configuration file with `edit config system` and search for 'permissions'." +
170
- " Under the keys `api:permissions:actions`, " +
162
+ "The administrator for this server has not allowed actions.\n\n"
163
+ "Please contact the system administrator, or if you are running this server, "
164
+ "open the configuration file with `edit config system` and search for 'permissions'."
165
+ " Under the keys `api:permissions:actions`, "
171
166
  "you can toggle non-admin actions."
172
167
  )
173
- pipe_object = get_pipe(connector_keys, metric_key, location_key)
174
- if not is_pipe_registered(pipe_object, pipes(refresh=True)):
168
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
169
+ if not is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
175
170
  raise fastapi.HTTPException(
176
- status_code=409, detail=f"{pipe_object} is not registered."
171
+ status_code=409, detail=f"{pipe} is not registered."
177
172
  )
178
- pipe_object.parameters = parameters
179
- results = get_api_connector().edit_pipe(pipe_object, patch=patch, debug=debug)
180
- pipes(refresh=True)
181
-
173
+ pipe.parameters = parameters
174
+ results = get_api_connector(instance_keys).edit_pipe(pipe, patch=patch, debug=debug)
175
+ pipes(instance_keys, refresh=True)
182
176
  return results
183
177
 
184
178
 
@@ -187,6 +181,7 @@ async def fetch_pipes_keys(
187
181
  connector_keys: str = "[]",
188
182
  metric_keys: str = "[]",
189
183
  location_keys: str = "[]",
184
+ instance_keys: Optional[str] = None,
190
185
  tags: str = "[]",
191
186
  params: str = "{}",
192
187
  curr_user = (
@@ -194,14 +189,14 @@ async def fetch_pipes_keys(
194
189
  ),
195
190
  ):
196
191
  """
197
- Get a list of tuples of all registered Pipes' keys.
192
+ Get a list of tuples of all registered pipes' keys.
198
193
  """
199
- keys = get_api_connector().fetch_pipes_keys(
200
- connector_keys = json.loads(connector_keys),
201
- metric_keys = json.loads(metric_keys),
202
- location_keys = json.loads(location_keys),
203
- tags = json.loads(tags),
204
- params = json.loads(params),
194
+ keys = get_api_connector(instance_keys).fetch_pipes_keys(
195
+ connector_keys=json.loads(connector_keys),
196
+ metric_keys=json.loads(metric_keys),
197
+ location_keys=json.loads(location_keys),
198
+ tags=json.loads(tags),
199
+ params=json.loads(params),
205
200
  )
206
201
  return keys
207
202
 
@@ -211,6 +206,7 @@ async def get_pipes(
211
206
  connector_keys: str = "",
212
207
  metric_keys: str = "",
213
208
  location_keys: str = "",
209
+ instance_keys: Optional[str] = None,
214
210
  curr_user=(
215
211
  fastapi.Depends(manager) if not no_auth else None
216
212
  ),
@@ -219,18 +215,20 @@ async def get_pipes(
219
215
  """
220
216
  Get all registered Pipes with metadata, excluding parameters.
221
217
  """
222
- kw = {'debug' : debug, 'mrsm_instance' : get_api_connector()}
218
+ kw = {'debug': debug, 'mrsm_instance': get_api_connector(instance_keys)}
223
219
  if connector_keys != "":
224
220
  kw['connector_keys'] = connector_keys
225
221
  if metric_keys != "":
226
222
  kw['metric_keys'] = metric_keys
227
223
  if location_keys != "":
228
224
  kw['location_keys'] = location_keys
229
- return replace_pipes_in_dict(_get_pipes(**kw), str)
225
+ return replace_pipes_in_dict(_get_pipes(**kw), lambda p: p.attributes)
226
+
230
227
 
231
228
  @app.get(pipes_endpoint + '/{connector_keys}', tags=['Pipes'])
232
229
  async def get_pipes_by_connector(
233
230
  connector_keys: str,
231
+ instance_keys: Optional[str] = None,
234
232
  curr_user = (
235
233
  fastapi.Depends(manager) if not no_auth else None
236
234
  ),
@@ -238,40 +236,39 @@ async def get_pipes_by_connector(
238
236
  """
239
237
  Get all registered Pipes by connector_keys with metadata, excluding parameters.
240
238
  """
241
- from meerschaum.utils.misc import replace_pipes_in_dict
242
- if connector_keys not in pipes():
239
+ if connector_keys not in pipes(instance_keys):
243
240
  raise fastapi.HTTPException(
244
- status_code=404, detail=f"connector_keys '{connector_keys}' not found."
241
+ status_code=404, detail=f"Connector '{connector_keys}' not found."
245
242
  )
246
- return replace_pipes_in_dict(pipes()[connector_keys], str)
243
+ return replace_pipes_in_dict(pipes(instance_keys)[connector_keys], lambda p: p.attributes)
244
+
247
245
 
248
246
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}', tags=['Pipes'])
249
247
  async def get_pipes_by_connector_and_metric(
250
248
  connector_keys: str,
251
249
  metric_key: str,
252
- parent: bool = False,
250
+ instance_keys: Optional[str] = None,
253
251
  curr_user = (
254
252
  fastapi.Depends(manager) if not no_auth else None
255
253
  ),
256
254
  ):
257
255
  """
258
256
  Get all registered Pipes by connector_keys and metric_key with metadata, excluding parameters.
259
-
260
- Parameters
261
- ----------
262
- parent: bool, default False
263
- Return the parent Pipe (`location_key` is `None`)
264
257
  """
265
- from meerschaum.utils.misc import replace_pipes_in_dict
266
- if connector_keys not in pipes():
258
+ if connector_keys not in pipes(instance_keys):
267
259
  raise fastapi.HTTPException(
268
- status_code=404, detail=f"connector_keys '{connector_keys}' not found."
260
+ status_code=404,
261
+ detail=f"Connector '{connector_keys}' not found.",
269
262
  )
270
- if metric_key not in pipes()[connector_keys]:
271
- raise fastapi.HTTPException(status_code=404, detail=f"metric_key '{metric_key}' not found.")
272
- if parent:
273
- return pipes()[connector_keys][metric_key][None]
274
- return replace_pipes_in_dict(pipes()[connector_keys][metric_key], str)
263
+ if metric_key not in pipes(instance_keys)[connector_keys]:
264
+ raise fastapi.HTTPException(
265
+ status_code=404,
266
+ detail=f"Metric '{metric_key}' not found.",
267
+ )
268
+ return replace_pipes_in_dict(
269
+ pipes(instance_keys)[connector_keys][metric_key],
270
+ lambda p: p.attributes
271
+ )
275
272
 
276
273
 
277
274
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}', tags=['Pipes'])
@@ -279,6 +276,7 @@ async def get_pipes_by_connector_and_metric_and_location(
279
276
  connector_keys: str,
280
277
  metric_key: str,
281
278
  location_key: str,
279
+ instance_keys: Optional[str] = None,
282
280
  curr_user = (
283
281
  fastapi.Depends(manager) if not no_auth else None
284
282
  ),
@@ -286,20 +284,22 @@ async def get_pipes_by_connector_and_metric_and_location(
286
284
  """
287
285
  Get a specific Pipe with metadata, excluding parameters.
288
286
  """
289
- if connector_keys not in pipes():
287
+ if connector_keys not in pipes(instance_keys):
290
288
  raise fastapi.HTTPException(
291
- status_code=404, detail=f"connector_keys '{connector_keys}' not found."
289
+ status_code=404,
290
+ detail=f"Connector '{connector_keys}' not found.",
292
291
  )
293
- if metric_key not in pipes()[connector_keys]:
294
- raise fastapi.HTTPException(status_code=404, detail=f"metric_key '{metric_key}' not found.")
292
+ if metric_key not in pipes(instance_keys)[connector_keys]:
293
+ raise fastapi.HTTPException(status_code=404, detail=f"Metric '{metric_key}' not found.")
295
294
  if location_key in ('[None]', 'None', 'null'):
296
295
  location_key = None
297
- if location_key not in pipes()[connector_keys][metric_key]:
296
+ if location_key not in pipes(instance_keys)[connector_keys][metric_key]:
298
297
  raise fastapi.HTTPException(
299
- status_code=404, detail=f"location_key '{location_key}' not found."
298
+ status_code=404,
299
+ detail=f"location_key '{location_key}' not found."
300
300
  )
301
301
 
302
- return str(pipes()[connector_keys][metric_key][location_key])
302
+ return str(pipes(instance_keys)[connector_keys][metric_key][location_key])
303
303
 
304
304
 
305
305
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/sync_time', tags=['Pipes'])
@@ -309,6 +309,7 @@ def get_sync_time(
309
309
  location_key: str,
310
310
  params: Optional[Dict[str, Any]] = None,
311
311
  newest: bool = True,
312
+ remote: bool = False,
312
313
  round_down: bool = True,
313
314
  debug: bool = False,
314
315
  curr_user = (
@@ -317,7 +318,7 @@ def get_sync_time(
317
318
  ) -> Union[str, int, None]:
318
319
  """
319
320
  Get a Pipe's latest datetime value.
320
- See `meerschaum.Pipe.get_sync_time`.
321
+ See [`meerschaum.Pipe.get_sync_time`](https://docs.meerschaum.io/meerschaum.html#Pipe.get_sync_time).
321
322
  """
322
323
  if location_key == '[None]':
323
324
  location_key = None
@@ -328,7 +329,7 @@ def get_sync_time(
328
329
  debug = debug,
329
330
  round_down = round_down,
330
331
  )
331
- if isinstance(sync_time, datetime.datetime):
332
+ if isinstance(sync_time, datetime):
332
333
  sync_time = sync_time.isoformat()
333
334
  return sync_time
334
335
 
@@ -351,26 +352,26 @@ def sync_pipe(
351
352
  ) -> List[Union[bool, str]]:
352
353
  """
353
354
  Add data to an existing Pipe.
354
- See `meerschaum.Pipe.sync`.
355
+ See [`meerschaum.Pipe.sync`](https://docs.meerschaum.io/meerschaum.html#Pipe.sync).
355
356
  """
356
357
  if data is None:
357
358
  data = {}
358
- p = get_pipe(connector_keys, metric_key, location_key)
359
- if p.target in ('users', 'plugins', 'pipes'):
359
+ pipe = get_pipe(connector_keys, metric_key, location_key)
360
+ if pipe.target in ('mrsm_users', 'mrsm_plugins', 'mrsm_pipes'):
360
361
  raise fastapi.HTTPException(
361
362
  status_code=409,
362
- detail=f"Cannot sync data to protected table '{p.target}'.",
363
+ detail=f"Cannot sync data to protected table '{pipe.target}'.",
363
364
  )
364
365
 
365
- if not p.columns and columns is not None:
366
- p.columns = json.loads(columns)
367
- if not p.columns and not is_pipe_registered(p, pipes(refresh=True)):
366
+ if not pipe.columns and columns is not None:
367
+ pipe.columns = json.loads(columns)
368
+ if not pipe.columns and not is_pipe_registered(pipe, pipes(refresh=True)):
368
369
  raise fastapi.HTTPException(
369
370
  status_code=409,
370
371
  detail="Pipe must be registered with index columns specified."
371
372
  )
372
373
 
373
- result = list(p.sync(
374
+ result = list(pipe.sync(
374
375
  data,
375
376
  debug=debug,
376
377
  check_existing=check_existing,
@@ -386,17 +387,20 @@ def get_pipe_data(
386
387
  connector_keys: str,
387
388
  metric_key: str,
388
389
  location_key: str,
390
+ instance_keys: Optional[str] = None,
389
391
  select_columns: Optional[str] = None,
390
392
  omit_columns: Optional[str] = None,
391
393
  begin: Union[str, int, None] = None,
392
394
  end: Union[str, int, None] = None,
393
395
  params: Optional[str] = None,
396
+ limit: int = MAX_RESPONSE_ROW_LIMIT,
394
397
  curr_user = (
395
398
  fastapi.Depends(manager) if not no_auth else None
396
399
  ),
397
400
  ) -> str:
398
401
  """
399
402
  Get a pipe's data, applying any filtering.
403
+ See [`Pipe.get_data()`](https://docs.meerschaum.io/meerschaum.html#Pipe.get_data).
400
404
 
401
405
  Note that `select_columns`, `omit_columns`, and `params` are JSON-encoded strings.
402
406
  """
@@ -404,6 +408,14 @@ def get_pipe_data(
404
408
  begin = int(begin)
405
409
  if is_int(end):
406
410
  end = int(end)
411
+ if limit > MAX_RESPONSE_ROW_LIMIT:
412
+ raise fastapi.HTTPException(
413
+ status_code=413,
414
+ detail=(
415
+ f"Requested limit {limit} exceeds the maximum response size of "
416
+ f"{MAX_RESPONSE_ROW_LIMIT} rows."
417
+ )
418
+ )
407
419
 
408
420
  _params = {}
409
421
  if params == 'null':
@@ -447,8 +459,8 @@ def get_pipe_data(
447
459
  detail="Omitted columns must be a JSON-encoded list.",
448
460
  )
449
461
 
450
- pipe = get_pipe(connector_keys, metric_key, location_key)
451
- if not is_pipe_registered(pipe, pipes(refresh=True)):
462
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
463
+ if not is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
452
464
  raise fastapi.HTTPException(
453
465
  status_code=409,
454
466
  detail="Pipe must be registered with the datetime column specified."
@@ -466,6 +478,7 @@ def get_pipe_data(
466
478
  begin=begin,
467
479
  end=end,
468
480
  params=_params,
481
+ limit=min(limit, MAX_RESPONSE_ROW_LIMIT),
469
482
  debug=debug,
470
483
  )
471
484
  if df is None:
@@ -474,12 +487,6 @@ def get_pipe_data(
474
487
  detail="Could not fetch data with the given parameters.",
475
488
  )
476
489
 
477
- ### NaN cannot be JSON-serialized.
478
- df = df.fillna(pd.NA)
479
- numeric_cols = get_numeric_cols(df)
480
- for col in numeric_cols:
481
- df[col] = df[col].apply(lambda x: f'{x:f}' if isinstance(x, Decimal) else x)
482
-
483
490
  json_content = to_json(df)
484
491
  return fastapi.Response(
485
492
  json_content,
@@ -492,6 +499,7 @@ def get_pipe_csv(
492
499
  connector_keys: str,
493
500
  metric_key: str,
494
501
  location_key: str,
502
+ instance_keys: Optional[str] = None,
495
503
  begin: Union[str, int, None] = None,
496
504
  end: Union[str, int, None] = None,
497
505
  params: Optional[str] = None,
@@ -500,16 +508,18 @@ def get_pipe_csv(
500
508
  ),
501
509
  ) -> str:
502
510
  """
503
- Get a Pipe's data as a CSV file. Optionally set query boundaries.
511
+ Get a pipe's data as a CSV file. Optionally set query boundaries.
504
512
  """
505
513
  if begin is not None:
506
514
  begin = (
507
- int(begin) if is_int(begin)
515
+ int(begin)
516
+ if is_int(begin)
508
517
  else dateutil_parser.parse(begin)
509
518
  )
510
519
  if end is not None:
511
520
  end = (
512
- int(end) if is_int(end)
521
+ int(end)
522
+ if is_int(end)
513
523
  else dateutil_parser.parse(end)
514
524
  )
515
525
 
@@ -520,7 +530,7 @@ def get_pipe_csv(
520
530
  import json
521
531
  try:
522
532
  _params = json.loads(params)
523
- except Exception as e:
533
+ except Exception:
524
534
  _params = None
525
535
 
526
536
  if not isinstance(_params, dict):
@@ -529,27 +539,33 @@ def get_pipe_csv(
529
539
  detail="Params must be a valid JSON-encoded dictionary.",
530
540
  )
531
541
 
532
- p = get_pipe(connector_keys, metric_key, location_key)
533
- if not is_pipe_registered(p, pipes(refresh=True)):
542
+ pipe = get_pipe(connector_keys, metric_key, location_key, instance_keys)
543
+ if not is_pipe_registered(pipe, pipes(instance_keys, refresh=True)):
534
544
  raise fastapi.HTTPException(
535
- status_code = 409,
536
- detail = "Pipe must be registered with the datetime column specified."
545
+ status_code=409,
546
+ detail="Pipe must be registered with the datetime column specified."
537
547
  )
538
548
 
539
- dt_col = p.columns.get('datetime', None)
549
+ dt_col = pipe.columns.get('datetime', None)
540
550
  if dt_col:
541
551
  if begin is None:
542
- begin = p.get_sync_time(round_down=False, newest=False)
552
+ begin = pipe.get_sync_time(round_down=False, newest=False)
543
553
  if end is None:
544
- end = p.get_sync_time(round_down=False, newest=True)
554
+ end = pipe.get_sync_time(round_down=False, newest=True)
555
+ if end is not None:
556
+ end += (
557
+ 1
558
+ if is_int(str(end))
559
+ else timedelta(minutes=1)
560
+ )
545
561
 
546
562
  bounds_text = (
547
563
  ('-' + str(begin) + '-' + str(end))
548
564
  if begin is not None and end is not None
549
565
  else ''
550
566
  )
551
- filename = p.target + bounds_text + '.csv'
552
- df = p.get_data(begin=begin, end=end, params=_params, debug=debug)
567
+ filename = pipe.target + bounds_text + '.csv'
568
+ df = pipe.get_data(begin=begin, end=end, params=_params, debug=debug)
553
569
  stream = io.StringIO()
554
570
  df.to_csv(stream, index=False)
555
571
  response = StreamingResponse(iter([stream.getvalue()]), media_type='text/csv')
@@ -562,14 +578,15 @@ def get_pipe_id(
562
578
  connector_keys: str,
563
579
  metric_key: str,
564
580
  location_key: str,
581
+ instance_keys: Optional[str] = None,
565
582
  curr_user = (
566
583
  fastapi.Depends(manager) if not no_auth else None
567
584
  ),
568
585
  ) -> Union[int, str]:
569
586
  """
570
- Get a Pipe's ID.
587
+ Get a pipe's ID.
571
588
  """
572
- pipe_id = get_pipe(connector_keys, metric_key, location_key).id
589
+ pipe_id = get_pipe(connector_keys, metric_key, location_key, instance_keys).get_id(debug=debug)
573
590
  if pipe_id is None:
574
591
  raise fastapi.HTTPException(status_code=404, detail="Pipe is not registered.")
575
592
  return pipe_id
@@ -577,18 +594,25 @@ def get_pipe_id(
577
594
 
578
595
  @app.get(
579
596
  pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/attributes',
580
- tags = ['Pipes']
597
+ tags=['Pipes']
581
598
  )
582
599
  def get_pipe_attributes(
583
600
  connector_keys: str,
584
601
  metric_key: str,
585
602
  location_key: str,
603
+ instance_keys: Optional[str] = None,
586
604
  curr_user=(
587
605
  fastapi.Depends(manager) if not no_auth else None
588
606
  ),
589
607
  ) -> Dict[str, Any]:
590
- """Get a Pipe's attributes."""
591
- return get_pipe(connector_keys, metric_key, location_key, refresh=True).attributes
608
+ """Get a pipe's attributes."""
609
+ return get_pipe(
610
+ connector_keys,
611
+ metric_key,
612
+ location_key,
613
+ instance_keys,
614
+ refresh=True,
615
+ ).attributes
592
616
 
593
617
 
594
618
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/exists', tags=['Pipes'])
@@ -596,24 +620,28 @@ def get_pipe_exists(
596
620
  connector_keys: str,
597
621
  metric_key: str,
598
622
  location_key: str,
623
+ instance_keys: Optional[str] = None,
599
624
  curr_user = (
600
625
  fastapi.Depends(manager) if not no_auth else None
601
626
  ),
602
627
  ) -> bool:
603
- """Determine whether a Pipe exists."""
604
- return get_pipe(connector_keys, metric_key, location_key).exists()
628
+ """Determine whether a pipe's target table exists."""
629
+ return get_pipe(connector_keys, metric_key, location_key, instance_keys).exists(debug=debug)
605
630
 
606
631
 
607
632
  @app.post(endpoints['metadata'], tags=['Pipes'])
608
633
  def create_metadata(
634
+ instance_keys: Optional[str] = None,
609
635
  curr_user = (
610
636
  fastapi.Depends(manager) if not no_auth else None
611
637
  ),
612
638
  ) -> bool:
613
- """Create Pipe metadata tables"""
614
- from meerschaum.connectors.sql.tables import get_tables
639
+ """Create pipe instance metadata tables."""
640
+ conn = get_api_connector(instance_keys)
641
+ if conn.type not in ('sql', 'api'):
642
+ return False
615
643
  try:
616
- tables = get_tables(mrsm_instance=get_api_connector(), debug=debug)
644
+ _ = get_tables(mrsm_instance=conn, debug=debug)
617
645
  except Exception as e:
618
646
  raise fastapi.HTTPException(status_code=500, detail=str(e))
619
647
  return True
@@ -624,24 +652,46 @@ def get_pipe_rowcount(
624
652
  connector_keys: str,
625
653
  metric_key: str,
626
654
  location_key: str,
655
+ instance_keys: Optional[str] = None,
627
656
  begin: Union[str, int, None] = None,
628
657
  end: Union[str, int, None] = None,
629
658
  params: Optional[Dict[str, Any]] = None,
659
+ remote: bool = False,
630
660
  curr_user = (
631
661
  fastapi.Depends(manager) if not no_auth else None
632
662
  ),
633
663
  ) -> int:
634
664
  """
635
665
  Return a pipe's rowcount.
666
+ See [`Pipe.get_rowcount()`](https://docs.meerschaum.io/meerschaum.html#Pipe.get_rowcount).
667
+
668
+ Parameters
669
+ ----------
670
+ begin: Union[str, int, None], default None
671
+ If provided, only count rows newer than or equal to `begin`.
672
+
673
+ end: Union[str, int, None], defaut None
674
+ If provided, only count rows older than `end`.
675
+
676
+ params: Optional[Dict[str, Any]], default None
677
+ If provided, only count rows which match the provided `params` dictionary.
678
+
679
+ remote: bool, default False
680
+ If `True`, return the rowcount for the fetch definition instead of the target table.
681
+
682
+ Returns
683
+ -------
684
+ The rowcount for a pipe's target table or fetch definition (if applicable).
636
685
  """
637
686
  if is_int(begin):
638
687
  begin = int(begin)
639
688
  if is_int(end):
640
689
  end = int(end)
641
- return get_pipe(connector_keys, metric_key, location_key).get_rowcount(
690
+ return get_pipe(connector_keys, metric_key, location_key, instance_keys).get_rowcount(
642
691
  begin=begin,
643
692
  end=end,
644
693
  params=params,
694
+ remote=remote,
645
695
  debug=debug,
646
696
  )
647
697
 
@@ -654,22 +704,24 @@ def get_pipe_columns_types(
654
704
  connector_keys: str,
655
705
  metric_key: str,
656
706
  location_key: str,
707
+ instance_keys: Optional[str] = None,
657
708
  curr_user=(
658
709
  fastapi.Depends(manager) if not no_auth else None
659
710
  ),
660
711
  ) -> Dict[str, str]:
661
712
  """
662
713
  Return a dictionary of column names and types.
714
+ See [`Pipe.dtypes`](https://meerschaum.io/reference/pipes/parameters/#dtypes) for supported types.
663
715
 
664
- ```
716
+ ```json
665
717
  {
666
718
  "dt": "datetime",
667
719
  "id": "int",
668
- "val": "float",
720
+ "val": "float"
669
721
  }
670
722
  ```
671
723
  """
672
- return get_pipe(connector_keys, metric_key, location_key).dtypes
724
+ return get_pipe(connector_keys, metric_key, location_key, instance_keys).dtypes
673
725
 
674
726
 
675
727
  @app.get(
@@ -680,14 +732,50 @@ def get_pipe_columns_indices(
680
732
  connector_keys: str,
681
733
  metric_key: str,
682
734
  location_key: str,
735
+ instance_keys: Optional[str] = None,
683
736
  curr_user=(
684
737
  fastapi.Depends(manager) if not no_auth else None
685
738
  ),
686
739
  ) -> Dict[str, List[Dict[str, str]]]:
687
740
  """
688
741
  Return a dictionary of column names and related indices.
742
+ See [`Pipe.get_columns_indices()`](https://docs.meerschaum.io/meerschaum.html#Pipe.get_columns_indices).
743
+
744
+ ```json
745
+ {
746
+ "datetime": [
747
+ {
748
+ "name": "plugin_stress_test_0_datetime_idx",
749
+ "type": "INDEX"
750
+ },
751
+ {
752
+ "name": "IX_plugin_stress_test_0_id_datetime",
753
+ "type": "INDEX"
754
+ },
755
+ {
756
+ "name": "UQ_plugin_stress_test_0_id_datetime",
757
+ "type": "INDEX"
758
+ }
759
+ ],
760
+ "id": [
761
+ {
762
+ "name": "IX_plugin_stress_test_0_id",
763
+ "type": "INDEX"
764
+ },
765
+ {
766
+ "name": "UQ_plugin_stress_test_0_id_datetime",
767
+ "type": "INDEX"
768
+ }
769
+ ]
770
+ }
771
+ ```
689
772
  """
690
- return get_pipe(connector_keys, metric_key, location_key).get_columns_indices()
773
+ return get_pipe(
774
+ connector_keys,
775
+ metric_key,
776
+ location_key,
777
+ instance_keys,
778
+ ).get_columns_indices(debug=debug)
691
779
 
692
780
 
693
781
  @app.get(
@@ -698,11 +786,19 @@ def get_pipe_index_names(
698
786
  connector_keys: str,
699
787
  metric_key: str,
700
788
  location_key: str,
789
+ instance_keys: Optional[str] = None,
701
790
  curr_user=(
702
791
  fastapi.Depends(manager) if not no_auth else None
703
792
  ),
704
793
  ) -> Dict[str, List[Dict[str, str]]]:
705
794
  """
706
795
  Return a dictionary of index keys and index names.
796
+
797
+ See [`Pipe.get_indices()`](https://docs.meerschaum.io/meerschaum.html#Pipe.get_indices).
707
798
  """
708
- return get_pipe(connector_keys, metric_key, location_key).get_indices()
799
+ return get_pipe(
800
+ connector_keys,
801
+ metric_key,
802
+ location_key,
803
+ instance_keys,
804
+ ).get_indices(debug=debug)