meerschaum 2.8.4__py3-none-any.whl → 2.9.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 (57) hide show
  1. meerschaum/api/_chunks.py +67 -0
  2. meerschaum/api/dash/callbacks/__init__.py +5 -2
  3. meerschaum/api/dash/callbacks/custom.py +21 -8
  4. meerschaum/api/dash/callbacks/dashboard.py +26 -4
  5. meerschaum/api/dash/callbacks/settings/__init__.py +8 -0
  6. meerschaum/api/dash/callbacks/settings/password_reset.py +76 -0
  7. meerschaum/api/dash/components.py +136 -25
  8. meerschaum/api/dash/pages/__init__.py +1 -0
  9. meerschaum/api/dash/pages/dashboard.py +11 -9
  10. meerschaum/api/dash/pages/plugins.py +31 -27
  11. meerschaum/api/dash/pages/settings/__init__.py +8 -0
  12. meerschaum/api/dash/pages/settings/password_reset.py +63 -0
  13. meerschaum/api/dash/webterm.py +6 -3
  14. meerschaum/api/resources/static/css/dash.css +8 -1
  15. meerschaum/api/resources/templates/termpage.html +4 -0
  16. meerschaum/api/routes/_pipes.py +232 -79
  17. meerschaum/config/_default.py +4 -0
  18. meerschaum/config/_version.py +1 -1
  19. meerschaum/connectors/__init__.py +1 -0
  20. meerschaum/connectors/api/_APIConnector.py +12 -1
  21. meerschaum/connectors/api/_pipes.py +106 -45
  22. meerschaum/connectors/api/_plugins.py +51 -45
  23. meerschaum/connectors/api/_request.py +1 -1
  24. meerschaum/connectors/parse.py +1 -2
  25. meerschaum/connectors/sql/_SQLConnector.py +3 -0
  26. meerschaum/connectors/sql/_cli.py +1 -0
  27. meerschaum/connectors/sql/_create_engine.py +51 -4
  28. meerschaum/connectors/sql/_pipes.py +38 -6
  29. meerschaum/connectors/sql/_sql.py +35 -4
  30. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -0
  31. meerschaum/connectors/valkey/_pipes.py +51 -39
  32. meerschaum/core/Pipe/__init__.py +1 -0
  33. meerschaum/core/Pipe/_data.py +1 -2
  34. meerschaum/core/Pipe/_sync.py +64 -4
  35. meerschaum/plugins/_Plugin.py +21 -5
  36. meerschaum/plugins/__init__.py +32 -8
  37. meerschaum/utils/dataframe.py +139 -2
  38. meerschaum/utils/dtypes/__init__.py +211 -1
  39. meerschaum/utils/dtypes/sql.py +296 -5
  40. meerschaum/utils/formatting/_shell.py +1 -4
  41. meerschaum/utils/misc.py +1 -1
  42. meerschaum/utils/packages/_packages.py +7 -1
  43. meerschaum/utils/sql.py +139 -11
  44. meerschaum/utils/venv/__init__.py +6 -1
  45. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/METADATA +17 -3
  46. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/RECORD +52 -52
  47. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/WHEEL +1 -1
  48. meerschaum/_internal/gui/__init__.py +0 -43
  49. meerschaum/_internal/gui/app/__init__.py +0 -50
  50. meerschaum/_internal/gui/app/_windows.py +0 -74
  51. meerschaum/_internal/gui/app/actions.py +0 -30
  52. meerschaum/_internal/gui/app/pipes.py +0 -47
  53. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/LICENSE +0 -0
  54. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/NOTICE +0 -0
  55. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/entry_points.txt +0 -0
  56. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/top_level.txt +0 -0
  57. {meerschaum-2.8.4.dist-info → meerschaum-2.9.0.dist-info}/zip-safe +0 -0
@@ -31,6 +31,14 @@ def pipe_r_url(
31
31
  )
32
32
 
33
33
 
