meerschaum 2.7.10__py3-none-any.whl → 2.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. meerschaum/_internal/arguments/_parser.py +17 -5
  2. meerschaum/actions/copy.py +3 -1
  3. meerschaum/actions/index.py +1 -1
  4. meerschaum/actions/show.py +7 -7
  5. meerschaum/actions/sync.py +5 -1
  6. meerschaum/actions/verify.py +14 -1
  7. meerschaum/api/__init__.py +77 -41
  8. meerschaum/api/_exceptions.py +18 -0
  9. meerschaum/api/dash/__init__.py +4 -2
  10. meerschaum/api/dash/callbacks/dashboard.py +30 -1
  11. meerschaum/api/dash/components.py +2 -2
  12. meerschaum/api/dash/webterm.py +23 -4
  13. meerschaum/api/models/_pipes.py +8 -8
  14. meerschaum/api/resources/static/css/dash.css +2 -2
  15. meerschaum/api/resources/templates/termpage.html +5 -1
  16. meerschaum/api/routes/__init__.py +15 -12
  17. meerschaum/api/routes/_connectors.py +30 -28
  18. meerschaum/api/routes/_index.py +16 -7
  19. meerschaum/api/routes/_misc.py +30 -22
  20. meerschaum/api/routes/_pipes.py +244 -148
  21. meerschaum/api/routes/_plugins.py +58 -47
  22. meerschaum/api/routes/_users.py +39 -31
  23. meerschaum/api/routes/_version.py +8 -10
  24. meerschaum/config/_default.py +10 -0
  25. meerschaum/config/_version.py +1 -1
  26. meerschaum/config/static/__init__.py +4 -1
  27. meerschaum/connectors/api/_APIConnector.py +4 -3
  28. meerschaum/connectors/api/_login.py +21 -17
  29. meerschaum/connectors/api/_pipes.py +1 -0
  30. meerschaum/connectors/api/_request.py +9 -10
  31. meerschaum/connectors/sql/_cli.py +11 -3
  32. meerschaum/connectors/sql/_instance.py +1 -1
  33. meerschaum/connectors/sql/_pipes.py +77 -57
  34. meerschaum/connectors/sql/_sql.py +26 -9
  35. meerschaum/core/Pipe/__init__.py +2 -0
  36. meerschaum/core/Pipe/_attributes.py +13 -2
  37. meerschaum/core/Pipe/_data.py +85 -0
  38. meerschaum/core/Pipe/_deduplicate.py +6 -8
  39. meerschaum/core/Pipe/_sync.py +63 -30
  40. meerschaum/core/Pipe/_verify.py +242 -77
  41. meerschaum/core/User/__init__.py +2 -6
  42. meerschaum/jobs/_Job.py +1 -1
  43. meerschaum/jobs/__init__.py +15 -0
  44. meerschaum/utils/dataframe.py +2 -0
  45. meerschaum/utils/dtypes/sql.py +26 -0
  46. meerschaum/utils/formatting/_pipes.py +1 -1
  47. meerschaum/utils/misc.py +11 -7
  48. meerschaum/utils/packages/_packages.py +1 -1
  49. meerschaum/utils/sql.py +6 -2
  50. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/METADATA +4 -4
  51. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/RECORD +57 -56
  52. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/LICENSE +0 -0
  53. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/NOTICE +0 -0
  54. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/WHEEL +0 -0
  55. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/entry_points.txt +0 -0
  56. {meerschaum-2.7.10.dist-info → meerschaum-2.8.0.dist-info}/top_level.txt +0 -0
  57. {meerschaum-2.7.10.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)