meerschaum 2.9.0rc2__py3-none-any.whl → 2.9.1__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 (40) hide show
  1. meerschaum/api/dash/callbacks/__init__.py +5 -2
  2. meerschaum/api/dash/callbacks/custom.py +17 -25
  3. meerschaum/api/dash/callbacks/dashboard.py +5 -21
  4. meerschaum/api/dash/callbacks/settings/__init__.py +8 -0
  5. meerschaum/api/dash/callbacks/settings/password_reset.py +76 -0
  6. meerschaum/api/dash/components.py +110 -7
  7. meerschaum/api/dash/pages/__init__.py +1 -0
  8. meerschaum/api/dash/pages/settings/__init__.py +8 -0
  9. meerschaum/api/dash/pages/settings/password_reset.py +63 -0
  10. meerschaum/api/resources/static/css/dash.css +7 -0
  11. meerschaum/api/routes/_pipes.py +76 -36
  12. meerschaum/config/_version.py +1 -1
  13. meerschaum/connectors/__init__.py +1 -0
  14. meerschaum/connectors/api/_pipes.py +79 -30
  15. meerschaum/connectors/sql/_pipes.py +38 -5
  16. meerschaum/connectors/valkey/_ValkeyConnector.py +2 -0
  17. meerschaum/connectors/valkey/_pipes.py +51 -39
  18. meerschaum/core/Pipe/__init__.py +1 -0
  19. meerschaum/core/Pipe/_sync.py +64 -4
  20. meerschaum/plugins/__init__.py +26 -4
  21. meerschaum/utils/dataframe.py +58 -3
  22. meerschaum/utils/dtypes/__init__.py +45 -17
  23. meerschaum/utils/dtypes/sql.py +182 -3
  24. meerschaum/utils/misc.py +1 -1
  25. meerschaum/utils/packages/_packages.py +6 -3
  26. meerschaum/utils/sql.py +122 -6
  27. meerschaum/utils/venv/__init__.py +4 -1
  28. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/METADATA +14 -9
  29. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/RECORD +35 -36
  30. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/WHEEL +1 -1
  31. meerschaum/_internal/gui/__init__.py +0 -43
  32. meerschaum/_internal/gui/app/__init__.py +0 -50
  33. meerschaum/_internal/gui/app/_windows.py +0 -74
  34. meerschaum/_internal/gui/app/actions.py +0 -30
  35. meerschaum/_internal/gui/app/pipes.py +0 -47
  36. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/LICENSE +0 -0
  37. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/NOTICE +0 -0
  38. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/entry_points.txt +0 -0
  39. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/top_level.txt +0 -0
  40. {meerschaum-2.9.0rc2.dist-info → meerschaum-2.9.1.dist-info}/zip-safe +0 -0
@@ -414,19 +414,24 @@ def get_pipe_id(
414
414
  self,
415
415
  pipe: mrsm.Pipe,
416
416
  debug: bool = False,
417
- ) -> int:
417
+ ) -> Union[int, str, None]:
418
418
  """Get a Pipe's ID from the API."""
419
419
  from meerschaum.utils.misc import is_int
420
420
  r_url = pipe_r_url(pipe)
421
421
  response = self.get(
422
422
  r_url + '/id',
423
- debug = debug
423
+ params={
424
+ 'instance': self.get_pipe_instance_keys(pipe),
425
+ },
426
+ debug=debug,
424
427
  )
425
428
  if debug:
426
429
  dprint(f"Got pipe ID: {response.text}")
427
430
  try:
428
431
  if is_int(response.text):
429
432
  return int(response.text)
433
+ if response.text and response.text[0] != '{':
434
+ return response.text
430
435
  except Exception as e:
431
436
  warn(f"Failed to get the ID for {pipe}:\n{e}")
432
437
  return None
