meerschaum 2.1.4__py3-none-any.whl → 2.1.6__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 (38) hide show
  1. meerschaum/__main__.py +1 -1
  2. meerschaum/_internal/arguments/_parser.py +5 -4
  3. meerschaum/_internal/docs/index.py +2 -2
  4. meerschaum/_internal/term/TermPageHandler.py +2 -2
  5. meerschaum/actions/register.py +5 -1
  6. meerschaum/actions/show.py +12 -1
  7. meerschaum/actions/sync.py +49 -21
  8. meerschaum/actions/tag.py +101 -0
  9. meerschaum/api/__init__.py +1 -1
  10. meerschaum/api/dash/callbacks/dashboard.py +1 -1
  11. meerschaum/api/routes/_login.py +4 -3
  12. meerschaum/config/_formatting.py +1 -1
  13. meerschaum/config/_version.py +1 -1
  14. meerschaum/connectors/api/APIConnector.py +6 -2
  15. meerschaum/connectors/sql/_pipes.py +43 -30
  16. meerschaum/connectors/sql/tables/__init__.py +0 -16
  17. meerschaum/core/Pipe/__init__.py +1 -2
  18. meerschaum/core/Pipe/_data.py +5 -4
  19. meerschaum/utils/__init__.py +3 -1
  20. meerschaum/utils/{get_pipes.py → _get_pipes.py} +5 -16
  21. meerschaum/utils/daemon/Daemon.py +4 -2
  22. meerschaum/utils/dataframe.py +3 -3
  23. meerschaum/utils/interactive.py +2 -16
  24. meerschaum/utils/misc.py +27 -28
  25. meerschaum/utils/packages/__init__.py +7 -1
  26. meerschaum/utils/packages/_packages.py +0 -1
  27. meerschaum/utils/pool.py +14 -17
  28. meerschaum/utils/sql.py +41 -19
  29. meerschaum/utils/typing.py +11 -0
  30. meerschaum/utils/venv/__init__.py +2 -1
  31. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/METADATA +2 -3
  32. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/RECORD +38 -37
  33. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/WHEEL +1 -1
  34. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/LICENSE +0 -0
  35. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/NOTICE +0 -0
  36. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/entry_points.txt +0 -0
  37. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/top_level.txt +0 -0
  38. {meerschaum-2.1.4.dist-info → meerschaum-2.1.6.dist-info}/zip-safe +0 -0
meerschaum/__main__.py CHANGED
@@ -76,7 +76,7 @@ def _close_pools():
76
76
  """Close multiprocessing pools before exiting."""
77
77
  ### Final step: close global pools.
78
78
  from meerschaum.utils.pool import get_pools
79
- for class_name, pool in get_pools().items():
79
+ for pool in get_pools().values():
80
80
  try:
81
81
  pool.close()
82
82
  pool.terminate()
@@ -8,7 +8,9 @@ This module creates the argparse Parser.
8
8
 
9
9
  from __future__ import annotations
10
10
  import sys
11
- import argparse, json
11
+ import argparse
12
+ import json
13
+ from datetime import datetime, timedelta, timezone
12
14
  from meerschaum.utils.typing import Union, Dict, List, Any, Tuple, Callable
13
15
  from meerschaum.utils.misc import string_to_dict
14
16
  from meerschaum.utils.warnings import error
@@ -35,19 +37,18 @@ class ArgumentParser(argparse.ArgumentParser):
35
37
  return result
36
38
 
37
39
 
38
- def parse_datetime(dt_str: str) -> datetime.datetime:
40
+ def parse_datetime(dt_str: str) -> datetime:
39
41
  """Parse a string into a datetime."""
40
42
  from meerschaum.utils.misc import is_int
41
43
  if is_int(dt_str):
42
44
  return int(dt_str)
43
45
 
44
46
  from meerschaum.utils.packages import attempt_import
45
- import datetime
46
47
  dateutil_parser = attempt_import('dateutil.parser')
47
48
 
48
49
  try:
49
50
  if dt_str.lower() == 'now':
