meerschaum 2.5.1__py3-none-any.whl → 2.6.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 (41) hide show
  1. meerschaum/_internal/arguments/_parser.py +6 -1
  2. meerschaum/actions/edit.py +6 -6
  3. meerschaum/actions/sql.py +12 -11
  4. meerschaum/api/dash/pages/login.py +17 -17
  5. meerschaum/api/dash/pipes.py +13 -4
  6. meerschaum/api/routes/_pipes.py +58 -40
  7. meerschaum/config/_edit.py +46 -19
  8. meerschaum/config/_read_config.py +20 -9
  9. meerschaum/config/_version.py +1 -1
  10. meerschaum/config/stack/__init__.py +1 -1
  11. meerschaum/config/static/__init__.py +1 -0
  12. meerschaum/connectors/api/_APIConnector.py +1 -0
  13. meerschaum/connectors/api/_pipes.py +39 -8
  14. meerschaum/connectors/sql/_SQLConnector.py +4 -3
  15. meerschaum/connectors/sql/_pipes.py +511 -118
  16. meerschaum/connectors/sql/_sql.py +55 -15
  17. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -2
  18. meerschaum/connectors/valkey/_pipes.py +11 -5
  19. meerschaum/core/Pipe/__init__.py +27 -9
  20. meerschaum/core/Pipe/_attributes.py +181 -18
  21. meerschaum/core/Pipe/_clear.py +10 -8
  22. meerschaum/core/Pipe/_copy.py +2 -0
  23. meerschaum/core/Pipe/_data.py +65 -17
  24. meerschaum/core/Pipe/_deduplicate.py +30 -28
  25. meerschaum/core/Pipe/_dtypes.py +4 -4
  26. meerschaum/core/Pipe/_fetch.py +12 -10
  27. meerschaum/core/Pipe/_sync.py +28 -11
  28. meerschaum/core/Pipe/_verify.py +52 -49
  29. meerschaum/utils/dataframe.py +62 -32
  30. meerschaum/utils/dtypes/__init__.py +25 -6
  31. meerschaum/utils/dtypes/sql.py +76 -33
  32. meerschaum/utils/misc.py +57 -24
  33. meerschaum/utils/sql.py +697 -44
  34. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/METADATA +1 -1
  35. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/RECORD +41 -41
  36. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/WHEEL +1 -1
  37. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/LICENSE +0 -0
  38. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/NOTICE +0 -0
  39. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/entry_points.txt +0 -0
  40. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/top_level.txt +0 -0
  41. {meerschaum-2.5.1.dist-info → meerschaum-2.6.0.dist-info}/zip-safe +0 -0
@@ -90,8 +90,13 @@ def parse_datetime(dt_str: str) -> Union[datetime, int, str]:
90
90
  except Exception as e:
91
91
  dt = None
92
92
  if dt is None:
93
- from meerschaum.utils.warnings import warn, error
93
+ from meerschaum.utils.warnings import error
94
94
  error(f"'{dt_str}' is not a valid datetime format.", stack=False)
95
+
96
+ if isinstance(dt, datetime):
97
+ from meerschaum.utils.dtypes import coerce_timezone
98
+ dt = coerce_timezone(dt)
99
+
95
100
  return dt
96
101
 
97
102
 
@@ -65,24 +65,24 @@ def _complete_edit(
65
65
  from meerschaum._internal.shell import default_action_completer
66
66
  return default_action_completer(action=(['edit'] + action), **kw)
67
67
 
68
- def _edit_config(action : Optional[List[str]] = None, **kw : Any) -> SuccessTuple:
68
+
69
+ def _edit_config(action: Optional[List[str]] = None, **kw : Any) -> SuccessTuple:
69
70
  """
70
71
  Edit Meerschaum configuration files.
71
-
72
+
72
73
  Specify a specific configuration key to edit.
73
74
  Defaults to editing `meerschaum` configuration (connectors, instance, etc.).
74
-
75
+
75
76
  Examples:
76
77
  ```
77
78
  ### Edit the main 'meerschaum' configuration.
78
79
  edit config
79
-
80
+
80
81
  ### Edit 'system' configuration.
81
82
  edit config system
82
-
83
+
83
84
  ### Create a new configuration file called 'myconfig'.
84
85
  edit config myconfig
85
-
86
86
  ```
87
87
  """
88
88
  from meerschaum.config._edit import edit_config
meerschaum/actions/sql.py CHANGED
@@ -14,6 +14,7 @@ exec_methods = {
14
14
  'exec',
15
15
  }
16
16
 
17
+
17
18
  def sql(
18
19
  action: Optional[List[str]] = None,
19
20
  gui: bool = False,
@@ -22,40 +23,40 @@ def sql(
22
23
  **kw: Any
23
24
  ):
24
25
  """Execute a SQL query or launch an interactive CLI. All positional arguments are optional.
25
-
26
+
26
27
  Usage:
27
28
  `sql {label} {method} {query / table}`
28
-
29
+
29
30
  Options:
30
31
  - `sql {label}`
31
32
  Launch an interactive CLI. If {label} is omitted, use 'main'.
32
-
33
+
33
34
  - `sql {label} read [query / table]`
34
35
  Read a table or query as a pandas DataFrame and print the result.
35
-
36
+
36
37
  - `sql {label} exec [query]`
37
38
  Execute a query and print the success status.
38
-
39
+
39
40
  Examples:
40
41
  - `sql`
41
42
  Open an interactive CLI for `sql:main`.
42
-
43
+
43
44
  - `sql local`
44
45
  Open an interactive CLI for `sql:local`.
45
-
46
+
46
47
  - `sql table`
47
48
  Read from `table` on `sql:main`
48
49
  (translates to `SELECT * FROM table`).
49
-
50
+
50
51
  - `sql local table`
51
52
  Read from `table` on `sql:local`.
52
-
53
+
53
54
  - `sql local read table`
54
55
  Read from `table` on `sql:local`.
55
-
56
+
56
57
  - `sql "SELECT * FROM table WHERE id = 1"`
57
58
  Execute the above query on `sql:main` and print the results.
58
-
59
+
59
60
  - `sql local exec "INSERT INTO table (id) VALUES (1)"
60
61
  Execute the above query on `sql:local`.
61
62
  """
@@ -19,13 +19,13 @@ allow_user_registration = permissions_config['registration']['users']
19
19
 
20
20
  registration_div = html.Div(
21
21
  id = 'registration-div',
22
- style = {'height' : '100%'},
23
- children = (
22
+ style={'height' : '100%'},
23
+ children=(
24
24
  [
25
25
  dcc.Link(
26
26
  'No account? Create one here.',
27
- href = (dash_endpoint + '/register'),
28
- refresh = False
27
+ href=(dash_endpoint + '/register'),
28
+ refresh=False
29
29
  ),
30
30
  ] if allow_user_registration
31
31
  else [
@@ -36,12 +36,12 @@ registration_div = html.Div(
36
36
  dbc.Button(
37
37
  'More information.',
38
38
  id = 'show-user-registration-disabled-button',
39
- color = 'link',
40
- size = 'sm',
39
+ color='link',
40
+ size='sm',
41
41
  ),
42
42
  dbc.Collapse(
43
- id = 'user-registration-disabled-collapse',
44
- children = [
43
+ id='user-registration-disabled-collapse',
44
+ children=[
45
45
  dcc.Markdown(
46
46
  "For example, to register user `newuser` on instance `sql:main`:"
47
47
  ),
@@ -60,10 +60,10 @@ registration_div = html.Div(
60
60
  html.Pre(
61
61
  html.Code(
62
62
  json.dumps({
63
- 'api' : {
64
- 'permissions' : {
65
- 'registration' : {
66
- 'users' : True,
63
+ 'api': {
64
+ 'permissions': {
65
+ 'registration': {
66
+ 'users': True,
67
67
  },
68
68
  }
69
69
  }
@@ -112,11 +112,11 @@ layout = dbc.Container([
112
112
  dbc.Row([
113
113
  dbc.Col([
114
114
  html.Button(
115
- children = 'Login',
116
- n_clicks = 0,
117
- type = 'submit',
118
- id = 'login-button',
119
- className = 'btn btn-primary btn-lg'
115
+ children='Login',
116
+ n_clicks=0,
117
+ type='submit',
118
+ id='login-button',
119
+ className='btn btn-primary btn-lg'
120
120
  ),
121
121
  ]),
122
122
  ]),
@@ -33,6 +33,11 @@ html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHEC
33
33
  humanfriendly = attempt_import('humanfriendly', check_update=CHECK_UPDATE)
34
34
  pd = import_pandas()
35
35
 
36
+ INDEX_PREFIX_EMOJI: Dict[str, str] = {
37
+ 'datetime': '🕓',
38
+ 'primary': '🔑',
39
+ }
40
+
36
41
 
37
42
  def pipe_from_ctx(ctx, trigger_property: str = 'n_clicks') -> Union[mrsm.Pipe, None]:
38
43
  """
@@ -420,9 +425,9 @@ def accordion_items_from_pipe(
420
425
  columns = pipe.columns
421
426
  index_column_names = pipe.get_indices()
422
427
  indices_rows = []
423
- for ix_key, ix_name in index_column_names.items():
428
+ for ix_key, ix_cols in indices.items():
424
429
  col = columns.get(ix_key, None)
425
- ix_cols = indices.get(ix_key, None)
430
+ ix_name = index_column_names.get(ix_key, None)
426
431
  if not col and not ix_cols:
427
432
  continue
428
433
  if not isinstance(ix_cols, (list, tuple)):
@@ -434,12 +439,16 @@ def accordion_items_from_pipe(
434
439
  else:
435
440
  col_item = html.Pre(html.Ul([html.Li(_c) for _c in ix_cols]))
436
441
  is_composite_item = "✅" if col else ""
437
- ix_key_item = html.Pre(ix_key) if ix_key != 'datetime' else html.Pre(f"🕓 {ix_key}")
442
+ ix_key_item = html.Pre(
443
+ INDEX_PREFIX_EMOJI.get(ix_key, '')
444
+ + (' ' if ix_key in INDEX_PREFIX_EMOJI else '')
445
+ + ix_key
446
+ )
438
447
  indices_rows.append(
439
448
  html.Tr([
440
449
  html.Td(ix_key_item),
441
450
  html.Td(col_item),
442
- html.Td(html.Pre(ix_name)),
451
+ html.Td(html.Pre(ix_name or '')),
443
452
  html.Td(is_composite_item),
444
453
  ])
445
454
  )
@@ -558,7 +558,7 @@ def get_pipe_id(
558
558
  curr_user = (
559
559
  fastapi.Depends(manager) if not no_auth else None
560
560
  ),
561
- ) -> int:
561
+ ) -> Union[int, str]:
562
562
  """
563
563
  Get a Pipe's ID.
564
564
  """
@@ -573,36 +573,36 @@ def get_pipe_id(
573
573
  tags = ['Pipes']
574
574
  )
575
575
  def get_pipe_attributes(
576
- connector_keys : str,
577
- metric_key : str,
578
- location_key : str,
579
- curr_user = (
580
- fastapi.Depends(manager) if not no_auth else None
581
- ),
582
- ) -> Dict[str, Any]:
576
+ connector_keys: str,
577
+ metric_key: str,
578
+ location_key: str,
579
+ curr_user=(
580
+ fastapi.Depends(manager) if not no_auth else None
581
+ ),
582
+ ) -> Dict[str, Any]:
583
583
  """Get a Pipe's attributes."""
584
584
  return get_pipe(connector_keys, metric_key, location_key).attributes
585
585
 
586
586
 
587
587
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/exists', tags=['Pipes'])
588
588
  def get_pipe_exists(
589
- connector_keys: str,
590
- metric_key: str,
591
- location_key: str,
592
- curr_user = (
593
- fastapi.Depends(manager) if not no_auth else None
594
- ),
595
- ) -> bool:
589
+ connector_keys: str,
590
+ metric_key: str,
591
+ location_key: str,
592
+ curr_user = (
593
+ fastapi.Depends(manager) if not no_auth else None
594
+ ),
595
+ ) -> bool:
596
596
  """Determine whether a Pipe exists."""
597
597
  return get_pipe(connector_keys, metric_key, location_key).exists()
598
598
 
599
599
 
600
600
  @app.post(endpoints['metadata'], tags=['Pipes'])
601
601
  def create_metadata(
602
- curr_user = (
603
- fastapi.Depends(manager) if not no_auth else None
604
- ),
605
- ) -> bool:
602
+ curr_user = (
603
+ fastapi.Depends(manager) if not no_auth else None
604
+ ),
605
+ ) -> bool:
606
606
  """Create Pipe metadata tables"""
607
607
  from meerschaum.connectors.sql.tables import get_tables
608
608
  try:
@@ -614,16 +614,16 @@ def create_metadata(
614
614
 
615
615
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/rowcount', tags=['Pipes'])
616
616
  def get_pipe_rowcount(
617
- connector_keys: str,
618
- metric_key: str,
619
- location_key: str,
620
- begin: Union[str, int, None] = None,
621
- end: Union[str, int, None] = None,
622
- params: Optional[Dict[str, Any]] = None,
623
- curr_user = (
624
- fastapi.Depends(manager) if not no_auth else None
625
- ),
626
- ) -> int:
617
+ connector_keys: str,
618
+ metric_key: str,
619
+ location_key: str,
620
+ begin: Union[str, int, None] = None,
621
+ end: Union[str, int, None] = None,
622
+ params: Optional[Dict[str, Any]] = None,
623
+ curr_user = (
624
+ fastapi.Depends(manager) if not no_auth else None
625
+ ),
626
+ ) -> int:
627
627
  """
628
628
  Return a pipe's rowcount.
629
629
  """
@@ -632,10 +632,10 @@ def get_pipe_rowcount(
632
632
  if is_int(end):
633
633
  end = int(end)
634
634
  return get_pipe(connector_keys, metric_key, location_key).get_rowcount(
635
- begin = begin,
636
- end = end,
637
- params = params,
638
- debug = debug
635
+ begin=begin,
636
+ end=end,
637
+ params=params,
638
+ debug=debug,
639
639
  )
640
640
 
641
641
 
@@ -644,13 +644,13 @@ def get_pipe_rowcount(
644
644
  tags=['Pipes']
645
645
  )
646
646
  def get_pipe_columns_types(
647
- connector_keys: str,
648
- metric_key: str,
649
- location_key: str,
650
- curr_user = (
651
- fastapi.Depends(manager) if not no_auth else None
652
- ),
653
- ) -> Dict[str, str]:
647
+ connector_keys: str,
648
+ metric_key: str,
649
+ location_key: str,
650
+ curr_user=(
651
+ fastapi.Depends(manager) if not no_auth else None
652
+ ),
653
+ ) -> Dict[str, str]:
654
654
  """
655
655
  Return a dictionary of column names and types.
656
656
 
@@ -663,3 +663,21 @@ def get_pipe_columns_types(
663
663
  ```
664
664
  """
665
665
  return get_pipe(connector_keys, metric_key, location_key).dtypes
666
+
667
+
668
+ @app.get(
669
+ pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/columns/indices',
670
+ tags=['Pipes']
671
+ )
672
+ def get_pipe_columns_indices(
673
+ connector_keys: str,
674
+ metric_key: str,
675
+ location_key: str,
676
+ curr_user=(
677
+ fastapi.Depends(manager) if not no_auth else None
678
+ ),
679
+ ) -> Dict[str, List[Dict[str, str]]]:
680
+ """
681
+ Return a dictionary of column names and related indices.
682
+ """
683
+ return get_pipe(connector_keys, metric_key, location_key).get_columns_indices()
@@ -7,44 +7,71 @@ Functions for editing the configuration file
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- import sys
11
10
  import pathlib
12
11
  from meerschaum.utils.typing import Optional, Any, SuccessTuple, Mapping, Dict, List, Union
13
12
 
13
+
14
14
  def edit_config(
15
- keys : Optional[List[str]] = None,
16
- params : Optional[Dict[str, Any]] = None,
17
- debug : bool = False,
18
- **kw : Any
19
- ) -> SuccessTuple:
15
+ keys: Optional[List[str]] = None,
16
+ params: Optional[Dict[str, Any]] = None,
17
+ debug: bool = False,
18
+ **kw: Any
19
+ ) -> SuccessTuple:
20
20
  """Edit the configuration files."""
21
21
  from meerschaum.config import get_config, config
22
- from meerschaum.config._read_config import get_keyfile_path
22
+ from meerschaum.config._read_config import get_keyfile_path, read_config
23
23
  from meerschaum.config._paths import CONFIG_DIR_PATH
24
24
  from meerschaum.utils.packages import reload_meerschaum
25
25
  from meerschaum.utils.misc import edit_file
26
- from meerschaum.utils.debug import dprint
26
+ from meerschaum.utils.warnings import warn, dprint
27
+ from meerschaum.utils.prompt import prompt
27
28
 
28
29
  if keys is None:
29
30
  keys = []
30
31
 
31
- for k in keys:
32
- ### If defined in default, create the config file.
33
- if isinstance(config, dict) and k in config:
34
- del config[k]
35
- get_config(k, write_missing=True, warn=False)
36
- edit_file(get_keyfile_path(k, create_new=True))
32
+ def _edit_key(key: str):
33
+ while True:
34
+ ### If defined in default, create the config file.
35
+ key_config = config.pop(key, None)
36
+ keyfile_path = get_keyfile_path(key, create_new=True)
37
+ get_config(key, write_missing=True, warn=False)
38
+
39
+ edit_file(get_keyfile_path(key, create_new=True))
40
+
41
+ ### TODO: verify that the file is valid. Retry if not.
42
+ try:
43
+ new_key_config = read_config(
44
+ CONFIG_DIR_PATH,
45
+ [key],
46
+ write_missing=False,
47
+ raise_parsing_errors=True,
48
+ )
49
+ except Exception:
50
+ if key_config:
51
+ config[key] = key_config
52
+ warn(f"Could not parse key '{key}'.", stack=False)
53
+ _ = prompt(f"Press [Enter] to edit '{keyfile_path}', [CTRL+C] to exit.")
54
+ continue
55
+
56
+ if new_key_config:
57
+ break
58
+
59
+ try:
60
+ for k in keys:
61
+ _edit_key(k)
62
+ except KeyboardInterrupt:
63
+ return False, f""
37
64
 
38
65
  reload_meerschaum(debug=debug)
39
66
  return (True, "Success")
40
67
 
41
68
 
42
69
  def write_config(
43
- config_dict: Optional[Dict[str, Any]] = None,
44
- directory: Union[str, pathlib.Path, None] = None,
45
- debug: bool = False,
46
- **kw : Any
47
- ) -> bool:
70
+ config_dict: Optional[Dict[str, Any]] = None,
71
+ directory: Union[str, pathlib.Path, None] = None,
72
+ debug: bool = False,
73
+ **kw: Any
74
+ ) -> bool:
48
75
  """Write YAML and JSON files to the configuration directory.
49
76
 
50
77
  Parameters
@@ -6,22 +6,26 @@ Import the config yaml file
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+ import pathlib
10
+
9
11
  from meerschaum.utils.typing import Optional, Dict, Any, List, Tuple, Union
10
12
  from meerschaum.config import get_config
11
13
 
14
+
12
15
  def read_config(
13
- directory: Optional[str] = None,
16
+ directory: Union[pathlib.Path, str, None] = None,
14
17
  keys: Optional[List[str]] = None,
15
- write_missing : bool = True,
16
- substitute : bool = True,
17
- with_filenames : bool = False,
18
+ write_missing: bool = True,
19
+ substitute: bool = True,
20
+ with_filenames: bool = False,
21
+ raise_parsing_errors: bool = False,
18
22
  ) -> Union[Dict[str, Any], Tuple[Dict[str, Any], List[str]]]:
19
23
  """
20
24
  Read the configuration directory.
21
25
 
22
26
  Parameters
23
27
  ----------
24
- directory: Optional[str], default None
28
+ directory: Union[pathlib.Path, str, None], default None
25
29
  The directory with configuration files (.json and .yaml).
26
30
 
27
31
  keys: Optional[List[str]], default None
@@ -36,7 +40,10 @@ def read_config(
36
40
 
37
41
  with_filename: bool, default False
38
42
  If `True`, return a tuple of the configuration dictionary with a list of read filenames.
39
-
43
+
44
+ raise_parsing_errors: bool, default False
45
+ If `True`, re-raise parsing exceptions.
46
+
40
47
  Examples
41
48
  --------
42
49
  >>> read_config(keys=['meerschaum'], with_filename=True)
@@ -63,9 +70,9 @@ def read_config(
63
70
 
64
71
  default_filetype = STATIC_CONFIG['config']['default_filetype']
65
72
  filetype_loaders = {
66
- 'yml' : yaml.load,
67
- 'yaml' : yaml.load,
68
- 'json' : json.load,
73
+ 'yml': yaml.load,
74
+ 'yaml': yaml.load,
75
+ 'json': json.load,
69
76
  }
70
77
 
71
78
  ### Construct filekeys (files to parse).
@@ -167,6 +174,8 @@ def read_config(
167
174
  _config_key = filetype_loaders[_type](f)
168
175
  except Exception as e:
169
176
  print(f"Error processing file: {filepath}")
177
+ if raise_parsing_errors:
178
+ raise e
170
179
  import traceback
171
180
  traceback.print_exc()
172
181
  _config_key = {}
@@ -184,6 +193,8 @@ def read_config(
184
193
  config[symlinks_key][key] = _single_key_config[symlinks_key][key]
185
194
  break
186
195
  except Exception as e:
196
+ if raise_parsing_errors:
197
+ raise e
187
198
  print(f"Unable to parse {filename}!")
188
199
  import traceback
189
200
  traceback.print_exc()
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.5.1"
5
+ __version__ = "2.6.0"
@@ -39,7 +39,7 @@ valkey_password = 'MRSM{meerschaum:connectors:valkey:main:password}'
39
39
 
40
40
  env_dict = {
41
41
  'COMPOSE_PROJECT_NAME': 'mrsm',
42
- 'TIMESCALEDB_VERSION': 'latest-pg16-oss',
42
+ 'TIMESCALEDB_VERSION': 'latest-pg16',
43
43
  'POSTGRES_USER': db_user,
44
44
  'POSTGRES_PASSWORD': db_pass,
45
45
  'POSTGRES_DB': db_base,
@@ -148,6 +148,7 @@ STATIC_CONFIG: Dict[str, Any] = {
148
148
  'min_ratio_columns_changed_for_full_astype': 0.5,
149
149
  },
150
150
  'exists_timeout_seconds': 5.0,
151
+ 'static_schema_cache_seconds': 60.0,
151
152
  },
152
153
  'jobs': {
153
154
  'check_restart_seconds': 1.0,
@@ -58,6 +58,7 @@ class APIConnector(Connector):
58
58
  drop_pipe,
59
59
  clear_pipe,
60
60
  get_pipe_columns_types,
61
+ get_pipe_columns_indices,
61
62
  )
62
63
  from ._fetch import fetch
63
64
  from ._plugins import (
@@ -669,11 +669,11 @@ def clear_pipe(
669
669
  kw.pop('force', None)
670
670
  return self.do_action_legacy(
671
671
  ['clear', 'pipes'],
672
- connector_keys = pipe.connector_keys,
673
- metric_keys = pipe.metric_key,
674
- location_keys = pipe.location_key,
675
- force = True,
676
- debug = debug,
672
+ connector_keys=pipe.connector_keys,
673
+ metric_keys=pipe.metric_key,
674
+ location_keys=pipe.location_key,
675
+ force=True,
676
+ debug=debug,
677
677
  **kw
678
678
  )
679
679
 
@@ -690,7 +690,7 @@ def get_pipe_columns_types(
690
690
  ----------
691
691
  pipe: meerschaum.Pipe
692
692
  The pipe whose columns to be queried.
693
-
693
+
694
694
  Returns
695
695
  -------
696
696
  A dictionary mapping column names to their database types.
@@ -707,11 +707,42 @@ def get_pipe_columns_types(
707
707
  r_url = pipe_r_url(pipe) + '/columns/types'
708
708
  response = self.get(
709
709
  r_url,
710
- debug = debug
710
+ debug=debug
711
+ )
712
+ j = response.json()
713
+ if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
714
+ warn(j['detail'])
715
+ return None
716
+ if not isinstance(j, dict):
717
+ warn(response.text)
718
+ return None
719
+ return j
720
+
721
+
722
+ def get_pipe_columns_indices(
723
+ self,
724
+ pipe: mrsm.Pipe,
725
+ debug: bool = False,
726
+ ) -> Union[Dict[str, str], None]:
727
+ """
728
+ Fetch the index information for a pipe.
729
+
730
+ Parameters
731
+ ----------
732
+ pipe: mrsm.Pipe
733
+ The pipe whose columns to be queried.
734
+
735
+ Returns
736
+ -------
737
+ A dictionary mapping column names to a list of associated index information.
738
+ """
739
+ r_url = pipe_r_url(pipe) + '/columns/indices'
740
+ response = self.get(
741
+ r_url,
742
+ debug=debug
711
743
  )
712
744
  j = response.json()
713
745
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
714
- from meerschaum.utils.warnings import warn
715
746
  warn(j['detail'])
716
747
  return None
717
748
  if not isinstance(j, dict):
@@ -67,6 +67,8 @@ class SQLConnector(Connector):
67
67
  get_pipe_columns_types,
68
68
  get_to_sql_dtype,
69
69
  get_pipe_schema,
70
+ create_pipe_table_from_df,
71
+ get_pipe_columns_indices,
70
72
  )
71
73
  from ._plugins import (
72
74
  register_plugin,
@@ -141,9 +143,9 @@ class SQLConnector(Connector):
141
143
  if uri.startswith('postgres') and not uri.startswith('postgresql'):
142
144
  uri = uri.replace('postgres', 'postgresql', 1)
143
145
  if uri.startswith('postgresql') and not uri.startswith('postgresql+'):
144
- uri = uri.replace('postgresql://', 'postgresql+psycopg', 1)
146
+ uri = uri.replace('postgresql://', 'postgresql+psycopg://', 1)
145
147
  if uri.startswith('timescaledb://'):
146
- uri = uri.replace('timescaledb://', 'postgresql://', 1)
148
+ uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
147
149
  flavor = 'timescaledb'
148
150
  kw['uri'] = uri
149
151
  from_uri_params = self.from_uri(kw['uri'], as_dict=True)
@@ -155,7 +157,6 @@ class SQLConnector(Connector):
155
157
  if flavor:
156
158
  kw['flavor'] = flavor
157
159
 
158
-
159
160
  ### set __dict__ in base class
160
161
  super().__init__(
161
162
  'sql',