@@ -450,7 +455,13 @@ def get_pipe_attributes(
450
455
  If the pipe does not exist, return an empty dictionary.
451
456
  """
452
457
  r_url = pipe_r_url(pipe)
453
- 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
+ )
454
465
  try:
455
466
  return json.loads(response.text)
456
467
  except Exception as e:
@@ -489,9 +500,13 @@ def get_sync_time(
489
500
  r_url = pipe_r_url(pipe)
490
501
  response = self.get(
491
502
  r_url + '/sync_time',
492
- json = params,
493
- params = {'newest': newest, 'debug': debug},
494
- 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,
495
510
  )
496
511
  if not response:
497
512
  warn(f"Failed to get the sync time for {pipe}:\n" + response.text)
@@ -532,7 +547,13 @@ def pipe_exists(
532
547
  from meerschaum.utils.debug import dprint
533
548
  from meerschaum.utils.warnings import warn
534
549
  r_url = pipe_r_url(pipe)
535
- 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
+ )
536
557
  if not response:
537
558
  warn(f"Failed to check if {pipe} exists:\n{response.text}")
538
559
  return False
@@ -570,8 +591,8 @@ def create_metadata(
570
591
  def get_pipe_rowcount(
571
592
  self,
572
593
  pipe: mrsm.Pipe,
573
- begin: Optional[datetime] = None,
574
- end: Optional[datetime] = None,
594
+ begin: Union[str, datetime, int, None] = None,
595
+ end: Union[str, datetime, int, None] = None,
575
596
  params: Optional[Dict[str, Any]] = None,
576
597
  remote: bool = False,
577
598
  debug: bool = False,
@@ -583,10 +604,10 @@ def get_pipe_rowcount(
583
604
  pipe: 'meerschaum.Pipe':
584
605
  The pipe whose row count we are counting.
585
606
 
586
- begin: Optional[datetime], default None
607
+ begin: Union[str, datetime, int, None], default None
587
608
  If provided, bound the count by this datetime.
588
609
 
589
- end: Optional[datetime]
610
+ end: Union[str, datetime, int, None], default None
590
611
  If provided, bound the count by this datetime.
591
612
 
592
613
  params: Optional[Dict[str, Any]], default None
@@ -608,6 +629,7 @@ def get_pipe_rowcount(
608
629
  'begin': begin,
609
630
  'end': end,
610
631
  'remote': remote,
632
+ 'instance': self.get_pipe_instance_keys(pipe),
611
633
  },
612
634
  debug = debug
613
635
  )
@@ -645,7 +667,10 @@ def drop_pipe(
645
667
  r_url = pipe_r_url(pipe)
646
668
  response = self.delete(
647
669
  r_url + '/drop',
648
- debug = debug,
670
+ params={
671
+ 'instance': self.get_pipe_instance_keys(pipe),
672
+ },
673
+ debug=debug,
649
674
  )
650
675
  if debug:
651
676
  dprint(response.text)
@@ -668,6 +693,9 @@ def drop_pipe(
668
693
  def clear_pipe(
669
694
  self,
670
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,
671
699
  debug: bool = False,
672
700
  **kw
673
701
  ) -> SuccessTuple:
@@ -683,20 +711,33 @@ def clear_pipe(
683
711
  -------
684
712
  A success tuple.
685
713
  """
686
- kw.pop('metric_keys', None)
687
- kw.pop('connector_keys', None)
688
- kw.pop('location_keys', None)
689
- kw.pop('action', None)
690
- kw.pop('force', None)
691
- return self.do_action_legacy(
692
- ['clear', 'pipes'],
693
- connector_keys=pipe.connector_keys,
694
- metric_keys=pipe.metric_key,
695
- location_keys=pipe.location_key,
696
- 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
+ },
697
723
  debug=debug,
698
- **kw
699
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
700
741
 
701
742
 
702
743
  def get_pipe_columns_types(
@@ -728,7 +769,10 @@ def get_pipe_columns_types(
728
769
  r_url = pipe_r_url(pipe) + '/columns/types'
729
770
  response = self.get(
730
771
  r_url,
731
- debug=debug
772
+ params={
773
+ 'instance': self.get_pipe_instance_keys(pipe),
774
+ },
775
+ debug=debug,
732
776
  )
733
777
  j = response.json()
734
778
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
@@ -760,7 +804,10 @@ def get_pipe_columns_indices(
760
804
  r_url = pipe_r_url(pipe) + '/columns/indices'
761
805
  response = self.get(
762
806
  r_url,
763
- debug=debug
807
+ params={
808
+ 'instance': self.get_pipe_instance_keys(pipe),
809
+ },
810
+ debug=debug,
764
811
  )
765
812
  j = response.json()
766
813
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
@@ -779,14 +826,16 @@ def get_pipe_index_names(self, pipe: mrsm.Pipe, debug: bool = False) -> Dict[str
779
826
  r_url = pipe_r_url(pipe) + '/indices/names'
780
827
  response = self.get(
781
828
  r_url,
782
- debug=debug
829
+ params={
830
+ 'instance': self.get_pipe_instance_keys(pipe),
831
+ },
832
+ debug=debug,
783
833
  )
784
834
  j = response.json()
785
835
  if isinstance(j, dict) and 'detail' in j and len(j.keys()) == 1:
786
836
  warn(j['detail'])
787
- return None
837
+ return {}
788
838
  if not isinstance(j, dict):
789
839
  warn(response.text)
790
- return None
840
+ return {}
791
841
  return j
792
-
@@ -499,6 +499,7 @@ def get_create_index_queries(
499
499
  get_rename_table_queries,
500
500
  COALESCE_UNIQUE_INDEX_FLAVORS,
501
501
  )
502
+ from meerschaum.utils.dtypes import are_dtypes_equal
502
503
  from meerschaum.utils.dtypes.sql import (
503
504
  get_db_type_from_pd_type,
504
505
  get_pd_type_from_db_type,
@@ -793,8 +794,19 @@ def get_create_index_queries(
793
794
  cols_names = [sql_item_name(col, self.flavor, None) for col in cols if col]
794
795
  if not cols_names:
795
796
  continue
797
+
796
798
  cols_names_str = ", ".join(cols_names)
797
- index_queries[ix_key] = [f"CREATE INDEX {ix_name} ON {_pipe_name} ({cols_names_str})"]
799
+ index_query_params_clause = f" ({cols_names_str})"
800
+ if self.flavor == 'postgis':
801
+ for col in cols:
802
+ col_typ = existing_cols_pd_types.get(cols[0], 'object')
803
+ if col_typ != 'object' and are_dtypes_equal(col_typ, 'geometry'):
804
+ index_query_params_clause = f" USING GIST ({cols_names_str})"
805
+ break
806
+
807
+ index_queries[ix_key] = [
808
+ f"CREATE INDEX {ix_name} ON {_pipe_name}{index_query_params_clause}"
809
+ ]
798
810
 
799
811
  indices_cols_str = ', '.join(
800
812
  list({
@@ -1544,7 +1556,11 @@ def create_pipe_table_from_df(
1544
1556
  get_datetime_cols,
1545
1557
  get_bytes_cols,
1546
1558
  )
1547
- from meerschaum.utils.sql import get_create_table_queries, sql_item_name
1559
+ from meerschaum.utils.sql import (
1560
+ get_create_table_queries,
1561
+ sql_item_name,
1562
+ get_create_schema_if_not_exists_queries,
1563
+ )
1548
1564
  from meerschaum.utils.dtypes.sql import get_db_type_from_pd_type
1549
1565
  primary_key = pipe.columns.get('primary', None)
1550
1566
  primary_key_typ = (
@@ -1601,15 +1617,21 @@ def create_pipe_table_from_df(
1601
1617
  if autoincrement:
1602
1618
  _ = new_dtypes.pop(primary_key, None)
1603
1619
 
1620
+ schema = self.get_pipe_schema(pipe)
1604
1621
  create_table_queries = get_create_table_queries(
1605
1622
  new_dtypes,
1606
1623
  pipe.target,
1607
1624
  self.flavor,
1608
- schema=self.get_pipe_schema(pipe),
1625
+ schema=schema,
1609
1626
  primary_key=primary_key,
1610
1627
  primary_key_db_type=primary_key_db_type,
1611
1628
  datetime_column=dt_col,
1612
1629
  )
1630
+ if schema:
1631
+ create_table_queries = (
1632
+ get_create_schema_if_not_exists_queries(schema, self.flavor)
1633
+ + create_table_queries
1634
+ )
1613
1635
  success = all(
1614
1636
  self.exec_queries(create_table_queries, break_on_error=True, rollback=True, debug=debug)
1615
1637
  )
@@ -2085,6 +2107,7 @@ def sync_pipe_inplace(
2085
2107
  get_update_queries,
2086
2108
  get_null_replacement,
2087
2109
  get_create_table_queries,
2110
+ get_create_schema_if_not_exists_queries,
2088
2111
  get_table_cols_types,
2089
2112
  session_execute,
2090
2113
  dateadd_str,
@@ -2164,18 +2187,28 @@ def sync_pipe_inplace(
2164
2187
  warn(drop_stale_msg)
2165
2188
  return drop_stale_success, drop_stale_msg
2166
2189
 
2167
- sqlalchemy, sqlalchemy_orm = mrsm.attempt_import('sqlalchemy', 'sqlalchemy.orm')
2190
+ sqlalchemy, sqlalchemy_orm = mrsm.attempt_import(
2191
+ 'sqlalchemy',
2192
+ 'sqlalchemy.orm',
2193
+ )
2168
2194
  if not pipe.exists(debug=debug):
2195
+ schema = self.get_pipe_schema(pipe)
2169
2196
  create_pipe_queries = get_create_table_queries(
2170
2197
  metadef,
2171
2198
  pipe.target,
2172
2199
  self.flavor,
2173
- schema=self.get_pipe_schema(pipe),
2200
+ schema=schema,
2174
2201
  primary_key=primary_key,
2175
2202
  primary_key_db_type=primary_key_db_type,
2176
2203
  autoincrement=autoincrement,
2177
2204
  datetime_column=dt_col,
2178
2205
  )
2206
+ if schema:
2207
+ create_pipe_queries = (
2208
+ get_create_schema_if_not_exists_queries(schema, self.flavor)
2209
+ + create_pipe_queries
2210
+ )
2211
+
2179
2212
  results = self.exec_queries(create_pipe_queries, debug=debug)
2180
2213
  if not all(results):
2181
2214
  _ = clean_up_temp_tables()
@@ -50,6 +50,8 @@ class ValkeyConnector(Connector):
50
50
  get_sync_time,
51
51
  get_pipe_rowcount,
52
52
  fetch_pipes_keys,
53
+ get_document_key,
54
+ get_table_quoted_doc_key,
53
55
  )
54
56
  from ._fetch import (
55
57
  fetch,
@@ -10,8 +10,9 @@ from datetime import datetime, timezone
10
10
 
11
11
  import meerschaum as mrsm
12
12
  from meerschaum.utils.typing import SuccessTuple, Any, Union, Optional, Dict, List, Tuple
13
- from meerschaum.utils.misc import json_serialize_datetime, string_to_dict
14
- from meerschaum.utils.warnings import warn
13
+ from meerschaum.utils.misc import string_to_dict
14
+ from meerschaum.utils.dtypes import json_serialize_value
15
+ from meerschaum.utils.warnings import warn, dprint
15
16
  from meerschaum.config.static import STATIC_CONFIG
16
17
 
17
18
  PIPES_TABLE: str = 'mrsm_pipes'
@@ -46,25 +47,15 @@ def serialize_document(doc: Dict[str, Any]) -> str:
46
47
  -------
47
48
  A serialized string for the document.
48
49
  """
49
- from meerschaum.utils.dtypes import serialize_bytes
50
50
  return json.dumps(
51
51
  doc,
52
- default=(
53
- lambda x: (
54
- json_serialize_datetime(x)
55
- if hasattr(x, 'tzinfo')
56
- else (
57
- serialize_bytes(x)
58
- if isinstance(x, bytes)
59
- else str(x)
60
- )
61
- )
62
- ),
52
+ default=json_serialize_value,
63
53
  separators=(',', ':'),
64
54
  sort_keys=True,
65
55
  )
66
56
 
67
57
 
58
+ @staticmethod
68
59
  def get_document_key(
69
60
  doc: Dict[str, Any],
70
61
  indices: List[str],
@@ -91,25 +82,39 @@ def get_document_key(
91
82
  from meerschaum.utils.dtypes import coerce_timezone
92
83
  index_vals = {
93
84
  key: (
94
- str(val)
85
+ str(val).replace(':', COLON)
95
86
  if not isinstance(val, datetime)
96
87
  else str(int(coerce_timezone(val).replace(tzinfo=timezone.utc).timestamp()))
97
88
  )
98
89
  for key, val in doc.items()
99
- if key in indices
100
- } if indices else {}
101
- indices_str = ((table_name + ':indices:') if table_name else '') + ','.join(
102
- sorted(
103
- [
104
- f'{key}{COLON}{val}'
105
- for key, val in index_vals.items()
106
- ]
90
+ if ((key in indices) if indices else True)
91
+ }
92
+ indices_str = (
93
+ (
94
+ (
95
+ (
96
+ table_name
97
+ + ':'
98
+ + ('indices:' if True else '')
99
+ )
100
+ )
101
+ if table_name
102
+ else ''
103
+ ) + ','.join(
104
+ sorted(
105
+ [
106
+ f'{key}{COLON}{val}'
107
+ for key, val in index_vals.items()
108
+ ]
109
+ )
107
110
  )
108
- ) if indices else serialize_document(doc)
111
+ )
109
112
  return indices_str
110
113
 
111
114
 
115
+ @classmethod
112
116
  def get_table_quoted_doc_key(
117
+ cls,
113
118
  table_name: str,
114
119
  doc: Dict[str, Any],
115
120
  indices: List[str],
@@ -120,7 +125,7 @@ def get_table_quoted_doc_key(
120
125
  """
121
126
  return json.dumps(
122
127
  {
123
- get_document_key(doc, indices, table_name): serialize_document(doc),
128
+ cls.get_document_key(doc, indices, table_name): serialize_document(doc),
124
129
  **(
125
130
  {datetime_column: doc.get(datetime_column, 0)}
126
131
  if datetime_column
@@ -129,7 +134,7 @@ def get_table_quoted_doc_key(
129
134
  },
130
135
  sort_keys=True,
131
136
  separators=(',', ':'),
132
- default=(lambda x: json_serialize_datetime(x) if hasattr(x, 'tzinfo') else str(x)),
137
+ default=json_serialize_value,
133
138
  )
134
139
 
135
140
 
@@ -377,7 +382,7 @@ def delete_pipe(
377
382
  doc = docs[0]
378
383
  doc_str = json.dumps(
379
384
  doc,
380
- default=(lambda x: json_serialize_datetime(x) if hasattr(x, 'tzinfo') else str(x)),
385
+ default=json_serialize_value,
381
386
  separators=(',', ':'),
382
387
  sort_keys=True,
383
388
  )
@@ -443,11 +448,16 @@ def get_pipe_data(
443
448
  debug=debug,
444
449
  )
445
450
  ]
451
+ print(f"{ix_docs=}")
446
452
  try:
447
453
  docs_strings = [
448
- self.get(get_document_key(
449
- doc, indices, table_name
450
- ))
454
+ self.get(
455
+ self.get_document_key(
456
+ doc,
457
+ indices,
458
+ table_name,
459
+ )
460
+ )
451
461
  for doc in ix_docs
452
462
  ]
453
463
  except Exception as e:
@@ -535,7 +545,7 @@ def sync_pipe(
535
545
  def _serialize_indices_docs(_docs):
536
546
  return [
537
547
  {
538
- 'ix': get_document_key(doc, indices),
548
+ 'ix': self.get_document_key(doc, indices),
539
549
  **(
540
550
  {
541
551
  dt_col: doc.get(dt_col, 0)
@@ -594,7 +604,7 @@ def sync_pipe(
594
604
  unseen_docs = unseen_df.to_dict(orient='records') if unseen_df is not None else []
595
605
  unseen_indices_docs = _serialize_indices_docs(unseen_docs)
596
606
  unseen_ix_vals = {
597
- get_document_key(doc, indices, table_name): serialize_document(doc)
607
+ self.get_document_key(doc, indices, table_name): serialize_document(doc)
598
608
  for doc in unseen_docs
599
609
  }
600
610
  for key, val in unseen_ix_vals.items():
@@ -615,7 +625,7 @@ def sync_pipe(
615
625
 
616
626
  update_docs = update_df.to_dict(orient='records') if update_df is not None else []
617
627
  update_ix_docs = {
618
- get_document_key(doc, indices, table_name): doc
628
+ self.get_document_key(doc, indices, table_name): doc
619
629
  for doc in update_docs
620
630
  }
621
631
  existing_docs_data = {
@@ -633,7 +643,7 @@ def sync_pipe(
633
643
  if key not in existing_docs
634
644
  }
635
645
  new_ix_vals = {
636
- get_document_key(doc, indices, table_name): serialize_document(doc)
646
+ self.get_document_key(doc, indices, table_name): serialize_document(doc)
637
647
  for doc in new_update_docs.values()
638
648
  }
639
649
  for key, val in new_ix_vals.items():
@@ -743,8 +753,8 @@ def clear_pipe(
743
753
  table_name = self.quote_table(pipe.target)
744
754
  indices = [col for col in pipe.columns.values() if col]
745
755
  for doc in docs:
746
- set_doc_key = get_document_key(doc, indices)
747
- table_doc_key = get_document_key(doc, indices, table_name)
756
+ set_doc_key = self.get_document_key(doc, indices)
757
+ table_doc_key = self.get_document_key(doc, indices, table_name)
748
758
  try:
749
759
  if dt_col:
750
760
  self.client.zrem(table_name, set_doc_key)
@@ -826,13 +836,15 @@ def get_pipe_rowcount(
826
836
  return 0
827
837
 
828
838
  try:
829
- if begin is None and end is None and params is None:
839
+ if begin is None and end is None and not params:
830
840
  return (
831
841
  self.client.zcard(table_name)
832
842
  if dt_col
833
- else self.client.llen(table_name)
843
+ else self.client.scard(table_name)
834
844
  )
835
- except Exception:
845
+ except Exception as e:
846
+ if debug:
847
+ dprint(f"Failed to get rowcount for {pipe}:\n{e}")
836
848
  return None
837
849
 
838
850
  df = pipe.get_data(begin=begin, end=end, params=params, debug=debug)
@@ -137,6 +137,7 @@ class Pipe:
137
137
  _persist_new_numeric_columns,
138
138
  _persist_new_uuid_columns,
139
139
  _persist_new_bytes_columns,
140
+ _persist_new_geometry_columns,
140
141
  )
141
142
  from ._verify import (
142
143
  verify,
@@ -158,6 +158,7 @@ def sync(
158
158
  'error_callback': error_callback,
159
159
  'sync_chunks': sync_chunks,
160
160
  'chunksize': chunksize,
161
+ 'safe_copy': True,
161
162
  })
162
163
 
163
164
  ### NOTE: Invalidate `_exists` cache before and after syncing.
@@ -268,6 +269,7 @@ def sync(
268
269
  **kw
269
270
  )
270
271
  )
272
+ kw['safe_copy'] = False
271
273
  except Exception as e:
272
274
  get_console().print_exception(
273
275
  suppress=[
@@ -402,6 +404,7 @@ def sync(
402
404
  self._persist_new_numeric_columns(df, debug=debug)
403
405
  self._persist_new_uuid_columns(df, debug=debug)
404
406
  self._persist_new_bytes_columns(df, debug=debug)
407
+ self._persist_new_geometry_columns(df, debug=debug)
405
408
 
406
409
  if debug:
407
410
  dprint(
@@ -1009,7 +1012,7 @@ def _persist_new_numeric_columns(self, df, debug: bool = False) -> SuccessTuple:
1009
1012
 
1010
1013
  self._attributes_sync_time = None
1011
1014
  dtypes = self.parameters.get('dtypes', {})
1012
- dtypes.update({col: 'numeric' for col in numeric_cols})
1015
+ dtypes.update({col: 'numeric' for col in new_numeric_cols})
1013
1016
  self.parameters['dtypes'] = dtypes
1014
1017
  if not self.temporary:
1015
1018
  edit_success, edit_msg = self.edit(interactive=False, debug=debug)
@@ -1034,7 +1037,7 @@ def _persist_new_uuid_columns(self, df, debug: bool = False) -> SuccessTuple:
1034
1037
 
1035
1038
  self._attributes_sync_time = None
1036
1039
  dtypes = self.parameters.get('dtypes', {})
1037
- dtypes.update({col: 'uuid' for col in uuid_cols})
1040
+ dtypes.update({col: 'uuid' for col in new_uuid_cols})
1038
1041
  self.parameters['dtypes'] = dtypes
1039
1042
  if not self.temporary:
1040
1043
  edit_success, edit_msg = self.edit(interactive=False, debug=debug)
@@ -1059,7 +1062,7 @@ def _persist_new_json_columns(self, df, debug: bool = False) -> SuccessTuple:
1059
1062
 
1060
1063
  self._attributes_sync_time = None
1061
1064
  dtypes = self.parameters.get('dtypes', {})
1062
- dtypes.update({col: 'json' for col in json_cols})
1065
+ dtypes.update({col: 'json' for col in new_json_cols})
1063
1066
  self.parameters['dtypes'] = dtypes
1064
1067
 
1065
1068
  if not self.temporary:
@@ -1085,7 +1088,64 @@ def _persist_new_bytes_columns(self, df, debug: bool = False) -> SuccessTuple:
1085
1088
 
1086
1089
  self._attributes_sync_time = None
1087
1090
  dtypes = self.parameters.get('dtypes', {})
1088
- dtypes.update({col: 'bytes' for col in bytes_cols})
1091
+ dtypes.update({col: 'bytes' for col in new_bytes_cols})
1092
+ self.parameters['dtypes'] = dtypes
1093
+
1094
+ if not self.temporary:
1095
+ edit_success, edit_msg = self.edit(interactive=False, debug=debug)
1096
+ if not edit_success:
1097
+ warn(f"Unable to update bytes dtypes for {self}:\n{edit_msg}")
1098
+
1099
+ return edit_success, edit_msg
1100
+
1101
+ return True, "Success"
1102
+
1103
+
1104
+ def _persist_new_geometry_columns(self, df, debug: bool = False) -> SuccessTuple:
1105
+ """
1106
+ Check for new `geometry` columns and update the parameters.
1107
+ """
1108
+ from meerschaum.utils.dataframe import get_geometry_cols
1109
+ geometry_cols_types_srids = get_geometry_cols(df, with_types_srids=True)
1110
+ existing_geometry_cols = [
1111
+ col
1112
+ for col, typ in self.dtypes.items()
1113
+ if typ.startswith('geometry') or typ.startswith('geography')
1114
+ ]
1115
+ new_geometry_cols = [
1116
+ col
1117
+ for col in geometry_cols_types_srids
1118
+ if col not in existing_geometry_cols
1119
+ ]
1120
+ if not new_geometry_cols:
1121
+ return True, "Success"
1122
+
1123
+ self._attributes_sync_time = None
1124
+ dtypes = self.parameters.get('dtypes', {})
1125
+
1126
+ new_cols_types = {}
1127
+ for col, (geometry_type, srid) in geometry_cols_types_srids.items():
1128
+ if col not in new_geometry_cols:
1129
+ continue
1130
+
1131
+ new_dtype = "geometry"
1132
+ modifier = ""
1133
+ if not srid and geometry_type.lower() == 'geometry':
1134
+ new_cols_types[col] = new_dtype
1135
+ continue
1136
+
1137
+ modifier = "["
1138
+ if geometry_type.lower() != 'geometry':
1139
+ modifier += f"{geometry_type}"
1140
+
1141
+ if srid:
1142
+ if modifier != '[':
1143
+ modifier += ", "
1144
+ modifier += f"{srid}"
1145
+ modifier += "]"
1146
+ new_cols_types[col] = f"{new_dtype}{modifier}"
1147
+
1148
+ dtypes.update(new_cols_types)
1089
1149
  self.parameters['dtypes'] = dtypes
1090
1150
 
1091
1151
  if not self.temporary: