meerschaum 3.0.0rc2__py3-none-any.whl → 3.0.0rc4__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 (47) hide show
  1. meerschaum/_internal/shell/Shell.py +5 -4
  2. meerschaum/actions/bootstrap.py +1 -1
  3. meerschaum/actions/edit.py +6 -3
  4. meerschaum/actions/start.py +1 -1
  5. meerschaum/api/_events.py +5 -0
  6. meerschaum/api/dash/callbacks/__init__.py +1 -0
  7. meerschaum/api/dash/callbacks/dashboard.py +93 -115
  8. meerschaum/api/dash/callbacks/jobs.py +11 -5
  9. meerschaum/api/dash/callbacks/pipes.py +194 -14
  10. meerschaum/api/dash/callbacks/settings/__init__.py +0 -1
  11. meerschaum/api/dash/callbacks/{settings/tokens.py → tokens.py} +3 -2
  12. meerschaum/api/dash/components.py +6 -7
  13. meerschaum/api/dash/jobs.py +1 -1
  14. meerschaum/api/dash/keys.py +17 -1
  15. meerschaum/api/dash/pages/__init__.py +2 -1
  16. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  17. meerschaum/api/dash/pages/pipes.py +16 -5
  18. meerschaum/api/dash/pages/settings/__init__.py +0 -1
  19. meerschaum/api/dash/pages/{settings/tokens.py → tokens.py} +6 -8
  20. meerschaum/api/dash/pipes.py +219 -3
  21. meerschaum/api/dash/tokens.py +27 -30
  22. meerschaum/config/_default.py +5 -4
  23. meerschaum/config/_paths.py +1 -0
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/connectors/instance/_tokens.py +6 -2
  26. meerschaum/connectors/sql/_SQLConnector.py +14 -0
  27. meerschaum/connectors/sql/_pipes.py +63 -23
  28. meerschaum/connectors/sql/tables/__init__.py +254 -122
  29. meerschaum/core/Pipe/__init__.py +17 -1
  30. meerschaum/core/Pipe/_attributes.py +5 -2
  31. meerschaum/core/Token/_Token.py +1 -1
  32. meerschaum/plugins/bootstrap.py +508 -3
  33. meerschaum/utils/_get_pipes.py +31 -5
  34. meerschaum/utils/dataframe.py +8 -2
  35. meerschaum/utils/dtypes/__init__.py +2 -3
  36. meerschaum/utils/dtypes/sql.py +11 -11
  37. meerschaum/utils/formatting/_pprint.py +1 -0
  38. meerschaum/utils/pipes.py +6 -2
  39. meerschaum/utils/sql.py +1 -1
  40. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/METADATA +1 -1
  41. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/RECORD +47 -47
  42. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/WHEEL +0 -0
  43. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/entry_points.txt +0 -0
  44. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/licenses/LICENSE +0 -0
  45. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/licenses/NOTICE +0 -0
  46. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/top_level.txt +0 -0
  47. {meerschaum-3.0.0rc2.dist-info → meerschaum-3.0.0rc4.dist-info}/zip-safe +0 -0
@@ -16,7 +16,8 @@ from meerschaum.utils.formatting._shell import clear_screen
16
16
 
17
17
  FEATURE_CHOICES: Dict[str, str] = {
18
18
  'fetch' : 'Fetch data\n (e.g. extracting from a remote API)\n',
19
- 'connector': 'Custom connector\n (e.g. manage credentials)\n',
19
+ 'connector': 'Connector\n (fetch data & manage credentials)\n',
20
+ 'instance-connector': 'Instance connector\n (implement the pipes interface)\n',
20
21
  'action' : 'New actions\n (e.g. `mrsm sing song`)\n',
21
22
  'api' : 'New API endpoints\n (e.g. `POST /my/new/endpoint`)\n',
22
23
  'web' : 'New web console page\n (e.g. `/dash/my-web-app`)\n',
@@ -25,6 +26,7 @@ FEATURE_CHOICES: Dict[str, str] = {
25
26
  IMPORTS_LINES: Dict[str, str] = {
26
27
  'stdlib': (
27
28
  "from datetime import datetime, timedelta, timezone\n"
29
+ "from typing import Any, Union, List, Dict\n"
28
30
  ),
29
31
  'default': (
30
32
  "import meerschaum as mrsm\n"
@@ -33,6 +35,9 @@ IMPORTS_LINES: Dict[str, str] = {
33
35
  'connector': (
34
36
  "from meerschaum.connectors import Connector, make_connector\n"
35
37
  ),
38
+ 'instance-connector': (
39
+ "from meerschaum.connectors import InstanceConnector, make_connector\n"
40
+ ),
36
41
  'action': (
37
42
  "from meerschaum.actions import make_action\n"
38
43
  ),
@@ -106,6 +111,501 @@ FEATURE_LINES: Dict[str, str] = {
106
111
  " # populate docs with dictionaries (rows).\n"
107
112
  " return docs\n\n\n"
108
113
  ),
114
+ 'instance-connector': (
115
+ "@make_connector\n"
116
+ "class {plugin_name_capitalized}Connector(InstanceConnector):\n"
117
+ " \"\"\"Implement '{plugin_name_lower}' connectors.\"\"\"\n\n"
118
+ " REQUIRED_ATTRIBUTES: list[str] = []\n"
119
+ "\n"
120
+ " def fetch(\n"
121
+ " self,\n"
122
+ " pipe: mrsm.Pipe,\n"
123
+ " begin: datetime | None = None,\n"
124
+ " end: datetime | None = None,\n"
125
+ " **kwargs\n"
126
+ " ):\n"
127
+ " \"\"\"Return or yield dataframes.\"\"\"\n"
128
+ " docs = []\n"
129
+ " # populate docs with dictionaries (rows).\n"
130
+ " return docs\n"
131
+ """
132
+ def register_pipe(
133
+ self,
134
+ pipe: mrsm.Pipe,
135
+ debug: bool = False,
136
+ **kwargs: Any
137
+ ) -> mrsm.SuccessTuple:
138
+ \"\"\"
139
+ Insert the pipe's attributes into the internal `pipes` table.
140
+
141
+ Parameters
142
+ ----------
143
+ pipe: mrsm.Pipe
144
+ The pipe to be registered.
145
+
146
+ Returns
147
+ -------
148
+ A `SuccessTuple` of the result.
149
+ \"\"\"
150
+ attributes = {{
151
+ 'connector_keys': str(pipe.connector_keys),
152
+ 'metric_key': str(pipe.metric_key),
153
+ 'location_key': str(pipe.location_key),
154
+ 'parameters': pipe._attributes.get('parameters', {{}}),
155
+ }}
156
+
157
+ ### TODO insert `attributes` as a row in the pipes table.
158
+ # self.pipes_collection.insert_one(attributes)
159
+
160
+ return True, \"Success\"
161
+
162
+ def get_pipe_attributes(
163
+ self,
164
+ pipe: mrsm.Pipe,
165
+ debug: bool = False,
166
+ **kwargs: Any
167
+ ) -> dict[str, Any]:
168
+ \"\"\"
169
+ Return the pipe's document from the internal `pipes` collection.
170
+
171
+ Parameters
172
+ ----------
173
+ pipe: mrsm.Pipe
174
+ The pipe whose attributes should be retrieved.
175
+
176
+ Returns
177
+ -------
178
+ The document that matches the keys of the pipe.
179
+ \"\"\"
180
+ query = {{
181
+ 'connector_keys': str(pipe.connector_keys),
182
+ 'metric_key': str(pipe.metric_key),
183
+ 'location_key': str(pipe.location_key),
184
+ }}
185
+ ### TODO query the `pipes` table either using these keys or `get_pipe_id()`.
186
+ result = {{}}
187
+ # result = self.pipes_collection.find_one(query) or {{}}
188
+ return result
189
+
190
+ def get_pipe_id(
191
+ self,
192
+ pipe: mrsm.Pipe,
193
+ debug: bool = False,
194
+ **kwargs: Any
195
+ ) -> str | int | None:
196
+ \"\"\"
197
+ Return the ID for the pipe if it exists.
198
+
199
+ Parameters
200
+ ----------
201
+ pipe: mrsm.Pipe
202
+ The pipe whose ID to fetch.
203
+
204
+ Returns
205
+ -------
206
+ The ID for the pipe or `None`.
207
+ \"\"\"
208
+ query = {{
209
+ 'connector_keys': str(pipe.connector_keys),
210
+ 'metric_key': str(pipe.metric_key),
211
+ 'location_key': str(pipe.location_key),
212
+ }}
213
+ ### TODO fetch the ID mapped to this pipe.
214
+ return None
215
+
216
+ def edit_pipe(
217
+ self,
218
+ pipe: mrsm.Pipe,
219
+ debug: bool = False,
220
+ **kwargs: Any
221
+ ) -> mrsm.SuccessTuple:
222
+ \"\"\"
223
+ Edit the attributes of the pipe.
224
+
225
+ Parameters
226
+ ----------
227
+ pipe: mrsm.Pipe
228
+ The pipe whose in-memory parameters must be persisted.
229
+
230
+ Returns
231
+ -------
232
+ A `SuccessTuple` indicating success.
233
+ \"\"\"
234
+ query = {{
235
+ 'connector_keys': str(pipe.connector_keys),
236
+ 'metric_key': str(pipe.metric_key),
237
+ 'location_key': str(pipe.location_key),
238
+ }}
239
+ pipe_parameters = pipe._attributes.get('parameters', {{}})
240
+ ### TODO Update the row with new parameters.
241
+ # self.pipes_collection.update_one(query, {{'$set': {{'parameters': pipe_parameters}}}})
242
+ return True, "Success"
243
+
244
+ def delete_pipe(
245
+ self,
246
+ pipe: mrsm.Pipe,
247
+ debug: bool = False,
248
+ **kwargs: Any
249
+ ) -> mrsm.SuccessTuple:
250
+ \"\"\"
251
+ Delete a pipe's registration from the `pipes` collection.
252
+
253
+ Parameters
254
+ ----------
255
+ pipe: mrsm.Pipe
256
+ The pipe to be deleted.
257
+
258
+ Returns
259
+ -------
260
+ A `SuccessTuple` indicating success.
261
+ \"\"\"
262
+ ### TODO Delete the pipe's row from the pipes table.
263
+ # self.pipes_collection.delete_one({{'_id': pipe_id}})
264
+ return True, "Success"
265
+
266
+ def fetch_pipes_keys(
267
+ self,
268
+ connector_keys: list[str] | None = None,
269
+ metric_keys: list[str] | None = None,
270
+ location_keys: list[str] | None = None,
271
+ tags: list[str] | None = None,
272
+ debug: bool = False,
273
+ **kwargs: Any
274
+ ) -> list[tuple[str, str, str]]:
275
+ \"\"\"
276
+ Return a list of tuples for the registered pipes' keys according to the provided filters.
277
+
278
+ Parameters
279
+ ----------
280
+ connector_keys: list[str] | None, default None
281
+ The keys passed via `-c`.
282
+
283
+ metric_keys: list[str] | None, default None
284
+ The keys passed via `-m`.
285
+
286
+ location_keys: list[str] | None, default None
287
+ The keys passed via `-l`.
288
+
289
+ tags: List[str] | None, default None
290
+ Tags passed via `--tags` which are stored under `parameters:tags`.
291
+
292
+ Returns
293
+ -------
294
+ A list of connector, metric, and location keys in tuples.
295
+ You may return the string "None" for location keys in place of nulls.
296
+
297
+ Examples
298
+ --------
299
+ >>> import meerschaum as mrsm
300
+ >>> conn = mrsm.get_connector('example:demo')
301
+ >>>
302
+ >>> pipe_a = mrsm.Pipe('a', 'demo', tags=['foo'], instance=conn)
303
+ >>> pipe_b = mrsm.Pipe('b', 'demo', tags=['bar'], instance=conn)
304
+ >>> pipe_a.register()
305
+ >>> pipe_b.register()
306
+ >>>
307
+ >>> conn.fetch_pipes_keys(['a', 'b'])
308
+ [('a', 'demo', 'None'), ('b', 'demo', 'None')]
309
+ >>> conn.fetch_pipes_keys(metric_keys=['demo'])
310
+ [('a', 'demo', 'None'), ('b', 'demo', 'None')]
311
+ >>> conn.fetch_pipes_keys(tags=['foo'])
312
+ [('a', 'demo', 'None')]
313
+ >>> conn.fetch_pipes_keys(location_keys=[None])
314
+ [('a', 'demo', 'None'), ('b', 'demo', 'None')]
315
+
316
+ \"\"\"
317
+ from meerschaum.utils.misc import separate_negation_values
318
+
319
+ in_ck, nin_ck = separate_negation_values([str(val) for val in (connector_keys or [])])
320
+ in_mk, nin_mk = separate_negation_values([str(val) for val in (metric_keys or [])])
321
+ in_lk, nin_lk = separate_negation_values([str(val) for val in (location_keys or [])])
322
+ in_tags, nin_tags = separate_negation_values([str(val) for val in (tags or [])])
323
+
324
+ ### TODO build a query like so, only including clauses if the given list is not empty.
325
+ ### The `tags` clause is an OR ("?|"), meaning any of the tags may match.
326
+ ###
327
+ ###
328
+ ### SELECT connector_keys, metric_key, location_key
329
+ ### FROM pipes
330
+ ### WHERE connector_keys IN ({{in_ck}})
331
+ ### AND connector_keys NOT IN ({{nin_ck}})
332
+ ### AND metric_key IN ({{in_mk}})
333
+ ### AND metric_key NOT IN ({{nin_mk}})
334
+ ### AND location_key IN ({{in_lk}})
335
+ ### AND location_key NOT IN ({{nin_lk}})
336
+ ### AND (parameters->'tags')::JSONB ?| ARRAY[{{tags}}]
337
+ ### AND NOT (parameters->'tags')::JSONB ?| ARRAY[{{nin_tags}}]
338
+ return []
339
+
340
+ def pipe_exists(
341
+ self,
342
+ pipe: mrsm.Pipe,
343
+ debug: bool = False,
344
+ **kwargs: Any
345
+ ) -> bool:
346
+ \"\"\"
347
+ Check whether a pipe's target table exists.
348
+
349
+ Parameters
350
+ ----------
351
+ pipe: mrsm.Pipe
352
+ The pipe to check whether its table exists.
353
+
354
+ Returns
355
+ -------
356
+ A `bool` indicating the table exists.
357
+ \"\"\"
358
+ table_name = pipe.target
359
+ ### TODO write a query to determine the existence of `table_name`.
360
+ table_exists = False
361
+ return table_exists
362
+
363
+ def drop_pipe(
364
+ self,
365
+ pipe: mrsm.Pipe,
366
+ debug: bool = False,
367
+ **kwargs: Any
368
+ ) -> mrsm.SuccessTuple:
369
+ \"\"\"
370
+ Drop a pipe's collection if it exists.
371
+
372
+ Parameters
373
+ ----------
374
+ pipe: mrsm.Pipe
375
+ The pipe to be dropped.
376
+
377
+ Returns
378
+ -------
379
+ A `SuccessTuple` indicating success.
380
+ \"\"\"
381
+ ### TODO write a query to drop `table_name`.
382
+ table_name = pipe.target
383
+ return True, \"Success\"
384
+
385
+ def sync_pipe(
386
+ self,
387
+ pipe: mrsm.Pipe,
388
+ df: 'pd.DataFrame',
389
+ debug: bool = False,
390
+ **kwargs: Any
391
+ ) -> mrsm.SuccessTuple:
392
+ \"\"\"
393
+ Upsert new documents into the pipe's target table.
394
+
395
+ Parameters
396
+ ----------
397
+ pipe: mrsm.Pipe
398
+ The pipe to which the data should be upserted.
399
+
400
+ df: pd.DataFrame
401
+ The data to be synced.
402
+
403
+ Returns
404
+ -------
405
+ A `SuccessTuple` indicating success.
406
+ \"\"\"
407
+ ### TODO Write the upsert logic for the target table.
408
+ ### `pipe.filter_existing()` is provided for your convenience to
409
+ ### remove duplicates and separate inserts from updates.
410
+
411
+ unseen_df, update_df, delta_df = pipe.filter_existing(df, debug=debug)
412
+ return True, \"Success\"
413
+
414
+ def clear_pipe(
415
+ self,
416
+ pipe: mrsm.Pipe,
417
+ begin: datetime | int | None = None,
418
+ end: datetime | int | None = None,
419
+ params: dict[str, Any] | None = None,
420
+ debug: bool = False,
421
+ ) -> mrsm.SuccessTuple:
422
+ \"\"\"
423
+ Delete rows within `begin`, `end`, and `params`.
424
+
425
+ Parameters
426
+ ----------
427
+ pipe: mrsm.Pipe
428
+ The pipe whose rows to clear.
429
+
430
+ begin: datetime | int | None, default None
431
+ If provided, remove rows >= `begin`.
432
+
433
+ end: datetime | int | None, default None
434
+ If provided, remove rows < `end`.
435
+
436
+ params: dict[str, Any] | None, default None
437
+ If provided, only remove rows which match the `params` filter.
438
+
439
+ Returns
440
+ -------
441
+ A `SuccessTuple` indicating success.
442
+ \"\"\"
443
+ ### TODO Write a query to remove rows which match `begin`, `end`, and `params`.
444
+ return True, \"Success\"
445
+
446
+ def get_pipe_data(
447
+ self,
448
+ pipe: mrsm.Pipe,
449
+ select_columns: list[str] | None = None,
450
+ omit_columns: list[str] | None = None,
451
+ begin: datetime | int | None = None,
452
+ end: datetime | int | None = None,
453
+ params: dict[str, Any] | None = None,
454
+ debug: bool = False,
455
+ **kwargs: Any
456
+ ) -> Union['pd.DataFrame', None]:
457
+ \"\"\"
458
+ Query a pipe's target table and return the DataFrame.
459
+
460
+ Parameters
461
+ ----------
462
+ pipe: mrsm.Pipe
463
+ The pipe with the target table from which to read.
464
+
465
+ select_columns: list[str] | None, default None
466
+ If provided, only select these given columns.
467
+ Otherwise select all available columns (i.e. `SELECT *`).
468
+
469
+ omit_columns: list[str] | None, default None
470
+ If provided, remove these columns from the selection.
471
+
472
+ begin: datetime | int | None, default None
473
+ The earliest `datetime` value to search from (inclusive).
474
+
475
+ end: datetime | int | None, default None
476
+ The lastest `datetime` value to search from (exclusive).
477
+
478
+ params: dict[str | str] | None, default None
479
+ Additional filters to apply to the query.
480
+
481
+ Returns
482
+ -------
483
+ The target table's data as a DataFrame.
484
+ \"\"\"
485
+ if not pipe.exists(debug=debug):
486
+ return None
487
+
488
+ table_name = pipe.target
489
+ dt_col = pipe.columns.get(\"datetime\", None)
490
+
491
+ ### TODO Write a query to fetch from `table_name`
492
+ ### and apply the filters `begin`, `end`, and `params`.
493
+ ###
494
+ ### To improve performance, add logic to only read from
495
+ ### `select_columns` and not `omit_columns` (if provided).
496
+ ###
497
+ ### SELECT {{', '.join(cols_to_select)}}
498
+ ### FROM \"{{table_name}}\"
499
+ ### WHERE \"{{dt_col}}\" >= '{{begin}}'
500
+ ### AND \"{{dt_col}}\" < '{{end}}'
501
+
502
+ ### The function `parse_df_datetimes()` is a convenience function
503
+ ### to cast a list of dictionaries into a DataFrame and convert datetime columns.
504
+ from meerschaum.utils.dataframe import parse_df_datetimes
505
+ rows = []
506
+ return parse_df_datetimes(rows)
507
+
508
+ def get_sync_time(
509
+ self,
510
+ pipe: mrsm.Pipe,
511
+ params: dict[str, Any] | None = None,
512
+ newest: bool = True,
513
+ debug: bool = False,
514
+ **kwargs: Any
515
+ ) -> datetime | int | None:
516
+ \"\"\"
517
+ Return the most recent value for the `datetime` axis.
518
+
519
+ Parameters
520
+ ----------
521
+ pipe: mrsm.Pipe
522
+ The pipe whose collection contains documents.
523
+
524
+ params: dict[str, Any] | None, default None
525
+ Filter certain parameters when determining the sync time.
526
+
527
+ newest: bool, default True
528
+ If `True`, return the maximum value for the column.
529
+
530
+ Returns
531
+ -------
532
+ The largest `datetime` or `int` value of the `datetime` axis.
533
+ \"\"\"
534
+ ### TODO write a query to get the largest value for `dt_col`.
535
+ ### If `newest` is `False`, return the smallest value.
536
+ ### Apply the `params` filter in case of multiplexing.
537
+ return None
538
+
539
+ def get_pipe_columns_types(
540
+ self,
541
+ pipe: mrsm.Pipe,
542
+ debug: bool = False,
543
+ **kwargs: Any
544
+ ) -> dict[str, str]:
545
+ \"\"\"
546
+ Return the data types for the columns in the target table for data type enforcement.
547
+
548
+ Parameters
549
+ ----------
550
+ pipe: mrsm.Pipe
551
+ The pipe whose target table contains columns and data types.
552
+
553
+ Returns
554
+ -------
555
+ A dictionary mapping columns to data types.
556
+ \"\"\"
557
+ table_name = pipe.target
558
+ ### TODO write a query to fetch the columns contained in `table_name`.
559
+ columns_types = {{}}
560
+
561
+ ### Return a dictionary mapping the columns
562
+ ### to their Pandas dtypes, e.g.:
563
+ ### `{{'foo': 'int64'`}}`
564
+ ### or to SQL-style dtypes, e.g.:
565
+ ### `{{'bar': 'INT'}}`
566
+ return columns_types
567
+
568
+ def get_pipe_rowcount(
569
+ self,
570
+ pipe: mrsm.Pipe,
571
+ begin: datetime | int | None = None,
572
+ end: datetime | int | None = None,
573
+ params: dict[str, Any] | None = None,
574
+ remote: bool = False,
575
+ debug: bool = False,
576
+ **kwargs: Any
577
+ ) -> int:
578
+ \"\"\"
579
+ Return the rowcount for the pipe's table.
580
+
581
+ Parameters
582
+ ----------
583
+ pipe: mrsm.Pipe
584
+ The pipe whose table should be counted.
585
+
586
+ begin: datetime | int | None, default None
587
+ If provided, only count rows >= `begin`.
588
+
589
+ end: datetime | int | None, default None
590
+ If provided, only count rows < `end`.
591
+
592
+ params: dict[str, Any] | None
593
+ If provided, only count rows othat match the `params` filter.
594
+
595
+ remote: bool, default False
596
+ If `True`, return the rowcount for the pipe's fetch definition.
597
+ In this case, `self` refers to `Pipe.connector`, not `Pipe.instance_connector`.
598
+
599
+ Returns
600
+ -------
601
+ The rowcount for this pipe's table according the given parameters.
602
+ \"\"\"
603
+ ### TODO write a query to count how many rows exist in `table_name` according to the filters.
604
+ table_name = pipe.target
605
+ count = 0
606
+ return count
607
+ """
608
+ ),
109
609
  'action': (
110
610
  "@make_action\n"
111
611
  "def {action_name}(**kwargs) -> mrsm.SuccessTuple:\n"
@@ -212,8 +712,10 @@ def bootstrap_plugin(
212
712
  body_text += FEATURE_LINES['header'].format(**plugin_labels)
213
713
  body_text += IMPORTS_LINES['stdlib'].format(**plugin_labels)
214
714
  body_text += IMPORTS_LINES['default'].format(**plugin_labels)
215
- if 'connector' in features:
715
+ if 'connector' in features and 'instance-connector' not in features:
216
716
  body_text += IMPORTS_LINES['connector'].format(**plugin_labels)
717
+ if 'instance-connector' in features:
718
+ body_text += IMPORTS_LINES['instance-connector'].format(**plugin_labels)
217
719
  if 'action' in features:
218
720
  body_text += IMPORTS_LINES['action'].format(**plugin_labels)
219
721
  if 'api' in features and 'web' in features:
@@ -231,9 +733,12 @@ def bootstrap_plugin(
231
733
  body_text += FEATURE_LINES['register'].format(**plugin_labels)
232
734
  body_text += FEATURE_LINES['fetch'].format(**plugin_labels)
233
735
 
234
- if 'connector' in features:
736
+ if 'connector' in features and 'instance-connector' not in features:
235
737
  body_text += FEATURE_LINES['connector'].format(**plugin_labels)
236
738
 
739
+ if 'instance-connector' in features:
740
+ body_text += FEATURE_LINES['instance-connector'].format(**plugin_labels)
741
+
237
742
  if 'action' in features:
238
743
  body_text += FEATURE_LINES['action'].format(**plugin_labels)
239
744
 
@@ -128,11 +128,12 @@ def get_pipes(
128
128
  ```
129
129
  """
130
130
 
131
+ import json
132
+ from collections import defaultdict
131
133
  from meerschaum.config import get_config
132
134
  from meerschaum.utils.warnings import error
133
135
  from meerschaum.utils.misc import filter_keywords
134
136
  from meerschaum.utils.pool import get_pool
135
- from collections import defaultdict
136
137
 
137
138
  if connector_keys is None:
138
139
  connector_keys = []
@@ -188,25 +189,48 @@ def get_pipes(
188
189
  debug = debug
189
190
  )
190
191
  if result is None:
191
- error(f"Unable to build pipes!")
192
+ error("Unable to build pipes!")
192
193
 
193
194
  ### Populate the `pipes` dictionary with Pipes based on the keys
194
195
  ### obtained from the chosen `method`.
195
196
  from meerschaum import Pipe
196
197
  pipes = {}
197
- for ck, mk, lk in result:
198
+ for keys_tuple in result:
199
+ ck, mk, lk = keys_tuple[0], keys_tuple[1], keys_tuple[2]
200
+ pipe_tags_or_parameters = keys_tuple[3] if len(keys_tuple) == 4 else None
201
+ pipe_parameters = (
202
+ pipe_tags_or_parameters
203
+ if isinstance(pipe_tags_or_parameters, (dict, str))
204
+ else None
205
+ )
206
+ if isinstance(pipe_parameters, str):
207
+ pipe_parameters = json.loads(pipe_parameters)
208
+ pipe_tags = (
209
+ pipe_tags_or_parameters
210
+ if isinstance(pipe_tags_or_parameters, list)
211
+ else (
212
+ pipe_tags_or_parameters.get('tags', None)
213
+ if isinstance(pipe_tags_or_parameters, dict)
214
+ else None
215
+ )
216
+ )
217
+
198
218
  if ck not in pipes:
199
219
  pipes[ck] = {}
200
220
 
201
221
  if mk not in pipes[ck]:
202
222
  pipes[ck][mk] = {}
203
223
 
204
- pipes[ck][mk][lk] = Pipe(
224
+ pipe = Pipe(
205
225
  ck, mk, lk,
206
226
  mrsm_instance = connector,
227
+ parameters = pipe_parameters,
228
+ tags = pipe_tags,
207
229
  debug = debug,
208
230
  **filter_keywords(Pipe, **kw)
209
231
  )
232
+ pipe.__dict__['_tags'] = pipe_tags
233
+ pipes[ck][mk][lk] = pipe
210
234
 
211
235
  if not as_list and not as_tags_dict:
212
236
  return pipes
@@ -218,7 +242,9 @@ def get_pipes(
218
242
 
219
243
  pool = get_pool(workers=(workers if connector.IS_THREAD_SAFE else 1))
220
244
  def gather_pipe_tags(pipe: mrsm.Pipe) -> Tuple[mrsm.Pipe, List[str]]:
221
- return pipe, (pipe.tags or [])
245
+ _tags = pipe.__dict__.get('_tags', None)
246
+ gathered_tags = _tags if _tags is not None else pipe.tags
247
+ return pipe, (gathered_tags or [])
222
248
 
223
249
  tags_pipes = defaultdict(lambda: [])
224
250
  pipes_tags = dict(pool.map(gather_pipe_tags, pipes_list))
@@ -1508,7 +1508,7 @@ def enforce_dtypes(
1508
1508
  )
1509
1509
  )
1510
1510
 
1511
- if debug:
1511
+ if debug and (explicitly_numeric or df_numeric_cols or mixed_numeric_types):
1512
1512
  from meerschaum.utils.formatting import make_header
1513
1513
  msg = (
1514
1514
  make_header(f"Coercing column '{col}' to numeric:", left_pad=0)
@@ -1517,7 +1517,13 @@ def enforce_dtypes(
1517
1517
  + f" Current type: {typ if col not in df_numeric_cols else 'Decimal'}"
1518
1518
  + ("\n Column is explicitly numeric." if explicitly_numeric else "")
1519
1519
  ) if cast_to_numeric else (
1520
- f"Will not coerce column '{col}' to numeric."
1520
+ f"Will not coerce column '{col}' to numeric.\n"
1521
+ f" Numeric columns in dataframe: {df_numeric_cols}\n"
1522
+ f" Mixed numeric types: {mixed_numeric_types}\n"
1523
+ f" Explicitly float: {explicitly_float}\n"
1524
+ f" Explicitly int: {explicitly_int}\n"
1525
+ f" All NaN: {all_nan}\n"
1526
+ f" Coerce numeric: {coerce_numeric}"
1521
1527
  )
1522
1528
  dprint(msg)
1523
1529
 
@@ -233,8 +233,8 @@ def are_dtypes_equal(
233
233
  return True
234
234
 
235
235
  date_dtypes = (
236
- 'date', 'date32[pyarrow]', 'date32[day][pyarrow]',
237
- 'date64[pyarrow]', 'date64[ms][pyarrow]',
236
+ 'date', 'date32', 'date32[pyarrow]', 'date32[day][pyarrow]',
237
+ 'date64', 'date64[pyarrow]', 'date64[ms][pyarrow]',
238
238
  )
239
239
  if ldtype in date_dtypes and rdtype in date_dtypes:
240
240
  return True
@@ -1141,7 +1141,6 @@ def round_time(
1141
1141
  ) -> datetime:
1142
1142
  """
1143
1143
  Round a datetime object to a multiple of a timedelta.
1144
- http://stackoverflow.com/questions/3463930/how-to-round-the-minute-of-a-datetime-object-python
1145
1144
 
1146
1145
  Parameters
1147
1146
  ----------