34
+ def get_pipe_instance_keys(self, pipe: mrsm.Pipe) -> Union[str, None]:
35
+ """
36
+ Return the configured instance keys for a pipe if set,
37
+ else fall back to the default `instance_keys` for this `APIConnector`.
38
+ """
39
+ return pipe.parameters.get('instance_keys', self.instance_keys)
40
+
41
+
34
42
  def register_pipe(
35
43
  self,
36
44
  pipe: mrsm.Pipe,
@@ -40,13 +48,12 @@ def register_pipe(
40
48
  Returns a tuple of (success_bool, response_dict).
41
49
  """
42
50
  from meerschaum.utils.debug import dprint
43
- ### NOTE: if `parameters` is supplied in the Pipe constructor,
44
- ### then `pipe.parameters` will exist and not be fetched from the database.
45
51
  r_url = pipe_r_url(pipe)
46
52
  response = self.post(
47
53
  r_url + '/register',
48
- json = pipe.parameters,
49
- debug = debug,
54
+ json=pipe._attributes.get('parameters', {}),
55
+ params={'instance_keys': self.get_pipe_instance_keys(pipe)},
56
+ debug=debug,
50
57
  )
51
58
  if debug:
52
59
  dprint(response.text)
@@ -79,9 +86,9 @@ def edit_pipe(
79
86
  r_url = pipe_r_url(pipe)
80
87
  response = self.patch(
81
88
  r_url + '/edit',
82
- params = {'patch': patch,},
83
- json = pipe.parameters,
84
- debug = debug,
89
+ params={'patch': patch, 'instance_keys': self.get_pipe_instance_keys(pipe)},
90
+ json=pipe.parameters,
91
+ debug=debug,
85
92
  )
86
93
  if debug:
87
94
  dprint(response.text)
@@ -149,12 +156,13 @@ def fetch_pipes_keys(
149
156
  try:
150
157
  j = self.get(
151
158
  r_url,
152
- params = {
159
+ params={
153
160
  'connector_keys': json.dumps(connector_keys),
154
161
  'metric_keys': json.dumps(metric_keys),
155
162
  'location_keys': json.dumps(location_keys),
156
163
  'tags': json.dumps(tags),
157
164
  'params': json.dumps(params),
165
+ 'instance_keys': self.instance_keys,
158
166
  },
159
167
  debug=debug
160
168
  ).json()
@@ -250,8 +258,10 @@ def sync_pipe(
250
258
  chunks = (df[i] for i in more_itertools.chunked(df, _chunksize))
251
259
 
252
260
  ### Send columns in case the user has defined them locally.
261
+ request_params = kw.copy()
253
262
  if pipe.columns:
254
- kw['columns'] = json.dumps(pipe.columns)
263
+ request_params['columns'] = json.dumps(pipe.columns)
264
+ request_params['instance_keys'] = self.get_pipe_instance_keys(pipe)
255
265
  r_url = pipe_r_url(pipe) + '/data'
256
266
 
257
267
  rowcount = 0
@@ -268,10 +278,9 @@ def sync_pipe(
268
278
  try:
269
279
  response = self.post(
270
280
  r_url,
271
- ### handles check_existing
272
- params = kw,
273
- data = json_str,
274
- debug = debug
281
+ params=request_params,
282
+ data=json_str,
283
+ debug=debug,
275
284
  )
276
285
  except Exception as e:
277
286
  msg = f"Failed to post a chunk to {pipe}:\n{e}"
@@ -323,7 +332,8 @@ def delete_pipe(
323
332
  r_url = pipe_r_url(pipe)
324
333
  response = self.delete(
325
334
  r_url + '/delete',
326
- debug = debug,
335
+ params={'instance_keys': self.get_pipe_instance_keys(pipe)},
336
+ debug=debug,
327
337
  )
328
338
  if debug:
329
339
  dprint(response.text)
@@ -361,7 +371,9 @@ def get_pipe_data(
361
371
  'omit_columns': json.dumps(omit_columns),
362
372
  'begin': begin,
363
373
  'end': end,
364
- 'params': json.dumps(params, default=str)
374
+ 'params': json.dumps(params, default=str),
375
+ 'instance': self.get_pipe_instance_keys(pipe),
376
+ 'as_chunks': as_chunks,
365
377
  },
366
378
  debug=debug
367
379
  )
@@ -402,19 +414,24 @@ def get_pipe_id(
402
414
  self,
403
415
  pipe: mrsm.Pipe,
404
416
  debug: bool = False,
405
- ) -> int:
417
+ ) -> Union[int, str, None]:
406
418
  """Get a Pipe's ID from the API."""
407
419
  from meerschaum.utils.misc import is_int
408
420
  r_url = pipe_r_url(pipe)
409
421
  response = self.get(
410
422
  r_url + '/id',
411
- debug = debug
423
+ params={
424
+ 'instance': self.get_pipe_instance_keys(pipe),
425
+ },
426
+ debug=debug,
412
427
  )
413
428
  if debug:
414
429
  dprint(f"Got pipe ID: {response.text}")
415
430
  try:
416
431
  if is_int(response.text):
417
432
  return int(response.text)
433
+ if response.text and response.text[0] != '{':
434
+ return response.text
418
435
  except Exception as e:
419
436
  warn(f"Failed to get the ID for {pipe}:\n{e}")
420
437
  return None
@@ -438,7 +455,13 @@ def get_pipe_attributes(
438
455
  If the pipe does not exist, return an empty dictionary.
439
456
  """
440
457
  r_url = pipe_r_url(pipe)
441
- response = self.get(r_url + '/attributes', debug=debug)
458
+ response = self.get(
459
+ r_url + '/attributes',
460
+ params={
461
+ 'instance': self.get_pipe_instance_keys(pipe),
462
+ },
463
+ debug=debug
464
+ )
442
465
  try:
443
466
  return json.loads(response.text)
444
467
  except Exception as e:
@@ -477,9 +500,13 @@ def get_sync_time(
477
500
  r_url = pipe_r_url(pipe)
478
501
  response = self.get(
479
502
  r_url + '/sync_time',
480
- json = params,
481
- params = {'newest': newest, 'debug': debug},
482
- debug = debug,
503
+ json=params,
504
+ params={
505
+ 'instance': self.get_pipe_instance_keys(pipe),
506
+ 'newest': newest,
507
+ 'debug': debug,
508
+ },
509
+ debug=debug,
483
510
  )
484
511
  if not response:
485
512
  warn(f"Failed to get the sync time for {pipe}:\n" + response.text)
@@ -520,7 +547,13 @@ def pipe_exists(
520
547
  from meerschaum.utils.debug import dprint
521
548
  from meerschaum.utils.warnings import warn
522
549
  r_url = pipe_r_url(pipe)
523
- response = self.get(r_url + '/exists', debug=debug)
550
+ response = self.get(
551
+ r_url + '/exists',
552
+ params={
553
+ 'instance': self.get_pipe_instance_keys(pipe),
554
+ },
555
+ debug=debug,
556
+ )
524
557
  if not response:
525
558
  warn(f"Failed to check if {pipe} exists:\n{response.text}")
526
559
  return False
@@ -558,8 +591,8 @@ def create_metadata(
558
591
  def get_pipe_rowcount(
559
592
  self,
560
593
  pipe: mrsm.Pipe,
561
- begin: Optional[datetime] = None,
562
- end: Optional[datetime] = None,
594
+ begin: Union[str, datetime, int, None] = None,
595
+ end: Union[str, datetime, int, None] = None,
563
596
  params: Optional[Dict[str, Any]] = None,
564
597
  remote: bool = False,
565
598
  debug: bool = False,
@@ -571,10 +604,10 @@ def get_pipe_rowcount(
571
604
  pipe: 'meerschaum.Pipe':
572
605
  The pipe whose row count we are counting.
573
606
 
574
- begin: Optional[datetime], default None
607
+ begin: Union[str, datetime, int, None], default None
575
608
  If provided, bound the count by this datetime.
576
609
 
577
- end: Optional[datetime]
610
+ end: Union[str, datetime, int, None], default None
578
611
  If provided, bound the count by this datetime.
579
612
 
580
613
  params: Optional[Dict[str, Any]], default None
@@ -596,6 +629,7 @@ def get_pipe_rowcount(
596
629
  'begin': begin,
597
630
  'end': end,
598
631
  'remote': remote,
632
+ 'instance': self.get_pipe_instance_keys(pipe),
599
633
  },
600
634
  debug = debug
601
635
  )
@@ -633,7 +667,10 @@ def drop_pipe(
633
667
  r_url = pipe_r_url(pipe)
634
668
  response = self.delete(
635
669
  r_url + '/drop',
636
- debug = debug,
670
+ params={
671
+ 'instance': self.get_pipe_instance_keys(pipe),
672
+ },
673
+ debug=debug,
637
674
  )
638
675
  if debug:
639
676
  dprint(response.text)
@@ -656,6 +693,9 @@ def drop_pipe(
656
693
  def clear_pipe(
657
694
  self,
658
695
  pipe: mrsm.Pipe,
696
+ begin: Union[str, datetime, int, None] = None,
697
+ end: Union[str, datetime, int, None] = None,
698
+ params: Optional[Dict[str, Any]] = None,
659
699
  debug: bool = False,
660
700
  **kw
661
701
  ) -> SuccessTuple:
@@ -671,20 +711,33 @@ def clear_pipe(
671
711
  -------
672
712
  A success tuple.
673
713
  """
674
- kw.pop('metric_keys', None)
675
- kw.pop('connector_keys', None)
676
- kw.pop('location_keys', None)
677
- kw.pop('action', None)
678
- kw.pop('force', None)
679
- return self.do_action_legacy(
680
- ['clear', 'pipes'],
681
- connector_keys=pipe.connector_keys,
682
- metric_keys=pipe.metric_key,
683
- location_keys=pipe.location_key,
684
- force=True,
714
+ r_url = pipe_r_url(pipe)
715
+ response = self.delete(
716
+ r_url + '/clear',
717
+ params={
718
+ 'begin': begin,
719
+ 'end': end,
720
+ 'params': json.dumps(params),
721
+ 'instance': self.get_pipe_instance_keys(pipe),
722
+ },
685
723
  debug=debug,
686
- **kw
687
724
  )
725
+ if debug:
726
+ dprint(response.text)
727
+
728
+ try:
729
+ data = response.json()
730
+ except Exception as e:
731
+ return False, f"Failed to clear {pipe} with constraints {begin=}, {end=}, {params=}."
732
+
733
+ if isinstance(data, list):
734
+ response_tuple = data[0], data[1]
735
+ elif 'detail' in response.json():
736
+ response_tuple = response.__bool__(), data['detail']
737
+ else:
738
+ response_tuple = response.__bool__(), response.text
739
+
740
+ return response_tuple
688
741
 
689
742
 
690
743
  def get_pipe_columns_types(
@@ -716,7 +769,10 @@ def get_pipe_columns_types(
716
769
  r_url = pipe_r_url(pipe) + '/columns/types'
717
770
  response = self.get(
718
771
  r_url,
719
- debug=debug
772
+ params={
773
+ 'instance': self.get_pipe_instance_keys(pipe),
774
+ },
775
+ debug=debug,
720
776
  )
721
777
  j = response.json()
722
778
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
@@ -748,7 +804,10 @@ def get_pipe_columns_indices(
748
804
  r_url = pipe_r_url(pipe) + '/columns/indices'
749
805
  response = self.get(
750
806
  r_url,
751
- debug=debug
807
+ params={
808
+ 'instance': self.get_pipe_instance_keys(pipe),
809
+ },
810
+ debug=debug,
752
811
  )
753
812
  j = response.json()
754
813
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
@@ -767,14 +826,16 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe, debug: bool = False) -> Dict[str
767
826
  r_url = pipe_r_url(pipe) + '/indices/names'
768
827
  response = self.get(
769
828
  r_url,
770
- debug=debug
829
+ params={
830
+ 'instance': self.get_pipe_instance_keys(pipe),
831
+ },
832
+ debug=debug,
771
833
  )
772
834
  j = response.json()
773
835
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
774
836
  warn(j['detail'])
775
- return None
837
+ return {}
776
838
  if not isinstance(j, dict):
777
839
  warn(response.text)
778
- return None
840
+ return {}
779
841
  return j
780
-
@@ -7,21 +7,25 @@ Manage plugins via the API connector
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import Union, Any, Optional, SuccessTuple, Mapping, Sequence
10
+
11
+ import meerschaum as mrsm
12
+ from meerschaum.utils.typing import Union, Any, Optional, SuccessTuple, List, Dict
13
+
11
14
 
12
15
  def plugin_r_url(
13
- plugin: Union[meerschaum.core.Plugin, str]
14
- ) -> str:
16
+ plugin: Union[mrsm.core.Plugin, str],
17
+ ) -> str:
15
18
  """Generate a relative URL path from a Plugin."""
16
19
  from meerschaum.config.static import STATIC_CONFIG
17
20
  return f"{STATIC_CONFIG['api']['endpoints']['plugins']}/{plugin}"
18
21
 
22
+
19
23
  def register_plugin(
20
- self,
21
- plugin: meerschaum.core.Plugin,
22
- make_archive: bool = True,
23
- debug: bool = False,
24
- ) -> SuccessTuple:
24
+ self,
25
+ plugin: mrsm.core.Plugin,
26
+ make_archive: bool = True,
27
+ debug: bool = False,
28
+ ) -> SuccessTuple:
25
29
  """Register a plugin and upload its archive."""
26
30
  import json
27
31
  archive_path = plugin.make_tar(debug=debug) if make_archive else plugin.archive_path
@@ -34,27 +38,30 @@ def register_plugin(
34
38
  r_url = plugin_r_url(plugin)
35
39
  try:
36
40
  response = self.post(r_url, files=files, params=metadata, debug=debug)
37
- except Exception as e:
41
+ except Exception:
38
42
  return False, f"Failed to register plugin '{plugin}'."
39
43
  finally:
40
44
  file_pointer.close()
41
45
 
42
46
  try:
43
47
  success, msg = json.loads(response.text)
44
- except Exception as e:
48
+ except Exception:
45
49
  return False, response.text
46
50
 
47
51
  return success, msg
48
52
 
53
+
49
54
  def install_plugin(
50
- self,
51
- name: str,
52
- skip_deps: bool = False,
53
- force: bool = False,
54
- debug: bool = False
55
- ) -> SuccessTuple:
55
+ self,
56
+ name: str,
57
+ skip_deps: bool = False,
58
+ force: bool = False,
59
+ debug: bool = False
60
+ ) -> SuccessTuple:
56
61
  """Download and attempt to install a plugin from the API."""
57
- import os, pathlib, json
62
+ import os
63
+ import pathlib
64
+ import json
58
65
  from meerschaum.core import Plugin
59
66
  from meerschaum.config._paths import PLUGINS_TEMP_RESOURCES_PATH
60
67
  from meerschaum.utils.debug import dprint
@@ -75,41 +82,39 @@ def install_plugin(
75
82
  success, msg = tuple(j)
76
83
  elif isinstance(j, dict) and 'detail' in j:
77
84
  success, msg = False, fail_msg
78
- except Exception as e:
85
+ except Exception:
79
86
  success, msg = False, fail_msg
80
87
  return success, msg
81
88
  plugin = Plugin(name, archive_path=archive_path, repo_connector=self)
82
89
  return plugin.install(skip_deps=skip_deps, force=force, debug=debug)
83
90
 
91
+
84
92
  def get_plugins(
85
- self,
86
- user_id : Optional[int] = None,
87
- search_term : Optional[str] = None,
88
- debug : bool = False
89
- ) -> Sequence[str]:
93
+ self,
94
+ user_id: Optional[int] = None,
95
+ search_term: Optional[str] = None,
96
+ debug: bool = False
97
+ ) -> List[str]:
90
98
  """Return a list of registered plugin names.
91
99
 
92
100
  Parameters
93
101
  ----------
94
- user_id :
102
+ user_id: Optional[int], default None
95
103
  If specified, return all plugins from a certain user.
96
- user_id : Optional[int] :
97
- (Default value = None)
98
- search_term : Optional[str] :
99
- (Default value = None)
100
- debug : bool :
101
- (Default value = False)
104
+
105
+ search_term: Optional[str], default None
106
+ If specified, return plugins beginning with this string.
102
107
 
103
108
  Returns
104
109
  -------
105
-
110
+ A list of plugin names.
106
111
  """
107
112
  import json
108
- from meerschaum.utils.warnings import warn, error
113
+ from meerschaum.utils.warnings import error
109
114
  from meerschaum.config.static import STATIC_CONFIG
110
115
  response = self.get(
111
116
  STATIC_CONFIG['api']['endpoints']['plugins'],
112
- params = {'user_id' : user_id, 'search_term' : search_term},
117
+ params = {'user_id': user_id, 'search_term': search_term},
113
118
  use_token = True,
114
119
  debug = debug
115
120
  )
@@ -120,11 +125,12 @@ def get_plugins(
120
125
  error(response.text)
121
126
  return plugins
122
127
 
128
+
123
129
  def get_plugin_attributes(
124
- self,
125
- plugin: meerschaum.core.Plugin,
126
- debug: bool = False
127
- ) -> Mapping[str, Any]:
130
+ self,
131
+ plugin: mrsm.core.Plugin,
132
+ debug: bool = False
133
+ ) -> Dict[str, Any]:
128
134
  """
129
135
  Return a plugin's attributes.
130
136
  """
@@ -136,7 +142,7 @@ def get_plugin_attributes(
136
142
  if isinstance(attributes, str) and attributes and attributes[0] == '{':
137
143
  try:
138
144
  attributes = json.loads(attributes)
139
- except Exception as e:
145
+ except Exception:
140
146
  pass
141
147
  if not isinstance(attributes, dict):
142
148
  error(response.text)
@@ -145,23 +151,23 @@ def get_plugin_attributes(
145
151
  return {}
146
152
  return attributes
147
153
 
154
+
148
155
  def delete_plugin(
149
- self,
150
- plugin: meerschaum.core.Plugin,
151
- debug: bool = False
152
- ) -> SuccessTuple:
156
+ self,
157
+ plugin: mrsm.core.Plugin,
158
+ debug: bool = False
159
+ ) -> SuccessTuple:
153
160
  """Delete a plugin from an API repository."""
154
161
  import json
155
162
  r_url = plugin_r_url(plugin)
156
163
  try:
157
164
  response = self.delete(r_url, debug=debug)
158
- except Exception as e:
165
+ except Exception:
159
166
  return False, f"Failed to delete plugin '{plugin}'."
160
167
 
161
168
  try:
162
169
  success, msg = json.loads(response.text)
163
- except Exception as e:
170
+ except Exception:
164
171
  return False, response.text
165
172
 
166
173
  return success, msg
167
-
@@ -95,7 +95,7 @@ def make_request(
95
95
  return self.session.request(
96
96
  method.upper(),
97
97
  request_url,
98
- headers = headers,
98
+ headers=headers,
99
99
  **kwargs
100
100
  )
101
101
 
@@ -140,7 +140,6 @@ def is_valid_connector_keys(
140
140
  """
141
141
  try:
142
142
  success = parse_connector_keys(keys, construct=False) is not None
143
- except Exception as e:
143
+ except Exception:
144
144
  success = False
145
145
  return success
146
-
@@ -151,6 +151,9 @@ class SQLConnector(Connector):
151
151
  if uri.startswith('timescaledb://'):
152
152
  uri = uri.replace('timescaledb://', 'postgresql+psycopg://', 1)
153
153
  flavor = 'timescaledb'
154
+ if uri.startswith('postgis://'):
155
+ uri = uri.replace('postgis://', 'postgresql+psycopg://', 1)
156
+ flavor = 'postgis'
154
157
  kw['uri'] = uri
155
158
  from_uri_params = self.from_uri(kw['uri'], as_dict=True)
156
159
  label = label or from_uri_params.get('label', None)
@@ -15,6 +15,7 @@ from meerschaum.utils.typing import SuccessTuple
15
15
 
16
16
  flavor_clis = {
17
17
  'postgresql' : 'pgcli',
18
+ 'postgis' : 'pgcli',
18
19
  'timescaledb' : 'pgcli',
19
20
  'cockroachdb' : 'pgcli',
20
21
  'citus' : 'pgcli',
@@ -7,6 +7,7 @@ This module contains the logic that builds the sqlalchemy engine string.
7
7
  """
8
8
 
9
9
  import traceback
10
+ import meerschaum as mrsm
10
11
  from meerschaum.utils.debug import dprint
11
12
 
12
13
  ### determine driver and requirements from flavor
@@ -47,6 +48,16 @@ flavor_configs = {
47
48
  'port': 5432,
48
49
  },
49
50
  },
51
+ 'postgis': {
52
+ 'engine': 'postgresql+psycopg',
53
+ 'create_engine': default_create_engine_args,
54
+ 'omit_create_engine': {'method',},
55
+ 'to_sql': {},
56
+ 'requirements': default_requirements,
57
+ 'defaults': {
58
+ 'port': 5432,
59
+ },
60
+ },
50
61
  'citus': {
51
62
  'engine': 'postgresql+psycopg',
52
63
  'create_engine': default_create_engine_args,
@@ -162,6 +173,7 @@ install_flavor_drivers = {
162
173
  'mariadb': ['pymysql'],
163
174
  'timescaledb': ['psycopg'],
164
175
  'postgresql': ['psycopg'],
176
+ 'postgis': ['psycopg', 'geoalchemy'],
165
177
  'citus': ['psycopg'],
166
178
  'cockroachdb': ['psycopg', 'sqlalchemy_cockroachdb', 'sqlalchemy_cockroachdb.psycopg'],
167
179
  'mssql': ['pyodbc'],
@@ -198,8 +210,7 @@ def create_engine(
198
210
  warn=False,
199
211
  )
200
212
  if self.flavor == 'mssql':
201
- pyodbc = attempt_import('pyodbc', debug=debug, lazy=False, warn=False)
202
- pyodbc.pooling = False
213
+ _init_mssql_sqlalchemy()
203
214
  if self.flavor in require_patching_flavors:
204
215
  from meerschaum.utils.packages import determine_version, _monkey_patch_get_distribution
205
216
  import pathlib
@@ -257,8 +268,8 @@ def create_engine(
257
268
 
258
269
  ### Sometimes the timescaledb:// flavor can slip in.
259
270
  if _uri and self.flavor in _uri:
260
- if self.flavor == 'timescaledb':
261
- engine_str = engine_str.replace(f'{self.flavor}', 'postgresql', 1)
271
+ if self.flavor in ('timescaledb', 'postgis'):
272
+ engine_str = engine_str.replace(self.flavor, 'postgresql', 1)
262
273
  elif _uri.startswith('postgresql://'):
263
274
  engine_str = engine_str.replace('postgresql://', 'postgresql+psycopg2://')
264
275
 
@@ -313,3 +324,39 @@ def create_engine(
313
324
  if include_uri:
314
325
  return engine, engine_str
315
326
  return engine
327
+
328
+
329
+ def _init_mssql_sqlalchemy():
330
+ """
331
+ When first instantiating a SQLAlchemy connection to MSSQL,
332
+ monkey-patch `pyodbc` handling in SQLAlchemy.
333
+ """
334
+ pyodbc, sqlalchemy_dialects_mssql_pyodbc = mrsm.attempt_import(
335
+ 'pyodbc',
336
+ 'sqlalchemy.dialects.mssql.pyodbc',
337
+ lazy=False,
338
+ warn=False,
339
+ )
340
+ pyodbc.pooling = False
341
+
342
+ MSDialect_pyodbc = sqlalchemy_dialects_mssql_pyodbc.MSDialect_pyodbc
343
+
344
+ def _handle_geometry(val):
345
+ from binascii import hexlify
346
+ hex_str = f"0x{hexlify(val).decode().upper()}"
347
+ return hex_str
348
+
349
+ def custom_on_connect(self):
350
+ super_ = super(MSDialect_pyodbc, self).on_connect()
351
+
352
+ def _on_connect(conn):
353
+ if super_ is not None:
354
+ super_(conn)
355
+
356
+ self._setup_timestampoffset_type(conn)
357
+ conn.add_output_converter(-151, _handle_geometry)
358
+
359
+ return _on_connect
360
+
361
+ ### TODO: Parse proprietary MSSQL geometry bytes into WKB.
362
+ # MSDialect_pyodbc.on_connect = custom_on_connect