50
- dt = datetime.datetime.utcnow()
51
+ dt = datetime.now(timezone.utc).replace(tzinfo=None)
51
52
  else:
52
53
  dt = dateutil_parser.parse(dt_str)
53
54
  except Exception as e:
@@ -11,14 +11,14 @@
11
11
  | ![PyPI - Python Version]( https://img.shields.io/pypi/pyversions/meerschaum?label=Python&logo=python&logoColor=%23ffffff ) | ![GitHub Sponsors](https://img.shields.io/github/sponsors/bmeares?color=eadf15&label=Sponsors) | [![meerschaum Tutorials](https://badges.openbase.com/python/tutorials/meerschaum.svg?token=2Yi8Oav9UZYWocO1ncwnIOnpUN5dTnUMWai7lAKTB+k=)](https://openbase.com/python/meerschaum?utm_source=embedded&utm_medium=badge&utm_campaign=rate-badge) | ![Number of registered users]( https://img.shields.io/badge/dynamic/json?color=3098c1&label=Registered%20Users&query=num_users&url=https%3A%2F%2Fapi.mrsm.io%2Finfo ) |
12
12
 
13
13
  <p align="center">
14
- <img src="https://meerschaum.io/files/images/demo.gif" alt="Meerschaum demo" height="450px">
14
+ <img src="https://meerschaum.io/files/images/demo.gif" alt="Meerschaum demo" style="width: 100%;">
15
15
  </p>
16
16
 
17
17
  ## What is Meerschaum?
18
18
  Meerschaum is a tool for quickly synchronizing time-series data streams called **pipes**. With Meerschaum, you can have a data visualization stack running in minutes.
19
19
 
20
20
  <p align="center">
21
- <img src="https://meerschaum.io/assets/screenshots/weather_pipes.png"/>
21
+ <img src="https://meerschaum.io/assets/screenshots/weather_pipes.png" style="width: 100%;"/>
22
22
  </p>
23
23
 
24
24
  ## Why Meerschaum?
@@ -8,9 +8,9 @@ Define the terminal page handler class.
8
8
 
9
9
  from __future__ import annotations
10
10
  from meerschaum.utils.packages import attempt_import
11
- tornado = attempt_import('tornado', lazy=False)
11
+ tornado_web = attempt_import('tornado.web', lazy=False)
12
12
 
13
- class TermPageHandler(tornado.web.RequestHandler):
13
+ class TermPageHandler(tornado_web.RequestHandler):
14
14
  def get(self):
15
15
  from meerschaum.api import endpoints
16
16
  return self.render(
@@ -7,7 +7,7 @@ Register new Pipes. Requires the API to be running.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import SuccessTuple, Any, List, Optional
10
+ from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Dict
11
11
 
12
12
  def register(
13
13
  action: Optional[List[str]] = None,
@@ -55,6 +55,7 @@ def _register_pipes(
55
55
  metric_keys: Optional[List[str]] = None,
56
56
  location_keys: Optional[List[str]] = None,
57
57
  params: Optional[Dict[str, Any]] = None,
58
+ tags: Optional[List[str]] = None,
58
59
  debug: bool = False,
59
60
  **kw: Any
60
61
  ) -> SuccessTuple:
@@ -79,6 +80,8 @@ def _register_pipes(
79
80
  location_keys = []
80
81
  if params is None:
81
82
  params = {}
83
+ if tags:
84
+ params['tags'] = tags
82
85
 
83
86
  if (
84
87
  len(connector_keys) == 0 or
@@ -96,6 +99,7 @@ def _register_pipes(
96
99
  metric_keys = metric_keys,
97
100
  location_keys = location_keys,
98
101
  params = params,
102
+ tags = tags,
99
103
  as_list = True,
100
104
  method = 'explicit',
101
105
  debug = debug,
@@ -738,6 +738,7 @@ def _show_environment(
738
738
 
739
739
 
740
740
  def _show_tags(
741
+ action: Optional[List[str]] = None,
741
742
  tags: Optional[List[str]] = None,
742
743
  workers: Optional[int] = None,
743
744
  nopretty: bool = False,
@@ -757,7 +758,12 @@ def _show_tags(
757
758
  )
758
759
  panel = rich_panel.Panel.fit('Tags')
759
760
  tree = rich_tree.Tree(panel)
760
- pipes = mrsm.get_pipes(as_list=True, **kwargs)
761
+ action = action or []
762
+ tags = action + (tags or [])
763
+ pipes = mrsm.get_pipes(as_list=True, tags=tags, **kwargs)
764
+ if not pipes:
765
+ return False, f"No pipes were found with the given tags."
766
+
761
767
  pool = get_pool(workers=workers)
762
768
  tag_prefix = get_config('formatting', 'pipes', 'unicode', 'icons', 'tag') if UNICODE else ''
763
769
  tag_style = get_config('formatting', 'pipes', 'ansi', 'styles', 'tags') if ANSI else None
@@ -769,6 +775,8 @@ def _show_tags(
769
775
 
770
776
  for pipe, tags in pipes_tags.items():
771
777
  for tag in tags:
778
+ if action and tag not in action:
779
+ continue
772
780
  tags_pipes[tag].append(pipe)
773
781
 
774
782
  columns = []
@@ -790,6 +798,9 @@ def _show_tags(
790
798
  tag_panel = rich_panel.Panel(tag_group, title=tag_text, title_align='left')
791
799
  columns.append(tag_panel)
792
800
 
801
+ if len(columns) == 0:
802
+ return False, "No pipes have been tagged."
803
+
793
804
  if not nopretty:
794
805
  mrsm.pprint(
795
806
  rich_columns.Columns(
@@ -9,7 +9,7 @@ NOTE: `sync` required a SQL connection and is not intended for client use
9
9
  """
10
10
 
11
11
  from __future__ import annotations
12
- from datetime import timedelta
12
+ from datetime import timedelta, datetime, timezone
13
13
  import meerschaum as mrsm
14
14
  from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Tuple, Union
15
15
 
@@ -39,7 +39,7 @@ def sync(
39
39
 
40
40
  def _pipes_lap(
41
41
  workers: Optional[int] = None,
42
- debug: bool = None,
42
+ debug: Optional[bool] = None,
43
43
  unblock: bool = False,
44
44
  force: bool = False,
45
45
  min_seconds: int = 1,
@@ -52,7 +52,7 @@ def _pipes_lap(
52
52
  nopretty: bool = False,
53
53
  _progress: Optional['rich.progress.Progress'] = None,
54
54
  **kw: Any
55
- ) -> Tuple[List[meerschaum.Pipe], List[meerschaum.Pipe]]:
55
+ ) -> Tuple[List[mrsm.Pipe], List[mrsm.Pipe]]:
56
56
  """
57
57
  Do a lap of syncing pipes.
58
58
  """
@@ -402,11 +402,20 @@ def _wrap_pipe(
402
402
  Wrapper function for handling exceptions.
403
403
  """
404
404
  import time
405
+ import traceback
406
+ from datetime import datetime, timedelta, timezone
407
+ import meerschaum as mrsm
408
+ from meerschaum.utils.typing import is_success_tuple, SuccessTuple
405
409
  from meerschaum.connectors import get_connector_plugin
406
410
  from meerschaum.utils.venv import Venv
407
411
  from meerschaum.plugins import _pre_sync_hooks, _post_sync_hooks
408
412
  from meerschaum.utils.misc import filter_keywords
413
+ from meerschaum.utils.pool import get_pool
414
+ from meerschaum.utils.warnings import warn
415
+
416
+ pool = get_pool(workers=workers)
409
417
 
418
+ sync_timestamp = datetime.now(timezone.utc)
410
419
  sync_start = time.perf_counter()
411
420
  sync_kwargs = {k: v for k, v in kw.items() if k != 'blocking'}
412
421
  sync_kwargs.update({
@@ -415,8 +424,9 @@ def _wrap_pipe(
415
424
  'debug': debug,
416
425
  'min_seconds': min_seconds,
417
426
  'workers': workers,
418
- 'bounded': 'bounded',
427
+ 'bounded': bounded,
419
428
  'chunk_interval': chunk_interval,
429
+ 'sync_timestamp': sync_timestamp,
420
430
  })
421
431
  if not verify and not deduplicate:
422
432
  sync_method = pipe.sync
@@ -427,12 +437,32 @@ def _wrap_pipe(
427
437
  sync_kwargs['deduplicate'] = deduplicate
428
438
  sync_kwargs['sync_method'] = sync_method
429
439
 
430
- for module_name, pre_sync_hooks in _pre_sync_hooks.items():
431
- plugin_name = module_name.split('.')[-1] if module_name.startswith('plugins.') else None
440
+ def call_sync_hook(plugin_name: str, sync_hook) -> SuccessTuple:
432
441
  plugin = mrsm.Plugin(plugin_name) if plugin_name else None
433
- with Venv(plugin):
434
- for pre_sync_hook in pre_sync_hooks:
435
- _ = pre_sync_hook(pipe, **filter_keywords(pre_sync_hook, **sync_kwargs))
442
+ with mrsm.Venv(plugin):
443
+ try:
444
+ sync_hook_result = sync_hook(pipe, **filter_keywords(sync_hook, **sync_kwargs))
445
+ if is_success_tuple(sync_hook_result):
446
+ return sync_hook_result
447
+ except Exception as e:
448
+ msg = (
449
+ f"Failed to execute sync hook '{sync_hook.__name__}' "
450
+ + f"from plugin '{plugin}':\n{traceback.format_exc()}"
451
+ )
452
+ warn(msg, stack=False)
453
+ return False, msg
454
+ return True, "Success"
455
+
456
+ hook_results = []
457
+ def apply_hooks(is_pre_sync: bool):
458
+ _sync_hooks = (_pre_sync_hooks if is_pre_sync else _post_sync_hooks)
459
+ for module_name, sync_hooks in _sync_hooks.items():
460
+ plugin_name = module_name.split('.')[-1] if module_name.startswith('plugins.') else None
461
+ for sync_hook in sync_hooks:
462
+ hook_result = pool.apply_async(call_sync_hook, (plugin_name, sync_hook))
463
+ hook_results.append(hook_result)
464
+
465
+ apply_hooks(True)
436
466
 
437
467
  try:
438
468
  with Venv(get_connector_plugin(pipe.connector), debug=debug):
@@ -444,18 +474,16 @@ def _wrap_pipe(
444
474
  return_tuple = (False, f"Failed to sync {pipe} with exception:" + "\n" + str(e))
445
475
 
446
476
  duration = time.perf_counter() - sync_start
447
- sync_kwargs['duration'] = duration
448
- for module_name, post_sync_hooks in _post_sync_hooks.items():
449
- plugin_name = module_name.split('.')[-1] if module_name.startswith('plugins.') else None
450
- plugin = mrsm.Plugin(plugin_name) if plugin_name else None
451
- with Venv(plugin):
452
- for post_sync_hook in post_sync_hooks:
453
- _ = post_sync_hook(
454
- pipe,
455
- return_tuple,
456
- **filter_keywords(post_sync_hook, **sync_kwargs)
457
- )
458
-
477
+ sync_kwargs.update({
478
+ 'success_tuple': return_tuple,
479
+ 'sync_duration': duration,
480
+ 'sync_complete_timestamp': datetime.now(timezone.utc),
481
+ })
482
+ apply_hooks(False)
483
+ for hook_result in hook_results:
484
+ hook_success, hook_msg = hook_result.get()
485
+ mrsm.pprint((hook_success, hook_msg))
486
+
459
487
  return return_tuple
460
488
 
461
489
 
@@ -0,0 +1,101 @@
1
+ #! /usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Functions for editing elements belong here.
7
+ """
8
+
9
+ from __future__ import annotations
10
+ import meerschaum as mrsm
11
+ from meerschaum.utils.typing import List, Any, SuccessTuple, Optional, Dict
12
+
13
+ def tag(
14
+ action: Optional[List[str]] = None,
15
+ **kwargs: Any
16
+ ) -> SuccessTuple:
17
+ """
18
+ Edit an existing element.
19
+ """
20
+ from meerschaum.actions import choose_subaction
21
+ options = {
22
+ 'pipes': _tag_pipes,
23
+ }
24
+ return choose_subaction(action, options, **kwargs)
25
+
26
+
27
+ def _tag_pipes(
28
+ action: Optional[List[str]] = None,
29
+ debug: bool = False,
30
+ **kwargs: Any
31
+ ) -> SuccessTuple:
32
+ """
33
+ Add or remove tags to registered pipes.
34
+ Prefix a tag with a leading underscore to remove it.
35
+
36
+ Note that the `--tags` flag applies to existing tags.
37
+ Specify the tags you wish to add or remove as positional arguments, e.g.:
38
+
39
+ ```
40
+ mrsm tag pipes production sync-daily -c sql:main
41
+ mrsm tag pipes _production --tags production
42
+ ```
43
+ """
44
+ from meerschaum.utils.warnings import warn
45
+ from meerschaum.utils.misc import separate_negation_values
46
+ if not action:
47
+ return False, "No tags were provided."
48
+
49
+ pipes = mrsm.get_pipes(as_list=True, debug=debug, **kwargs)
50
+ if not pipes:
51
+ return False, "No pipes were found with the given keys."
52
+ success_msg = f"Updated tags for {len(pipes)} pipe" + ('s' if len(pipes) != 1 else '') + '.'
53
+
54
+ add_tags, remove_tags = separate_negation_values(action)
55
+ edited_pipes = []
56
+ for pipe in pipes:
57
+ pipe_was_edited = False
58
+ pipe_tags = pipe.tags
59
+ existing_tags = set(pipe_tags)
60
+ for tag_to_add in add_tags:
61
+ if tag_to_add not in existing_tags:
62
+ pipe_tags.append(tag_to_add)
63
+ pipe_was_edited = True
64
+
65
+ for tag_to_remove in remove_tags:
66
+ if tag_to_remove in existing_tags:
67
+ pipe_tags.remove(tag_to_remove)
68
+ pipe_was_edited = True
69
+
70
+ if pipe_was_edited:
71
+ edited_pipes.append(pipe)
72
+
73
+ if not edited_pipes:
74
+ return True, success_msg
75
+
76
+ failed_edit_msgs = []
77
+ for pipe in edited_pipes:
78
+ edit_success, edit_msg = pipe.edit(debug=debug)
79
+ if not edit_success:
80
+ warn(f"Failed to update tags for {pipe}:\n{edit_msg}", stack=False)
81
+ failed_edit_msgs.append(edit_msg)
82
+
83
+ num_failures = len(failed_edit_msgs)
84
+ success = num_failures == 0
85
+ msg = (
86
+ success_msg
87
+ if success else (
88
+ f"Failed to update tags for {num_failures} pipe"
89
+ + ('s' if num_failures != 1 else '')
90
+ + ":\n"
91
+ + '\n'.join(failed_edit_msgs)
92
+ )
93
+ )
94
+ return success, msg
95
+
96
+
97
+ ### NOTE: This must be the final statement of the module.
98
+ ### Any subactions added below these lines will not
99
+ ### be added to the `help` docstring.
100
+ from meerschaum.actions import choices_docstring as _choices_docstring
101
+ tag.__doc__ += _choices_docstring('tag')
@@ -18,7 +18,7 @@ The Meerschaum Web API lets you access and control your data over the Internet.
18
18
  from meerschaum.config import get_config
19
19
  from meerschaum.config.static import STATIC_CONFIG, SERVER_ID
20
20
  from meerschaum.utils.packages import attempt_import
21
- from meerschaum.utils.get_pipes import get_pipes as _get_pipes
21
+ from meerschaum.utils import get_pipes as _get_pipes
22
22
  from meerschaum.config._paths import API_UVICORN_CONFIG_PATH, API_UVICORN_RESOURCES_PATH
23
23
  from meerschaum.plugins import _api_plugins
24
24
  from meerschaum.utils.warnings import warn
@@ -523,7 +523,7 @@ def update_keys_options(
523
523
  _mk_alone = metric_keys and num_filter == 1
524
524
  _lk_alone = location_keys and num_filter == 1
525
525
 
526
- from meerschaum.utils.get_pipes import fetch_pipes_keys
526
+ from meerschaum.utils import fetch_pipes_keys
527
527
 
528
528
  try:
529
529
  _all_keys = fetch_pipes_keys('registered', get_web_connector(ctx.states))
@@ -6,7 +6,8 @@
6
6
  Manage access and refresh tokens.
7
7
  """
8
8
 
9
- import fastapi, datetime
9
+ from datetime import datetime, timedelta, timezone
10
+ import fastapi
10
11
  from fastapi_login.exceptions import InvalidCredentialsException
11
12
  from fastapi.security import OAuth2PasswordRequestForm
12
13
  from starlette.responses import Response, JSONResponse
@@ -47,8 +48,8 @@ def login(
47
48
  raise InvalidCredentialsException
48
49
 
49
50
  expires_minutes = STATIC_CONFIG['api']['oauth']['token_expires_minutes']
50
- expires_delta = datetime.timedelta(minutes=expires_minutes)
51
- expires_dt = datetime.datetime.utcnow() + expires_delta
51
+ expires_delta = timedelta(minutes=expires_minutes)
52
+ expires_dt = datetime.now(timezone.utc).replace(tzinfo=None) + expires_delta
52
53
  access_token = manager.create_access_token(
53
54
  data = dict(sub=username),
54
55
  expires = expires_delta
@@ -35,7 +35,7 @@ default_formatting_config = {
35
35
  'running' : '🟢',
36
36
  'paused' : '🟡',
37
37
  'stopped' : '🔴',
38
- 'tag' : '🏷️',
38
+ 'tag' : '🔖',
39
39
  },
40
40
  'pipes' : {
41
41
  'unicode' : {
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.1.4"
5
+ __version__ = "2.1.6"
@@ -6,7 +6,7 @@
6
6
  Interact with Meerschaum APIs. May be chained together (see 'meerschaum:api_instance' in your config.yaml).
7
7
  """
8
8
 
9
- from datetime import datetime, timedelta
9
+ from datetime import datetime, timedelta, timezone
10
10
  from meerschaum.utils.typing import Optional
11
11
  from meerschaum.connectors import Connector
12
12
  from meerschaum.utils.warnings import warn, error
@@ -146,7 +146,11 @@ class APIConnector(Connector):
146
146
  def token(self):
147
147
  expired = (
148
148
  True if self._expires is None else (
149
- (self._expires < datetime.utcnow() + timedelta(minutes=1))
149
+ (
150
+ self._expires
151
+ <
152
+ datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(minutes=1)
153
+ )
150
154
  )
151
155
  )
152
156
 
@@ -242,7 +242,6 @@ def fetch_pipes_keys(
242
242
  pipes_tbl.c.metric_key,
243
243
  pipes_tbl.c.location_key,
244
244
  ]
245
- + ([pipes_tbl.c.parameters] if tags else [])
246
245
  )
247
246
 
248
247
  q = sqlalchemy.select(*select_cols).where(sqlalchemy.and_(True, *_where))
@@ -267,7 +266,8 @@ def fetch_pipes_keys(
267
266
  sqlalchemy.String,
268
267
  ).like(f'%"tags":%"{nt}"%')
269
268
  )
270
- ors.append(sqlalchemy.and_(*sub_ands))
269
+ if sub_ands:
270
+ ors.append(sqlalchemy.and_(*sub_ands))
271
271
 
272
272
  for xt in _ex_tags:
273
273
  nands.append(
@@ -303,26 +303,7 @@ def fetch_pipes_keys(
303
303
  except Exception as e:
304
304
  error(str(e))
305
305
 
306
- _keys = [(row[0], row[1], row[2]) for row in rows]
307
- if not tags:
308
- return _keys
309
- ### Make 100% sure that the tags are correct.
310
- keys = []
311
- for row in rows:
312
- ktup = (row[0], row[1], row[2])
313
- _actual_tags = (
314
- json.loads(row[3]) if isinstance(row[3], str)
315
- else row[3]
316
- ).get('tags', [])
317
- for nt in _in_tags:
318
- if nt in _actual_tags:
319
- keys.append(ktup)
320
- for xt in _ex_tags:
321
- if xt in _actual_tags:
322
- keys.remove(ktup)
323
- else:
324
- keys.append(ktup)
325
- return keys
306
+ return [(row[0], row[1], row[2]) for row in rows]
326
307
 
327
308
 
328
309
  def create_indices(
@@ -404,7 +385,13 @@ def get_create_index_queries(
404
385
  -------
405
386
  A dictionary of column names mapping to lists of queries.
406
387
  """
407
- from meerschaum.utils.sql import sql_item_name, get_distinct_col_count, update_queries
388
+ from meerschaum.utils.sql import (
389
+ sql_item_name,
390
+ get_distinct_col_count,
391
+ update_queries,
392
+ get_null_replacement,
393
+ COALESCE_UNIQUE_INDEX_FLAVORS,
394
+ )
408
395
  from meerschaum.config import get_config
409
396
  index_queries = {}
410
397
 
@@ -516,15 +503,37 @@ def get_create_index_queries(
516
503
  if ix and ix in existing_cols_types
517
504
  ]
518
505
  )
506
+ coalesce_indices_cols_str = ', '.join(
507
+ [
508
+ (
509
+ "COALESCE("
510
+ + sql_item_name(ix, self.flavor)
511
+ + ", "
512
+ + get_null_replacement(existing_cols_types[ix], self.flavor)
513
+ + ") "
514
+ ) if ix_key != 'datetime' else (sql_item_name(ix, self.flavor))
515
+ for ix_key, ix in pipe.columns.items()
516
+ if ix and ix in existing_cols_types
517
+ ]
518
+ )
519
+ unique_index_name = sql_item_name(pipe.target + '_unique_index', self.flavor)
519
520
  constraint_name = sql_item_name(pipe.target + '_constraint', self.flavor)
520
- constraint_query = (
521
+ add_constraint_query = (
521
522
  f"ALTER TABLE {_pipe_name} ADD CONSTRAINT {constraint_name} UNIQUE ({indices_cols_str})"
522
- if self.flavor != 'sqlite'
523
- else f"CREATE UNIQUE INDEX {constraint_name} ON {_pipe_name} ({indices_cols_str})"
524
523
  )
524
+ unique_index_cols_str = (
525
+ indices_cols_str
526
+ if self.flavor not in COALESCE_UNIQUE_INDEX_FLAVORS
527
+ else coalesce_indices_cols_str
528
+ )
529
+ create_unique_index_query = (
530
+ f"CREATE UNIQUE INDEX {unique_index_name} ON {_pipe_name} ({unique_index_cols_str})"
531
+ )
532
+ constraint_queries = [create_unique_index_query]
533
+ if self.flavor != 'sqlite':
534
+ constraint_queries.append(add_constraint_query)
525
535
  if upsert and indices_cols_str:
526
- index_queries[constraint_name] = [constraint_query]
527
-
536
+ index_queries[unique_index_name] = constraint_queries
528
537
  return index_queries
529
538
 
530
539
 
@@ -1093,7 +1102,7 @@ def get_pipe_attributes(
1093
1102
  def sync_pipe(
1094
1103
  self,
1095
1104
  pipe: mrsm.Pipe,
1096
- df: Union[pandas.DataFrame, str, Dict[Any, Any], None] = None,
1105
+ df: Union[pd.DataFrame, str, Dict[Any, Any], None] = None,
1097
1106
  begin: Optional[datetime] = None,
1098
1107
  end: Optional[datetime] = None,
1099
1108
  chunksize: Optional[int] = -1,
@@ -1305,7 +1314,11 @@ def sync_pipe(
1305
1314
  temp_pipe = Pipe(
1306
1315
  pipe.connector_keys.replace(':', '_') + '_', pipe.metric_key, pipe.location_key,
1307
1316
  instance = pipe.instance_keys,
1308
- columns = pipe.columns,
1317
+ columns = {
1318
+ ix_key: ix
1319
+ for ix_key, ix in pipe.columns.items()
1320
+ if ix and ix in update_df.columns
1321
+ },
1309
1322
  dtypes = pipe.dtypes,
1310
1323
  target = temp_target,
1311
1324
  temporary = True,
@@ -214,22 +214,6 @@ def create_tables(
214
214
  from meerschaum.utils.sql import get_rename_table_queries, table_exists
215
215
  _tables = tables if tables is not None else get_tables(conn)
216
216
 
217
- rename_queries = []
218
- for table_key, table in _tables.items():
219
- if table_exists(
220
- table_key,
221
- conn,
222
- schema = conn.instance_schema,
223
- ):
224
- rename_queries.extend(get_rename_table_queries(
225
- table_key,
226
- table.name,
227
- schema = conn.instance_schema,
228
- flavor = conn.flavor,
229
- ))
230
- if rename_queries:
231
- conn.exec_queries(rename_queries)
232
-
233
217
  try:
234
218
  conn.metadata.create_all(bind=conn.engine)
235
219
  except Exception as e:
@@ -7,8 +7,7 @@ Pipes are the primary metaphor of the Meerschaum system.
7
7
  You can interact with pipe data via `meerschaum.Pipe` objects.
8
8
 
9
9
  If you are working with multiple pipes, it is highly recommended that you instead use
10
- `meerschaum.utils.get_pipes` (available as `meerschaum.get_pipes`)
11
- to create a dictionary of Pipe objects.
10
+ `meerschaum.get_pipes` to create a dictionary of Pipe objects.
12
11
 
13
12
  ```
14
13
  >>> from meerschaum import get_pipes
@@ -8,7 +8,7 @@ Retrieve Pipes' data from instances.
8
8
 
9
9
  from __future__ import annotations
10
10
  from datetime import datetime, timedelta
11
- from meerschaum.utils.typing import Optional, Dict, Any, Union, Generator, List, Tuple
11
+ from meerschaum.utils.typing import Optional, Dict, Any, Union, Generator, List, Tuple, Iterator
12
12
  from meerschaum.config import get_config
13
13
 
14
14
  def get_data(
@@ -247,7 +247,7 @@ def _get_data_as_iterator(
247
247
  fresh: bool = False,
248
248
  debug: bool = False,
249
249
  **kw: Any
250
- ) -> Generator['pd.DataFrame']:
250
+ ) -> Iterator['pd.DataFrame']:
251
251
  """
252
252
  Return a pipe's data as a generator.
253
253
  """
@@ -267,16 +267,17 @@ def _get_data_as_iterator(
267
267
 
268
268
  _ = kw.pop('as_chunks', None)
269
269
  _ = kw.pop('as_iterator', None)
270
+ dt_col = self.columns.get('datetime', None)
270
271
  min_dt = (
271
272
  begin
272
273
  if begin is not None
273
274
  else self.get_sync_time(round_down=False, newest=False, params=params, debug=debug)
274
- )
275
+ ) if dt_col else None
275
276
  max_dt = (
276
277
  end
277
278
  if end is not None
278
279
  else self.get_sync_time(round_down=False, newest=True, params=params, debug=debug)
279
- )
280
+ ) if dt_col else None
280
281
 
281
282
  ### We want to search just past the maximum value.
282
283
  if end is None:
@@ -28,5 +28,7 @@ __all__ = (
28
28
  'venv',
29
29
  'warnings',
30
30
  'yaml',
31
+ "get_pipes",
32
+ "fetch_pipes_keys",
31
33
  )
32
- from meerschaum.utils.get_pipes import get_pipes
34
+ from meerschaum.utils._get_pipes import get_pipes, fetch_pipes_keys