meerschaum 2.9.0.dev1__py3-none-any.whl → 2.9.0rc2__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.
@@ -70,6 +70,7 @@ default_system_config = {
70
70
  'sql': {
71
71
  'bulk_insert': {
72
72
  'postgresql': True,
73
+ 'postgis': True,
73
74
  'citus': True,
74
75
  'timescaledb': True,
75
76
  'mssql': True,
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.9.0.dev1"
5
+ __version__ = "2.9.0rc2"
@@ -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
@@ -731,7 +731,7 @@ def get_create_index_queries(
731
731
  ) + f"{primary_key_name})"
732
732
  ),
733
733
  ])
734
- elif self.flavor in ('citus', 'postgresql', 'duckdb'):
734
+ elif self.flavor in ('citus', 'postgresql', 'duckdb', 'postgis'):
735
735
  primary_queries.extend([
736
736
  (
737
737
  f"ALTER TABLE {_pipe_name}\n"
@@ -1052,6 +1052,7 @@ def get_pipe_data(
1052
1052
  attempt_cast_to_numeric,
1053
1053
  attempt_cast_to_uuid,
1054
1054
  attempt_cast_to_bytes,
1055
+ attempt_cast_to_geometry,
1055
1056
  are_dtypes_equal,
1056
1057
  )
1057
1058
  from meerschaum.utils.dtypes.sql import get_pd_type_from_db_type
@@ -1138,6 +1139,11 @@ def get_pipe_data(
1138
1139
  for col, typ in pipe.dtypes.items()
1139
1140
  if typ == 'bytes' and col in dtypes
1140
1141
  ]
1142
+ geometry_columns = [
1143
+ col
1144
+ for col, typ in pipe.dtypes.items()
1145
+ if typ.startswith('geometry') and col in dtypes
1146
+ ]
1141
1147
 
1142
1148
  kw['coerce_float'] = kw.get('coerce_float', (len(numeric_columns) == 0))
1143
1149
 
@@ -1162,6 +1168,11 @@ def get_pipe_data(
1162
1168
  continue
1163
1169
  df[col] = df[col].apply(attempt_cast_to_bytes)
1164
1170
 
1171
+ for col in geometry_columns:
1172
+ if col not in df.columns:
1173
+ continue
1174
+ df[col] = df[col].apply(attempt_cast_to_geometry)
1175
+
1165
1176
  if self.flavor == 'sqlite':
1166
1177
  ignore_dt_cols = [
1167
1178
  col
@@ -1511,7 +1522,7 @@ def get_pipe_attributes(
1511
1522
  if isinstance(parameters, str) and parameters[0] == '{':
1512
1523
  parameters = json.loads(parameters)
1513
1524
  attributes['parameters'] = parameters
1514
- except Exception as e:
1525
+ except Exception:
1515
1526
  attributes['parameters'] = {}
1516
1527
 
1517
1528
  return attributes
@@ -17,7 +17,7 @@ from meerschaum.utils.debug import dprint
17
17
  from meerschaum.utils.warnings import warn
18
18
 
19
19
  ### database flavors that can use bulk insert
20
- _bulk_flavors = {'postgresql', 'timescaledb', 'citus', 'mssql'}
20
+ _bulk_flavors = {'postgresql', 'postgis', 'timescaledb', 'citus', 'mssql'}
21
21
  ### flavors that do not support chunks
22
22
  _disallow_chunks_flavors = ['duckdb']
23
23
  _max_chunks_flavors = {'sqlite': 1000}
@@ -798,6 +798,7 @@ def to_sql(
798
798
  get_numeric_cols,
799
799
  get_uuid_cols,
800
800
  get_bytes_cols,
801
+ get_geometry_cols,
801
802
  )
802
803
  from meerschaum.utils.dtypes import (
803
804
  are_dtypes_equal,
@@ -805,7 +806,9 @@ def to_sql(
805
806
  encode_bytes_for_bytea,
806
807
  serialize_bytes,
807
808
  serialize_decimal,
809
+ serialize_geometry,
808
810
  json_serialize_value,
811
+ get_geometry_type_srid,
809
812
  )
810
813
  from meerschaum.utils.dtypes.sql import (
811
814
  PD_TO_SQLALCHEMY_DTYPES_FLAVORS,
@@ -822,6 +825,7 @@ def to_sql(
822
825
 
823
826
  bytes_cols = get_bytes_cols(df)
824
827
  numeric_cols = get_numeric_cols(df)
828
+ geometry_cols = get_geometry_cols(df)
825
829
  ### NOTE: This excludes non-numeric serialized Decimals (e.g. SQLite).
826
830
  numeric_cols_dtypes = {
827
831
  col: typ
@@ -830,7 +834,6 @@ def to_sql(
830
834
  col in df.columns
831
835
  and 'numeric' in str(typ).lower()
832
836
  )
833
-
834
837
  }
835
838
  numeric_cols.extend([col for col in numeric_cols_dtypes if col not in numeric_cols])
836
839
  numeric_cols_precisions_scales = {
@@ -841,6 +844,22 @@ def to_sql(
841
844
  )
842
845
  for col, typ in numeric_cols_dtypes.items()
843
846
  }
847
+ geometry_cols_dtypes = {
848
+ col: typ
849
+ for col, typ in kw.get('dtype', {}).items()
850
+ if (
851
+ col in df.columns
852
+ and 'geometry' in str(typ).lower() or 'geography' in str(typ).lower()
853
+ )
854
+ }
855
+ geometry_cols.extend([col for col in geometry_cols_dtypes if col not in geometry_cols])
856
+ geometry_cols_types_srids = {
857
+ col: (typ.geometry_type, typ.srid)
858
+ if hasattr(typ, 'srid')
859
+ else get_geometry_type_srid()
860
+ for col, typ in geometry_cols_dtypes.items()
861
+ }
862
+
844
863
  cols_pd_types = {
845
864
  col: get_pd_type_from_db_type(str(typ))
846
865
  for col, typ in kw.get('dtype', {}).items()
@@ -856,8 +875,9 @@ def to_sql(
856
875
  }
857
876
 
858
877
  enable_bulk_insert = mrsm.get_config(
859
- 'system', 'connectors', 'sql', 'bulk_insert'
860
- ).get(self.flavor, False)
878
+ 'system', 'connectors', 'sql', 'bulk_insert', self.flavor,
879
+ warn=False,
880
+ ) or False
861
881
  stats = {'target': name}
862
882
  ### resort to defaults if None
863
883
  copied = False
@@ -901,6 +921,17 @@ def to_sql(
901
921
  )
902
922
  )
903
923
 
924
+ for col in geometry_cols:
925
+ geometry_type, srid = geometry_cols_types_srids.get(col, get_geometry_type_srid())
926
+ with warnings.catch_warnings():
927
+ warnings.simplefilter("ignore")
928
+ df[col] = df[col].apply(
929
+ functools.partial(
930
+ serialize_geometry,
931
+ as_wkt=(self.flavor == 'mssql')
932
+ )
933
+ )
934
+
904
935
  stats['method'] = method.__name__ if hasattr(method, '__name__') else str(method)
905
936
 
906
937
  default_chunksize = self._sys_config.get('chunksize', None)
@@ -153,6 +153,7 @@ def filter_unseen_df(
153
153
  attempt_cast_to_numeric,
154
154
  attempt_cast_to_uuid,
155
155
  attempt_cast_to_bytes,
156
+ attempt_cast_to_geometry,
156
157
  coerce_timezone,
157
158
  serialize_decimal,
158
159
  )
@@ -350,6 +351,10 @@ def filter_unseen_df(
350
351
  new_bytes_cols = get_bytes_cols(new_df)
351
352
  bytes_cols = set(new_bytes_cols + old_bytes_cols)
352
353
 
354
+ old_geometry_cols = get_geometry_cols(old_df)
355
+ new_geometry_cols = get_geometry_cols(new_df)
356
+ geometry_cols = set(new_geometry_cols + old_geometry_cols)
357
+
353
358
  joined_df = merge(
354
359
  new_df.infer_objects(copy=False).fillna(NA),
355
360
  old_df.infer_objects(copy=False).fillna(NA),
@@ -400,6 +405,14 @@ def filter_unseen_df(
400
405
  except Exception:
401
406
  warn(f"Unable to parse bytes column '{bytes_col}':\n{traceback.format_exc()}")
402
407
 
408
+ for geometry_col in geometry_cols:
409
+ if geometry_col not in delta_df.columns:
410
+ continue
411
+ try:
412
+ delta_df[geometry_col] = delta_df[geometry_col].apply(attempt_cast_to_geometry)
413
+ except Exception:
414
+ warn(f"Unable to parse bytes column '{bytes_col}':\n{traceback.format_exc()}")
415
+
403
416
  return delta_df
404
417
 
405
418
 
@@ -858,6 +871,44 @@ def get_bytes_cols(df: 'pd.DataFrame') -> List[str]:
858
871
  ]
859
872
 
860
873
 
874
+ def get_geometry_cols(df: 'pd.DataFrame') -> List[str]:
875
+ """
876
+ Get the columns which contain shapely objects from a Pandas DataFrame.
877
+
878
+ Parameters
879
+ ----------
880
+ df: pd.DataFrame
881
+ The DataFrame which may contain bytes strings.
882
+
883
+ Returns
884
+ -------
885
+ A list of columns to treat as `geometry`.
886
+ """
887
+ if df is None:
888
+ return []
889
+
890
+ is_dask = 'dask' in df.__module__
891
+ if is_dask:
892
+ df = get_first_valid_dask_partition(df)
893
+
894
+ if len(df) == 0:
895
+ return []
896
+
897
+ cols_indices = {
898
+ col: df[col].first_valid_index()
899
+ for col in df.columns
900
+ }
901
+ return [
902
+ col
903
+ for col, ix in cols_indices.items()
904
+ if (
905
+ ix is not None
906
+ and
907
+ 'shapely' in str(type(df.loc[ix][col]))
908
+ )
909
+ ]
910
+
911
+
861
912
  def enforce_dtypes(
862
913
  df: 'pd.DataFrame',
863
914
  dtypes: Dict[str, str],
@@ -911,6 +962,7 @@ def enforce_dtypes(
911
962
  attempt_cast_to_numeric,
912
963
  attempt_cast_to_uuid,
913
964
  attempt_cast_to_bytes,
965
+ attempt_cast_to_geometry,
914
966
  coerce_timezone as _coerce_timezone,
915
967
  )
916
968
  from meerschaum.utils.dtypes.sql import get_numeric_precision_scale
@@ -937,6 +989,11 @@ def enforce_dtypes(
937
989
  for col, typ in dtypes.items()
938
990
  if typ.startswith('numeric')
939
991
  ]
992
+ geometry_cols = [
993
+ col
994
+ for col, typ in dtypes.items()
995
+ if typ.startswith('geometry') or typ.startswith('geography')
996
+ ]
940
997
  uuid_cols = [
941
998
  col
942
999
  for col, typ in dtypes.items()
@@ -1026,6 +1083,28 @@ def enforce_dtypes(
1026
1083
  if col in df.columns:
1027
1084
  df[col] = _coerce_timezone(df[col], strip_utc=strip_timezone)
1028
1085
 
1086
+ if geometry_cols:
1087
+ geopandas = mrsm.attempt_import('geopandas')
1088
+ if debug:
1089
+ dprint(f"Checking for geometry: {geometry_cols}")
1090
+ parsed_geom_cols = []
1091
+ for col in geometry_cols:
1092
+ try:
1093
+ df[col] = df[col].apply(attempt_cast_to_geometry)
1094
+ parsed_geom_cols.append(col)
1095
+ except Exception as e:
1096
+ if debug:
1097
+ dprint(f"Unable to parse column '{col}' as geometry:\n{e}")
1098
+
1099
+ if parsed_geom_cols:
1100
+ if debug:
1101
+ dprint(f"Converting to GeoDataFrame (geometry column: '{parsed_geom_cols[0]}')...")
1102
+ df = geopandas.GeoDataFrame(df, geometry=parsed_geom_cols[0])
1103
+ try:
1104
+ df.rename_geometry(parsed_geom_cols[0], inplace=True)
1105
+ except ValueError:
1106
+ pass
1107
+
1029
1108
  df_dtypes = {c: str(t) for c, t in df.dtypes.items()}
1030
1109
  if are_dtypes_equal(df_dtypes, pipe_pandas_dtypes):
1031
1110
  if debug:
@@ -1602,13 +1681,19 @@ def to_json(
1602
1681
  -------
1603
1682
  A JSON string.
1604
1683
  """
1684
+ import warnings
1605
1685
  from meerschaum.utils.packages import import_pandas
1606
- from meerschaum.utils.dtypes import serialize_bytes, serialize_decimal
1686
+ from meerschaum.utils.dtypes import (
1687
+ serialize_bytes,
1688
+ serialize_decimal,
1689
+ serialize_geometry,
1690
+ )
1607
1691
  pd = import_pandas()
1608
1692
  uuid_cols = get_uuid_cols(df)
1609
1693
  bytes_cols = get_bytes_cols(df)
1610
1694
  numeric_cols = get_numeric_cols(df)
1611
- if safe_copy and bool(uuid_cols or bytes_cols):
1695
+ geometry_cols = get_geometry_cols(df)
1696
+ if safe_copy and bool(uuid_cols or bytes_cols or geometry_cols or numeric_cols):
1612
1697
  df = df.copy()
1613
1698
  for col in uuid_cols:
1614
1699
  df[col] = df[col].astype(str)
@@ -1616,6 +1701,10 @@ def to_json(
1616
1701
  df[col] = df[col].apply(serialize_bytes)
1617
1702
  for col in numeric_cols:
1618
1703
  df[col] = df[col].apply(serialize_decimal)
1704
+ with warnings.catch_warnings():
1705
+ warnings.simplefilter("ignore")
1706
+ for col in geometry_cols:
1707
+ df[col] = df[col].apply(serialize_geometry)
1619
1708
  return df.infer_objects(copy=False).fillna(pd.NA).to_json(
1620
1709
  date_format=date_format,
1621
1710
  date_unit=date_unit,
@@ -12,7 +12,7 @@ from datetime import timezone, datetime
12
12
  from decimal import Decimal, Context, InvalidOperation, ROUND_HALF_UP
13
13
 
14
14
  import meerschaum as mrsm
15
- from meerschaum.utils.typing import Dict, Union, Any, Optional
15
+ from meerschaum.utils.typing import Dict, Union, Any, Optional, Tuple
16
16
  from meerschaum.utils.warnings import warn
17
17
 
18
18
  MRSM_ALIAS_DTYPES: Dict[str, str] = {
@@ -27,10 +27,13 @@ MRSM_ALIAS_DTYPES: Dict[str, str] = {
27
27
  'bytea': 'bytes',
28
28
  'guid': 'uuid',
29
29
  'UUID': 'uuid',
30
+ 'geom': 'geometry',
30
31
  }
31
32
  MRSM_PD_DTYPES: Dict[Union[str, None], str] = {
32
33
  'json': 'object',
33
34
  'numeric': 'object',
35
+ 'geometry': 'object',
36
+ 'geography': 'object',
34
37
  'uuid': 'object',
35
38
  'datetime': 'datetime64[ns, UTC]',
36
39
  'bool': 'bool[pyarrow]',
@@ -60,6 +63,12 @@ def to_pandas_dtype(dtype: str) -> str:
60
63
  if dtype.startswith('numeric'):
61
64
  return MRSM_PD_DTYPES['numeric']
62
65
 
66
+ if dtype.startswith('geometry'):
67
+ return MRSM_PD_DTYPES['geometry']
68
+
69
+ if dtype.startswith('geography'):
70
+ return MRSM_PD_DTYPES['geography']
71
+
63
72
  ### NOTE: Kind of a hack, but if the first word of the given dtype is in all caps,
64
73
  ### treat it as a SQL db type.
65
74
  if dtype.split(' ')[0].isupper():
@@ -147,6 +156,10 @@ def are_dtypes_equal(
147
156
  if ldtype in bytes_dtypes and rdtype in bytes_dtypes:
148
157
  return True
149
158
 
159
+ geometry_dtypes = ('geometry', 'object', 'geography')
160
+ if ldtype in geometry_dtypes and rdtype in geometry_dtypes:
161
+ return True
162
+
150
163
  if ldtype.lower() == rdtype.lower():
151
164
  return True
152
165
 
@@ -277,6 +290,56 @@ def attempt_cast_to_bytes(value: Any) -> Any:
277
290
  return value
278
291
 
279
292
 
293
+ def attempt_cast_to_geometry(value: Any) -> Any:
294
+ """
295
+ Given a value, attempt to coerce it into a `shapely` (`geometry`) object.
296
+ """
297
+ shapely_wkt, shapely_wkb = mrsm.attempt_import('shapely.wkt', 'shapely.wkb', lazy=False)
298
+ if 'shapely' in str(type(value)):
299
+ return value
300
+
301
+ value_is_wkt = geometry_is_wkt(value)
302
+ if value_is_wkt is None:
303
+ return value
304
+
305
+ try:
306
+ return (
307
+ shapely_wkt.loads(value)
308
+ if value_is_wkt
309
+ else shapely_wkb.loads(value)
310
+ )
311
+ except Exception:
312
+ return value
313
+
314
+
315
+ def geometry_is_wkt(value: Union[str, bytes]) -> Union[bool, None]:
316
+ """
317
+ Determine whether an input value should be treated as WKT or WKB geometry data.
318
+
319
+ Parameters
320
+ ----------
321
+ value: Union[str, bytes]
322
+ The input data to be parsed into geometry data.
323
+
324
+ Returns
325
+ -------
326
+ A `bool` (`True` if `value` is WKT and `False` if it should be treated as WKB).
327
+ Return `None` if `value` should be parsed as neither.
328
+ """
329
+ import re
330
+ if isinstance(value, bytes):
331
+ return False
332
+
333
+ wkt_pattern = r'^\s*(POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)\s*\(.*\)\s*$'
334
+ if re.match(wkt_pattern, value, re.IGNORECASE):
335
+ return True
336
+
337
+ if all(c in '0123456789ABCDEFabcdef' for c in value) and len(value) % 2 == 0:
338
+ return False
339
+
340
+ return None
341
+
342
+
280
343
  def value_is_null(value: Any) -> bool:
281
344
  """
282
345
  Determine if a value is a null-like string.
@@ -458,6 +521,37 @@ def serialize_bytes(data: bytes) -> str:
458
521
  return base64.b64encode(data).decode('utf-8')
459
522
 
460
523
 
524
+ def serialize_geometry(geom: Any, as_wkt: bool = False) -> str:
525
+ """
526
+ Serialize geometry data as a hex-encoded well-known-binary string.
527
+
528
+ Parameters
529
+ ----------
530
+ geom: Any
531
+ The potential geometry data to be serialized.
532
+
533
+ as_wkt, bool, default False
534
+ If `True`, serialize geometry data as well-known text (WKT)
535
+ instead of well-known binary (WKB).
536
+
537
+ Returns
538
+ -------
539
+ A string containing the geometry data.
540
+ """
541
+ if hasattr(geom, 'wkb_hex'):
542
+ return geom.wkb_hex if not as_wkt else geom.wkt
543
+
544
+ return str(geom)
545
+
546
+
547
+ def deserialize_geometry(geom_wkb: Union[str, bytes]):
548
+ """
549
+ Deserialize a WKB string into a shapely geometry object.
550
+ """
551
+ shapely = mrsm.attempt_import(lazy=False)
552
+ return shapely.wkb.loads(geom_wkb)
553
+
554
+
461
555
  def deserialize_bytes_string(data: str | None, force_hex: bool = False) -> bytes | None:
462
556
  """
463
557
  Given a serialized ASCII string of bytes data, return the original bytes.
@@ -559,7 +653,94 @@ def json_serialize_value(x: Any, default_to_str: bool = True) -> str:
559
653
  if isinstance(x, Decimal):
560
654
  return serialize_decimal(x)
561
655
 
656
+ if 'shapely' in str(type(x)):
657
+ return serialize_geometry(x)
658
+
562
659
  if value_is_null(x):
563
660
  return None
564
661
 
565
662
  return str(x) if default_to_str else x
663
+
664
+
665
+ def get_geometry_type_srid(
666
+ dtype: str = 'geometry',
667
+ default_type: str = 'geometry',
668
+ default_srid: int = 4326,
669
+ ) -> Union[Tuple[str, int], Tuple[str, None]]:
670
+ """
671
+ Given the specified geometry `dtype`, return a tuple in the form (type, SRID).
672
+
673
+ Parameters
674
+ ----------
675
+ dtype: Optional[str], default None
676
+ Optionally provide a specific `geometry` syntax (e.g. `geometry[MultiLineString, 4326]`).
677
+ You may specify a supported `shapely` geometry type and an SRID in the dtype modifier:
678
+
679
+ - `Point`
680
+ - `LineString`
681
+ - `LinearRing`
682
+ - `Polygon`
683
+ - `MultiPoint`
684
+ - `MultiLineString`
685
+ - `MultiPolygon`
686
+ - `GeometryCollection`
687
+
688
+ Returns
689
+ -------
690
+ A tuple in the form (type, SRID).
691
+ Defaults to `(default_type, default_srid)`.
692
+
693
+ Examples
694
+ --------
695
+ >>> from meerschaum.utils.dtypes import get_geometry_type_srid
696
+ >>> get_geometry_type_srid()
697
+ ('geometry', 4326)
698
+ >>> get_geometry_type_srid('geometry[]')
699
+ ('geometry', 4326)
700
+ >>> get_geometry_type_srid('geometry[Point, 0]')
701
+ ('Point', 0)
702
+ >>> get_geometry_type_srid('geometry[0, Point]')
703
+ ('Point', 0)
704
+ >>> get_geometry_type_srid('geometry[0]')
705
+ ('geometry', 0)
706
+ >>> get_geometry_type_srid('geometry[MULTILINESTRING, 4326]')
707
+ ('MultiLineString', 4326)
708
+ >>> get_geometry_type_srid('geography')
709
+ ('geometry', 4326)
710
+ >>> get_geometry_type_srid('geography[POINT]')
711
+ ('Point', 4376)
712
+ """
713
+ from meerschaum.utils.misc import is_int
714
+ bare_dtype = dtype.split('[', maxsplit=1)[0]
715
+ modifier = dtype.split(bare_dtype, maxsplit=1)[-1].lstrip('[').rstrip(']')
716
+ if not modifier:
717
+ return default_type, default_srid
718
+
719
+ shapely_geometry_base = mrsm.attempt_import('shapely.geometry.base')
720
+ geometry_types = {
721
+ typ.lower(): typ
722
+ for typ in shapely_geometry_base.GEOMETRY_TYPES
723
+ }
724
+
725
+ parts = [part.lower().replace('srid=', '').replace('type=', '').strip() for part in modifier.split(',')]
726
+ parts_casted = [
727
+ (
728
+ int(part)
729
+ if is_int(part)
730
+ else part
731
+ ) for part in parts]
732
+
733
+ srid = default_srid
734
+ geometry_type = default_type
735
+
736
+ for part in parts_casted:
737
+ if isinstance(part, int):
738
+ srid = part
739
+ break
740
+
741
+ for part in parts:
742
+ if part.lower() in geometry_types:
743
+ geometry_type = geometry_types.get(part)
744
+ break
745
+
746
+ return geometry_type, srid
@@ -128,6 +128,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
128
128
  'int': {
129
129
  'timescaledb': 'BIGINT',
130
130
  'postgresql': 'BIGINT',
131
+ 'postgis': 'BIGINT',
131
132
  'mariadb': 'BIGINT',
132
133
  'mysql': 'BIGINT',
133
134
  'mssql': 'BIGINT',
@@ -141,6 +142,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
141
142
  'float': {
142
143
  'timescaledb': 'DOUBLE PRECISION',
143
144
  'postgresql': 'DOUBLE PRECISION',
145
+ 'postgis': 'DOUBLE PRECISION',
144
146
  'mariadb': 'DOUBLE PRECISION',
145
147
  'mysql': 'DOUBLE PRECISION',
146
148
  'mssql': 'FLOAT',
@@ -154,6 +156,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
154
156
  'double': {
155
157
  'timescaledb': 'DOUBLE PRECISION',
156
158
  'postgresql': 'DOUBLE PRECISION',
159
+ 'postgis': 'DOUBLE PRECISION',
157
160
  'mariadb': 'DOUBLE PRECISION',
158
161
  'mysql': 'DOUBLE PRECISION',
159
162
  'mssql': 'FLOAT',
@@ -167,6 +170,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
167
170
  'datetime64[ns]': {
168
171
  'timescaledb': 'TIMESTAMP',
169
172
  'postgresql': 'TIMESTAMP',
173
+ 'postgis': 'TIMESTAMP',
170
174
  'mariadb': 'DATETIME',
171
175
  'mysql': 'DATETIME',
172
176
  'mssql': 'DATETIME2',
@@ -180,6 +184,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
180
184
  'datetime64[ns, UTC]': {
181
185
  'timescaledb': 'TIMESTAMPTZ',
182
186
  'postgresql': 'TIMESTAMPTZ',
187
+ 'postgis': 'TIMESTAMPTZ',
183
188
  'mariadb': 'DATETIME',
184
189
  'mysql': 'DATETIME',
185
190
  'mssql': 'DATETIMEOFFSET',
@@ -193,6 +198,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
193
198
  'datetime': {
194
199
  'timescaledb': 'TIMESTAMPTZ',
195
200
  'postgresql': 'TIMESTAMPTZ',
201
+ 'postgis': 'TIMESTAMPTZ',
196
202
  'mariadb': 'DATETIME',
197
203
  'mysql': 'DATETIME',
198
204
  'mssql': 'DATETIMEOFFSET',
@@ -206,6 +212,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
206
212
  'datetimetz': {
207
213
  'timescaledb': 'TIMESTAMPTZ',
208
214
  'postgresql': 'TIMESTAMPTZ',
215
+ 'postgis': 'TIMESTAMPTZ',
209
216
  'mariadb': 'DATETIME',
210
217
  'mysql': 'DATETIME',
211
218
  'mssql': 'DATETIMEOFFSET',
@@ -219,6 +226,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
219
226
  'bool': {
220
227
  'timescaledb': 'BOOLEAN',
221
228
  'postgresql': 'BOOLEAN',
229
+ 'postgis': 'BOOLEAN',
222
230
  'mariadb': 'BOOLEAN',
223
231
  'mysql': 'BOOLEAN',
224
232
  'mssql': 'BIT',
@@ -232,6 +240,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
232
240
  'object': {
233
241
  'timescaledb': 'TEXT',
234
242
  'postgresql': 'TEXT',
243
+ 'postgis': 'TEXT',
235
244
  'mariadb': 'TEXT',
236
245
  'mysql': 'TEXT',
237
246
  'mssql': 'NVARCHAR(MAX)',
@@ -245,6 +254,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
245
254
  'string': {
246
255
  'timescaledb': 'TEXT',
247
256
  'postgresql': 'TEXT',
257
+ 'postgis': 'TEXT',
248
258
  'mariadb': 'TEXT',
249
259
  'mysql': 'TEXT',
250
260
  'mssql': 'NVARCHAR(MAX)',
@@ -258,6 +268,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
258
268
  'unicode': {
259
269
  'timescaledb': 'TEXT',
260
270
  'postgresql': 'TEXT',
271
+ 'postgis': 'TEXT',
261
272
  'mariadb': 'TEXT',
262
273
  'mysql': 'TEXT',
263
274
  'mssql': 'NVARCHAR(MAX)',
@@ -271,6 +282,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
271
282
  'json': {
272
283
  'timescaledb': 'JSONB',
273
284
  'postgresql': 'JSONB',
285
+ 'postgis': 'JSONB',
274
286
  'mariadb': 'TEXT',
275
287
  'mysql': 'TEXT',
276
288
  'mssql': 'NVARCHAR(MAX)',
@@ -284,6 +296,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
284
296
  'numeric': {
285
297
  'timescaledb': 'NUMERIC',
286
298
  'postgresql': 'NUMERIC',
299
+ 'postgis': 'NUMERIC',
287
300
  'mariadb': f'DECIMAL{NUMERIC_PRECISION_FLAVORS["mariadb"]}',
288
301
  'mysql': f'DECIMAL{NUMERIC_PRECISION_FLAVORS["mysql"]}',
289
302
  'mssql': f'NUMERIC{NUMERIC_PRECISION_FLAVORS["mssql"]}',
@@ -297,6 +310,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
297
310
  'uuid': {
298
311
  'timescaledb': 'UUID',
299
312
  'postgresql': 'UUID',
313
+ 'postgis': 'UUID',
300
314
  'mariadb': 'CHAR(36)',
301
315
  'mysql': 'CHAR(36)',
302
316
  'mssql': 'UNIQUEIDENTIFIER',
@@ -311,6 +325,7 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
311
325
  'bytes': {
312
326
  'timescaledb': 'BYTEA',
313
327
  'postgresql': 'BYTEA',
328
+ 'postgis': 'BYTEA',
314
329
  'mariadb': 'BLOB',
315
330
  'mysql': 'BLOB',
316
331
  'mssql': 'VARBINARY(MAX)',
@@ -321,11 +336,40 @@ PD_TO_DB_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
321
336
  'cockroachdb': 'BYTEA',
322
337
  'default': 'BLOB',
323
338
  },
339
+ 'geometry': {
340
+ 'timescaledb': 'TEXT',
341
+ 'postgresql': 'TEXT',
342
+ 'postgis': 'GEOMETRY',
343
+ 'mariadb': 'TEXT',
344
+ 'mysql': 'TEXT',
345
+ 'mssql': 'NVARCHAR(MAX)',
346
+ 'oracle': 'NVARCHAR2(2000)',
347
+ 'sqlite': 'TEXT',
348
+ 'duckdb': 'TEXT',
349
+ 'citus': 'TEXT',
350
+ 'cockroachdb': 'TEXT',
351
+ 'default': 'TEXT',
352
+ },
353
+ 'geography': {
354
+ 'timescaledb': 'TEXT',
355
+ 'postgresql': 'TEXT',
356
+ 'postgis': 'GEOGRAPHY',
357
+ 'mariadb': 'TEXT',
358
+ 'mysql': 'TEXT',
359
+ 'mssql': 'NVARCHAR(MAX)',
360
+ 'oracle': 'NVARCHAR2(2000)',
361
+ 'sqlite': 'TEXT',
362
+ 'duckdb': 'TEXT',
363
+ 'citus': 'TEXT',
364
+ 'cockroachdb': 'TEXT',
365
+ 'default': 'TEXT',
366
+ },
324
367
  }
325
368
  PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
326
369
  'int': {
327
370
  'timescaledb': 'BigInteger',
328
371
  'postgresql': 'BigInteger',
372
+ 'postgis': 'BigInteger',
329
373
  'mariadb': 'BigInteger',
330
374
  'mysql': 'BigInteger',
331
375
  'mssql': 'BigInteger',
@@ -339,6 +383,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
339
383
  'float': {
340
384
  'timescaledb': 'Float',
341
385
  'postgresql': 'Float',
386
+ 'postgis': 'Float',
342
387
  'mariadb': 'Float',
343
388
  'mysql': 'Float',
344
389
  'mssql': 'Float',
@@ -352,6 +397,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
352
397
  'datetime': {
353
398
  'timescaledb': 'DateTime(timezone=True)',
354
399
  'postgresql': 'DateTime(timezone=True)',
400
+ 'postgis': 'DateTime(timezone=True)',
355
401
  'mariadb': 'DateTime(timezone=True)',
356
402
  'mysql': 'DateTime(timezone=True)',
357
403
  'mssql': 'sqlalchemy.dialects.mssql.DATETIMEOFFSET',
@@ -365,6 +411,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
365
411
  'datetime64[ns]': {
366
412
  'timescaledb': 'DateTime',
367
413
  'postgresql': 'DateTime',
414
+ 'postgis': 'DateTime',
368
415
  'mariadb': 'DateTime',
369
416
  'mysql': 'DateTime',
370
417
  'mssql': 'sqlalchemy.dialects.mssql.DATETIME2',
@@ -378,6 +425,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
378
425
  'datetime64[ns, UTC]': {
379
426
  'timescaledb': 'DateTime(timezone=True)',
380
427
  'postgresql': 'DateTime(timezone=True)',
428
+ 'postgis': 'DateTime(timezone=True)',
381
429
  'mariadb': 'DateTime(timezone=True)',
382
430
  'mysql': 'DateTime(timezone=True)',
383
431
  'mssql': 'sqlalchemy.dialects.mssql.DATETIMEOFFSET',
@@ -391,6 +439,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
391
439
  'bool': {
392
440
  'timescaledb': 'Boolean',
393
441
  'postgresql': 'Boolean',
442
+ 'postgis': 'Boolean',
394
443
  'mariadb': 'Integer',
395
444
  'mysql': 'Integer',
396
445
  'mssql': 'sqlalchemy.dialects.mssql.BIT',
@@ -404,6 +453,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
404
453
  'object': {
405
454
  'timescaledb': 'UnicodeText',
406
455
  'postgresql': 'UnicodeText',
456
+ 'postgis': 'UnicodeText',
407
457
  'mariadb': 'UnicodeText',
408
458
  'mysql': 'UnicodeText',
409
459
  'mssql': 'UnicodeText',
@@ -417,6 +467,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
417
467
  'string': {
418
468
  'timescaledb': 'UnicodeText',
419
469
  'postgresql': 'UnicodeText',
470
+ 'postgis': 'UnicodeText',
420
471
  'mariadb': 'UnicodeText',
421
472
  'mysql': 'UnicodeText',
422
473
  'mssql': 'UnicodeText',
@@ -430,6 +481,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
430
481
  'json': {
431
482
  'timescaledb': 'sqlalchemy.dialects.postgresql.JSONB',
432
483
  'postgresql': 'sqlalchemy.dialects.postgresql.JSONB',
484
+ 'postgis': 'sqlalchemy.dialects.postgresql.JSONB',
433
485
  'mariadb': 'UnicodeText',
434
486
  'mysql': 'UnicodeText',
435
487
  'mssql': 'UnicodeText',
@@ -443,6 +495,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
443
495
  'numeric': {
444
496
  'timescaledb': 'Numeric',
445
497
  'postgresql': 'Numeric',
498
+ 'postgis': 'Numeric',
446
499
  'mariadb': 'Numeric',
447
500
  'mysql': 'Numeric',
448
501
  'mssql': 'Numeric',
@@ -456,6 +509,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
456
509
  'uuid': {
457
510
  'timescaledb': 'Uuid',
458
511
  'postgresql': 'Uuid',
512
+ 'postgis': 'Uuid',
459
513
  'mariadb': 'sqlalchemy.dialects.mysql.CHAR(36)',
460
514
  'mysql': 'sqlalchemy.dialects.mysql.CHAR(36)',
461
515
  'mssql': 'Uuid',
@@ -469,6 +523,7 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
469
523
  'bytes': {
470
524
  'timescaledb': 'LargeBinary',
471
525
  'postgresql': 'LargeBinary',
526
+ 'postgis': 'LargeBinary',
472
527
  'mariadb': 'LargeBinary',
473
528
  'mysql': 'LargeBinary',
474
529
  'mssql': 'LargeBinary',
@@ -479,11 +534,40 @@ PD_TO_SQLALCHEMY_DTYPES_FLAVORS: Dict[str, Dict[str, str]] = {
479
534
  'cockroachdb': 'LargeBinary',
480
535
  'default': 'LargeBinary',
481
536
  },
537
+ 'geometry': {
538
+ 'timescaledb': 'UnicodeText',
539
+ 'postgresql': 'UnicodeText',
540
+ 'postgis': 'geoalchemy2.Geometry',
541
+ 'mariadb': 'UnicodeText',
542
+ 'mysql': 'UnicodeText',
543
+ 'mssql': 'UnicodeText',
544
+ 'oracle': 'UnicodeText',
545
+ 'sqlite': 'UnicodeText',
546
+ 'duckdb': 'UnicodeText',
547
+ 'citus': 'UnicodeText',
548
+ 'cockroachdb': 'UnicodeText',
549
+ 'default': 'UnicodeText',
550
+ },
551
+ 'geography': {
552
+ 'timescaledb': 'UnicodeText',
553
+ 'postgresql': 'UnicodeText',
554
+ 'postgis': 'geoalchemy2.Geography',
555
+ 'mariadb': 'UnicodeText',
556
+ 'mysql': 'UnicodeText',
557
+ 'mssql': 'UnicodeText',
558
+ 'oracle': 'UnicodeText',
559
+ 'sqlite': 'UnicodeText',
560
+ 'duckdb': 'UnicodeText',
561
+ 'citus': 'UnicodeText',
562
+ 'cockroachdb': 'UnicodeText',
563
+ 'default': 'UnicodeText',
564
+ },
482
565
  }
483
566
 
484
567
  AUTO_INCREMENT_COLUMN_FLAVORS: Dict[str, str] = {
485
568
  'timescaledb': 'GENERATED BY DEFAULT AS IDENTITY',
486
569
  'postgresql': 'GENERATED BY DEFAULT AS IDENTITY',
570
+ 'postgis': 'GENERATED BY DEFAULT AS IDENTITY',
487
571
  'mariadb': 'AUTO_INCREMENT',
488
572
  'mysql': 'AUTO_INCREMENT',
489
573
  'mssql': 'IDENTITY(1,1)',
@@ -565,7 +649,7 @@ def get_db_type_from_pd_type(
565
649
  """
566
650
  from meerschaum.utils.warnings import warn
567
651
  from meerschaum.utils.packages import attempt_import
568
- from meerschaum.utils.dtypes import are_dtypes_equal, MRSM_ALIAS_DTYPES
652
+ from meerschaum.utils.dtypes import are_dtypes_equal, MRSM_ALIAS_DTYPES, get_geometry_type_srid
569
653
  from meerschaum.utils.misc import parse_arguments_str
570
654
  sqlalchemy_types = attempt_import('sqlalchemy.types', lazy=False)
571
655
 
@@ -576,18 +660,30 @@ def get_db_type_from_pd_type(
576
660
  )
577
661
 
578
662
  precision, scale = None, None
663
+ geometry_type, geometry_srid = None, None
579
664
  og_pd_type = pd_type
580
665
  if pd_type in MRSM_ALIAS_DTYPES:
581
666
  pd_type = MRSM_ALIAS_DTYPES[pd_type]
582
667
 
583
668
  ### Check whether we are able to match this type (e.g. pyarrow support).
584
669
  found_db_type = False
585
- if pd_type not in types_registry and not pd_type.startswith('numeric['):
670
+ if (
671
+ pd_type not in types_registry
672
+ and not any(
673
+ pd_type.startswith(f'{typ}[')
674
+ for typ in ('numeric', 'geometry', 'geography')
675
+ )
676
+ ):
586
677
  for mapped_pd_type in types_registry:
587
678
  if are_dtypes_equal(mapped_pd_type, pd_type):
588
679
  pd_type = mapped_pd_type
589
680
  found_db_type = True
590
681
  break
682
+ elif (pd_type.startswith('geometry[') or pd_type.startswith('geography[')):
683
+ og_pd_type = pd_type
684
+ pd_type = 'geometry' if 'geometry' in pd_type else 'geography'
685
+ geometry_type, geometry_srid = get_geometry_type_srid(og_pd_type)
686
+ found_db_type = True
591
687
  elif pd_type.startswith('numeric['):
592
688
  og_pd_type = pd_type
593
689
  pd_type = 'numeric'
@@ -628,6 +724,11 @@ def get_db_type_from_pd_type(
628
724
  if precision is not None and scale is not None:
629
725
  db_type_bare = db_type.split('(', maxsplit=1)[0]
630
726
  return f"{db_type_bare}({precision},{scale})"
727
+ if geometry_type is not None and geometry_srid is not None:
728
+ if 'geometry' not in db_type.lower() and 'geography' not in db_type.lower():
729
+ return db_type
730
+ db_type_bare = db_type.split('(', maxsplit=1)[0]
731
+ return f"{db_type_bare}({geometry_type.upper()}, {geometry_srid})"
631
732
  return db_type
632
733
 
633
734
  if db_type.startswith('sqlalchemy.dialects'):
@@ -643,6 +744,17 @@ def get_db_type_from_pd_type(
643
744
  return cls
644
745
  return cls(*cls_args, **cls_kwargs)
645
746
 
747
+ if 'geometry' in db_type.lower() or 'geography' in db_type.lower():
748
+ geoalchemy2 = attempt_import('geoalchemy2', lazy=False)
749
+ geometry_class = (
750
+ geoalchemy2.Geometry
751
+ if 'geometry' in db_type.lower()
752
+ else geoalchemy2.Geography
753
+ )
754
+ if geometry_type is None or geometry_srid is None:
755
+ return geometry_class
756
+ return geometry_class(geometry_type=geometry_type, srid=geometry_srid)
757
+
646
758
  if 'numeric' in db_type.lower():
647
759
  if precision is None or scale is None:
648
760
  return sqlalchemy_types.Numeric
@@ -136,12 +136,14 @@ packages['sql'] = {
136
136
  'numpy' : 'numpy>=1.18.5',
137
137
  'pandas' : 'pandas[parquet]>=2.0.1',
138
138
  'geopandas' : 'geopandas>=1.0.1',
139
+ 'shapely' : 'shapely>=2.0.7',
139
140
  'pyarrow' : 'pyarrow>=16.1.0',
140
141
  'dask' : 'dask[complete]>=2024.12.1',
141
142
  'partd' : 'partd>=1.4.2',
142
143
  'pytz' : 'pytz',
143
144
  'joblib' : 'joblib>=0.17.0',
144
145
  'sqlalchemy' : 'SQLAlchemy>=2.0.5',
146
+ 'geoalchemy' : 'GeoAlchemy2>=0.17.1',
145
147
  'databases' : 'databases>=0.4.0',
146
148
  'aiosqlite' : 'aiosqlite>=0.16.0',
147
149
  'asyncpg' : 'asyncpg>=0.21.0',
meerschaum/utils/sql.py CHANGED
@@ -41,13 +41,13 @@ version_queries = {
41
41
  }
42
42
  SKIP_IF_EXISTS_FLAVORS = {'mssql', 'oracle'}
43
43
  DROP_IF_EXISTS_FLAVORS = {
44
- 'timescaledb', 'postgresql', 'citus', 'mssql', 'mysql', 'mariadb', 'sqlite',
44
+ 'timescaledb', 'postgresql', 'postgis', 'citus', 'mssql', 'mysql', 'mariadb', 'sqlite',
45
45
  }
46
46
  DROP_INDEX_IF_EXISTS_FLAVORS = {
47
- 'mssql', 'timescaledb', 'postgresql', 'sqlite', 'citus',
47
+ 'mssql', 'timescaledb', 'postgresql', 'postgis', 'sqlite', 'citus',
48
48
  }
49
49
  SKIP_AUTO_INCREMENT_FLAVORS = {'citus', 'duckdb'}
50
- COALESCE_UNIQUE_INDEX_FLAVORS = {'timescaledb', 'postgresql', 'citus'}
50
+ COALESCE_UNIQUE_INDEX_FLAVORS = {'timescaledb', 'postgresql', 'postgis', 'citus'}
51
51
  UPDATE_QUERIES = {
52
52
  'default': """
53
53
  UPDATE {target_table_name} AS f
@@ -73,6 +73,12 @@ UPDATE_QUERIES = {
73
73
  FROM {patch_table_name}
74
74
  ON CONFLICT ({join_cols_str}) DO {update_or_nothing} {sets_subquery_none_excluded}
75
75
  """,
76
+ 'postgis-upsert': """
77
+ INSERT INTO {target_table_name} ({patch_cols_str})
78
+ SELECT {patch_cols_str}
79
+ FROM {patch_table_name}
80
+ ON CONFLICT ({join_cols_str}) DO {update_or_nothing} {sets_subquery_none_excluded}
81
+ """,
76
82
  'citus-upsert': """
77
83
  INSERT INTO {target_table_name} ({patch_cols_str})
78
84
  SELECT {patch_cols_str}
@@ -482,6 +488,7 @@ table_wrappers = {
482
488
  'citus' : ('"', '"'),
483
489
  'duckdb' : ('"', '"'),
484
490
  'postgresql' : ('"', '"'),
491
+ 'postgis' : ('"', '"'),
485
492
  'sqlite' : ('"', '"'),
486
493
  'mysql' : ('`', '`'),
487
494
  'mariadb' : ('`', '`'),
@@ -494,6 +501,7 @@ max_name_lens = {
494
501
  'mssql' : 128,
495
502
  'oracle' : 30,
496
503
  'postgresql' : 64,
504
+ 'postgis' : 64,
497
505
  'timescaledb': 64,
498
506
  'citus' : 64,
499
507
  'cockroachdb': 64,
@@ -501,10 +509,11 @@ max_name_lens = {
501
509
  'mysql' : 64,
502
510
  'mariadb' : 64,
503
511
  }
504
- json_flavors = {'postgresql', 'timescaledb', 'citus', 'cockroachdb'}
512
+ json_flavors = {'postgresql', 'postgis', 'timescaledb', 'citus', 'cockroachdb'}
505
513
  NO_SCHEMA_FLAVORS = {'oracle', 'sqlite', 'mysql', 'mariadb', 'duckdb'}
506
514
  DEFAULT_SCHEMA_FLAVORS = {
507
515
  'postgresql': 'public',
516
+ 'postgis': 'public',
508
517
  'timescaledb': 'public',
509
518
  'citus': 'public',
510
519
  'cockroachdb': 'public',
@@ -549,6 +558,7 @@ def dateadd_str(
549
558
  Currently supported flavors:
550
559
 
551
560
  - `'postgresql'`
561
+ - `'postgis'`
552
562
  - `'timescaledb'`
553
563
  - `'citus'`
554
564
  - `'cockroachdb'`
@@ -653,7 +663,7 @@ def dateadd_str(
653
663
  )
654
664
 
655
665
  da = ""
656
- if flavor in ('postgresql', 'timescaledb', 'cockroachdb', 'citus'):
666
+ if flavor in ('postgresql', 'postgis', 'timescaledb', 'cockroachdb', 'citus'):
657
667
  begin = (
658
668
  f"CAST({begin} AS {db_type})" if begin != 'now'
659
669
  else f"CAST(NOW() AT TIME ZONE 'utc' AS {db_type})"
@@ -1809,6 +1819,8 @@ def get_null_replacement(typ: str, flavor: str) -> str:
1809
1819
  """
1810
1820
  from meerschaum.utils.dtypes import are_dtypes_equal
1811
1821
  from meerschaum.utils.dtypes.sql import DB_FLAVORS_CAST_DTYPES
1822
+ if 'geometry' in typ.lower():
1823
+ return '010100000000008058346FCDC100008058346FCDC1'
1812
1824
  if 'int' in typ.lower() or typ.lower() in ('numeric', 'number'):
1813
1825
  return '-987654321'
1814
1826
  if 'bool' in typ.lower() or typ.lower() == 'bit':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: meerschaum
3
- Version: 2.9.0.dev1
3
+ Version: 2.9.0rc2
4
4
  Summary: Sync Time-Series Pipes with Meerschaum
5
5
  Home-page: https://meerschaum.io
6
6
  Author: Bennett Meares
@@ -127,12 +127,14 @@ Provides-Extra: sql
127
127
  Requires-Dist: numpy>=1.18.5; extra == "sql"
128
128
  Requires-Dist: pandas[parquet]>=2.0.1; extra == "sql"
129
129
  Requires-Dist: geopandas>=1.0.1; extra == "sql"
130
+ Requires-Dist: shapely>=2.0.7; extra == "sql"
130
131
  Requires-Dist: pyarrow>=16.1.0; extra == "sql"
131
132
  Requires-Dist: dask[complete]>=2024.12.1; extra == "sql"
132
133
  Requires-Dist: partd>=1.4.2; extra == "sql"
133
134
  Requires-Dist: pytz; extra == "sql"
134
135
  Requires-Dist: joblib>=0.17.0; extra == "sql"
135
136
  Requires-Dist: SQLAlchemy>=2.0.5; extra == "sql"
137
+ Requires-Dist: GeoAlchemy2>=0.17.1; extra == "sql"
136
138
  Requires-Dist: databases>=0.4.0; extra == "sql"
137
139
  Requires-Dist: aiosqlite>=0.16.0; extra == "sql"
138
140
  Requires-Dist: asyncpg>=0.21.0; extra == "sql"
@@ -188,12 +190,14 @@ Requires-Dist: valkey>=6.0.0; extra == "api"
188
190
  Requires-Dist: numpy>=1.18.5; extra == "api"
189
191
  Requires-Dist: pandas[parquet]>=2.0.1; extra == "api"
190
192
  Requires-Dist: geopandas>=1.0.1; extra == "api"
193
+ Requires-Dist: shapely>=2.0.7; extra == "api"
191
194
  Requires-Dist: pyarrow>=16.1.0; extra == "api"
192
195
  Requires-Dist: dask[complete]>=2024.12.1; extra == "api"
193
196
  Requires-Dist: partd>=1.4.2; extra == "api"
194
197
  Requires-Dist: pytz; extra == "api"
195
198
  Requires-Dist: joblib>=0.17.0; extra == "api"
196
199
  Requires-Dist: SQLAlchemy>=2.0.5; extra == "api"
200
+ Requires-Dist: GeoAlchemy2>=0.17.1; extra == "api"
197
201
  Requires-Dist: databases>=0.4.0; extra == "api"
198
202
  Requires-Dist: aiosqlite>=0.16.0; extra == "api"
199
203
  Requires-Dist: asyncpg>=0.21.0; extra == "api"
@@ -294,12 +298,14 @@ Requires-Dist: pycparser>=2.21.0; extra == "full"
294
298
  Requires-Dist: numpy>=1.18.5; extra == "full"
295
299
  Requires-Dist: pandas[parquet]>=2.0.1; extra == "full"
296
300
  Requires-Dist: geopandas>=1.0.1; extra == "full"
301
+ Requires-Dist: shapely>=2.0.7; extra == "full"
297
302
  Requires-Dist: pyarrow>=16.1.0; extra == "full"
298
303
  Requires-Dist: dask[complete]>=2024.12.1; extra == "full"
299
304
  Requires-Dist: partd>=1.4.2; extra == "full"
300
305
  Requires-Dist: pytz; extra == "full"
301
306
  Requires-Dist: joblib>=0.17.0; extra == "full"
302
307
  Requires-Dist: SQLAlchemy>=2.0.5; extra == "full"
308
+ Requires-Dist: GeoAlchemy2>=0.17.1; extra == "full"
303
309
  Requires-Dist: databases>=0.4.0; extra == "full"
304
310
  Requires-Dist: aiosqlite>=0.16.0; extra == "full"
305
311
  Requires-Dist: asyncpg>=0.21.0; extra == "full"
@@ -135,7 +135,7 @@ meerschaum/api/routes/_webterm.py,sha256=S7RXV8vvaTFbmVeehh4UhyXb4NCgcsyOQzoAG7j
135
135
  meerschaum/api/tables/__init__.py,sha256=e2aNC0CdlWICTUMx1i9RauF8Pm426J0RZJbsJWv4SWo,482
136
136
  meerschaum/config/__init__.py,sha256=5ZBq71P9t3nb74r5CGvMfNuauPscfegBX-nkaAUi5C4,11541
137
137
  meerschaum/config/_dash.py,sha256=BJHl4xMrQB-YHUEU7ldEW8q_nOPoIRSOqLrfGElc6Dw,187
138
- meerschaum/config/_default.py,sha256=8Jd-zvL159MtyOVvzMr7v2najvC-IqiCqBB4iOTs9v4,6549
138
+ meerschaum/config/_default.py,sha256=41onvi-NJK_agSkBnsZJwDz09accGLkH8AhkeCuIYZk,6582
139
139
  meerschaum/config/_edit.py,sha256=M9yX_SDD24gV5kWITZpy7p9AWTizJsIAGWAs3WZx-Ws,9087
140
140
  meerschaum/config/_environment.py,sha256=Vv4DLDfc2vKLbCLsMvkQDj77K4kEvHKEBmUBo-wCrgo,4419
141
141
  meerschaum/config/_formatting.py,sha256=OMuqS1EWOsj_34wSs2tOqGIWci3bTMIZ5l-uelZgsIM,6672
@@ -146,7 +146,7 @@ meerschaum/config/_preprocess.py,sha256=-AEA8m_--KivZwTQ1sWN6LTn5sio_fUr2XZ51BO6
146
146
  meerschaum/config/_read_config.py,sha256=RLC3HHi_1ndj7ITVDKLD9_uULY3caGRwSz3ATYE-ixA,15014
147
147
  meerschaum/config/_shell.py,sha256=46_m49Txc5q1rGfCgO49ca48BODx45DQJi8D0zz1R18,4245
148
148
  meerschaum/config/_sync.py,sha256=jHcWRkxd82_BgX8Xo8agsWvf7BSbv3qHLWmYl6ehp_0,4242
149
- meerschaum/config/_version.py,sha256=sy5oF3pXniu3CK-n6ajL2bnNRMZ4Js9kCqgkZH_UoTI,76
149
+ meerschaum/config/_version.py,sha256=qSHUj8MxnJ9gRyXkAhkZxz2mL83tODyagATrnfHLw2c,74
150
150
  meerschaum/config/paths.py,sha256=JjibeGN3YAdSNceRwsd42aNmeUrIgM6ndzC8qZAmNI0,621
151
151
  meerschaum/config/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
152
152
  meerschaum/config/stack/__init__.py,sha256=2UukC0Lmk-aVL1o1qXzumqmuIrw3vu9fD7iCuz4XD4I,10544
@@ -173,15 +173,15 @@ meerschaum/connectors/api/_uri.py,sha256=HWxqGx4R1cHZ3ywy9Ro9ePbFxxusw4RLaC3hpGt
173
173
  meerschaum/connectors/api/_users.py,sha256=kzb7ENgXwQ19OJYKOuuWzx2rwVuUZCly9dTnyvVuT2Q,5275
174
174
  meerschaum/connectors/plugin/PluginConnector.py,sha256=aQ1QaB7MordCFimZqoGLb0R12PfDUN_nWks2J5mzeAs,2084
175
175
  meerschaum/connectors/plugin/__init__.py,sha256=pwF7TGY4WNz2_HaVdmK4rPQ9ZwTOEuPHgzOqsGcoXJw,198
176
- meerschaum/connectors/sql/_SQLConnector.py,sha256=JVTK2LdUAtgK_fg0zgHOxgJHw7X7s0SHOunBZ9b-ai8,12084
176
+ meerschaum/connectors/sql/_SQLConnector.py,sha256=FmosRX4PKR7AAW-PgX69R_32AsA7E7preQXJf4xc3WI,12240
177
177
  meerschaum/connectors/sql/__init__.py,sha256=3cqYiDkVasn7zWdtOTAZbT4bo95AuvGOmDD2TkaAxtw,205
178
- meerschaum/connectors/sql/_cli.py,sha256=3wXRfPSr5mXlM6Wt8UqrBYfWvkLVZ4jTzKRUd04enCo,5116
179
- meerschaum/connectors/sql/_create_engine.py,sha256=h7c1nwdDWi33PBkRioPomHRT8h1DLc08EQ7INWyC_1Q,10717
178
+ meerschaum/connectors/sql/_cli.py,sha256=smwMBxq-euAeefbdZSXGTr83cm04GVGT1WIFEftMLa4,5142
179
+ meerschaum/connectors/sql/_create_engine.py,sha256=RRiTNNVNwE_sabeTRWrlZZoFB6jFXDfcEcXkENuo8Rc,12035
180
180
  meerschaum/connectors/sql/_fetch.py,sha256=mVe5zQo7SM9PSUU3Vjhacg4Bq1-Vttb7KkXL4p5YQdQ,12818
181
181
  meerschaum/connectors/sql/_instance.py,sha256=xCc8M0xWMzF5Tu_1uWIFivAoHey5N1ccFhN_Z7u04zk,6304
182
- meerschaum/connectors/sql/_pipes.py,sha256=8pp10lOYX8oVEqKHB9907Jny4nkT9fTgF8PrvhsdohI,128855
182
+ meerschaum/connectors/sql/_pipes.py,sha256=RAE3ACk7U0-Z2jnXBFJt_laKIqA4EABxPuDApPHkTc8,129185
183
183
  meerschaum/connectors/sql/_plugins.py,sha256=OVEdZ_UHTi-x5sF-5lu2TmR9ONxddp6SwDOmFo5TpU8,8051
184
- meerschaum/connectors/sql/_sql.py,sha256=3hrANOId2DAoXsl8nvePnxvoXo5rtB5UfQsJK_fCY9s,42696
184
+ meerschaum/connectors/sql/_sql.py,sha256=EziDFp9mLU-VPvFczG94ZUtGy-JmKxXB4wbLtCfkykk,43786
185
185
  meerschaum/connectors/sql/_uri.py,sha256=BFzu5pjlbL3kxLH13vHWlpKGYTPfg8wuA2j58O9NsCM,3440
186
186
  meerschaum/connectors/sql/_users.py,sha256=mRyjsUCfPV52nfTQUbpu9gMXfV_DHXNqEhw4N-lSS4Q,9954
187
187
  meerschaum/connectors/sql/tools.py,sha256=jz8huOaRCwGlYdtGfAqAh7SoK8uydYBrasKQba9FT38,187
@@ -223,7 +223,7 @@ meerschaum/plugins/__init__.py,sha256=Tl5B0Q4rIfgkPpgknJH3UKKB3fS_cAWI9TspKosvBP
223
223
  meerschaum/plugins/bootstrap.py,sha256=VwjpZAuYdqPJW0YoVgAoM_taHkdQHqP902-8T7OWWCI,11339
224
224
  meerschaum/utils/__init__.py,sha256=QrK1K9hIbPCRCM5k2nZGFqGnrqhA0Eh-iSmCU7FG6Cs,612
225
225
  meerschaum/utils/_get_pipes.py,sha256=tu4xKPoDn79Dz2kWM13cXTP4DSCkn-3G9M8KiLftopw,11073
226
- meerschaum/utils/dataframe.py,sha256=f9h3fmG_ePHHls2NQmGHNqUurUEZBBvpS1UR7tQgwjI,49341
226
+ meerschaum/utils/dataframe.py,sha256=SfwFGqBTGVcA5ulWEXOV2GZ1633JPcRXxF_l6oQ7L5o,52020
227
227
  meerschaum/utils/debug.py,sha256=GyIzJmunkoPnOcZNYVQdT4Sgd-aOb5MI2VbIgATOjIQ,3695
228
228
  meerschaum/utils/interactive.py,sha256=t-6jWozXSqL7lYGDHuwiOjTgr-UKhdcg61q_eR5mikI,3196
229
229
  meerschaum/utils/misc.py,sha256=8TOQQlFyF_aYnc8tnx98lccXr9tFrdlS-ngXeOQjHHY,47407
@@ -232,7 +232,7 @@ meerschaum/utils/pool.py,sha256=vkE42af4fjrTEJTxf6Ek3xGucm1MtEkpsSEiaVzNKHs,2655
232
232
  meerschaum/utils/process.py,sha256=as0-CjG4mqFP0TydVvmAmgki6er4thS5BqUopeiq98Q,8216
233
233
  meerschaum/utils/prompt.py,sha256=qj1As1tuiL0GZTku_YOC6I5DmOU6L5otDR7DW7LA5fM,19397
234
234
  meerschaum/utils/schedule.py,sha256=Vrcd2Qs-UPVn6xBayNUIgludf0Mlb6Wrgq6ATdyhV8c,11451
235
- meerschaum/utils/sql.py,sha256=2p8pa3kBCE5lM6tDle5HV4AamZCgOpnKmCB6mdkiLUA,80287
235
+ meerschaum/utils/sql.py,sha256=wOm_9bA2HRn-TGYqyiqwU3ILk3JBOH08X8e5k0TEDes,80786
236
236
  meerschaum/utils/threading.py,sha256=awjbVL_QR6G-o_9Qk85utac9cSdqkiC8tQSdERCdrG8,2814
237
237
  meerschaum/utils/typing.py,sha256=U3MC347sh1umpa3Xr1k71eADyDmk4LB6TnVCpq8dVzI,2830
238
238
  meerschaum/utils/warnings.py,sha256=n-phr3BftNNgyPnvnXC_VMSjtCvjiCZ-ewmVfcROhkc,6611
@@ -243,23 +243,23 @@ meerschaum/utils/daemon/RotatingFile.py,sha256=8_bXegBjjzNRlNEjFZ_EHU4pSaDfjXZTw
243
243
  meerschaum/utils/daemon/StdinFile.py,sha256=qdZ8E_RSOkURypwnS50mWeyWyRig1bAY9tKWMTVKajc,3307
244
244
  meerschaum/utils/daemon/__init__.py,sha256=ziRPyu_IM3l7Xd58y3Uvt0fZLoirJ9nuboFIxxult6c,8741
245
245
  meerschaum/utils/daemon/_names.py,sha256=d2ZwTxBoTAqXZkCfZ5LuX2XrkQmLNUq1OTlUqfoH5dA,4515
246
- meerschaum/utils/dtypes/__init__.py,sha256=O4EfRcv53NwvHYZpyP0kfJjS-I-9dUFdXUd78hoy0ZU,15492
247
- meerschaum/utils/dtypes/sql.py,sha256=c-3ib_NMklCAJBabGFa712o97VQnAyEyj8NRACKMo6s,22230
246
+ meerschaum/utils/dtypes/__init__.py,sha256=nFlslv8aL_UFscWNJOwY19F5KFvXsXAR0_UX0rjF6D8,20669
247
+ meerschaum/utils/dtypes/sql.py,sha256=MhuFT3wY8cx_o556a-oa3sKVKmAus0_O8ETbIIRlXik,26074
248
248
  meerschaum/utils/formatting/__init__.py,sha256=bA8qwBeTNIVHVQOBK682bJsKSKik1yS6xYJAoi0RErk,15528
249
249
  meerschaum/utils/formatting/_jobs.py,sha256=izsqPJhTtUkXUUtWnbXtReYsUYwulXtci3pBj72Ne64,6637
250
250
  meerschaum/utils/formatting/_pipes.py,sha256=gwl8-xCN5GYqBZJ7SkY20BebcofY0nU5X8Y4Emf5dz8,19570
251
251
  meerschaum/utils/formatting/_pprint.py,sha256=wyTmjHFnsHbxfyuytjTWzH-D42Z65GuIisQ_W6UnRPg,3096
252
252
  meerschaum/utils/formatting/_shell.py,sha256=2bFvtwNXapjl9jdlc0fg79PRWHbYVcllKiVcG5g36qI,3678
253
253
  meerschaum/utils/packages/__init__.py,sha256=TdKaj2tmN4bFwzusOfMv24P5ET7Zv73vyoOf9GOIr5E,64427
254
- meerschaum/utils/packages/_packages.py,sha256=_ygc4zH7z80TuxRz7ThIm8TuMtklpPWa4Yv8WoDjiSk,8928
254
+ meerschaum/utils/packages/_packages.py,sha256=BWBJRlwWqZD4SPcM-SbVxEJ2pwclUhioSeP1ldSiYWE,9047
255
255
  meerschaum/utils/packages/lazy_loader.py,sha256=VHnph3VozH29R4JnSSBfwtA5WKZYZQFT_GeQSShCnuc,2540
256
256
  meerschaum/utils/venv/_Venv.py,sha256=gc1TCeAj-kTZbQFAT9xl1bi4HXFV5ApT0dPOJfxwr78,3748
257
257
  meerschaum/utils/venv/__init__.py,sha256=6FDfOSBsGgw2RIXvBuFEwlF5740RIHs4Qum0ekati9I,27249
258
- meerschaum-2.9.0.dev1.dist-info/LICENSE,sha256=jG2zQEdRNt88EgHUWPpXVWmOrOduUQRx7MnYV9YIPaw,11359
259
- meerschaum-2.9.0.dev1.dist-info/METADATA,sha256=xo2G8mHGjiEnAk4r3W5sfOxPGxT2TPg58zhpE7MPRPA,24639
260
- meerschaum-2.9.0.dev1.dist-info/NOTICE,sha256=OTA9Fcthjf5BRvWDDIcBC_xfLpeDV-RPZh3M-HQBRtQ,114
261
- meerschaum-2.9.0.dev1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
262
- meerschaum-2.9.0.dev1.dist-info/entry_points.txt,sha256=5YBVzibw-0rNA_1VjB16z5GABsOGf-CDhW4yqH8C7Gc,88
263
- meerschaum-2.9.0.dev1.dist-info/top_level.txt,sha256=bNoSiDj0El6buocix-FRoAtJOeq1qOF5rRm2u9i7Q6A,11
264
- meerschaum-2.9.0.dev1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
265
- meerschaum-2.9.0.dev1.dist-info/RECORD,,
258
+ meerschaum-2.9.0rc2.dist-info/LICENSE,sha256=jG2zQEdRNt88EgHUWPpXVWmOrOduUQRx7MnYV9YIPaw,11359
259
+ meerschaum-2.9.0rc2.dist-info/METADATA,sha256=18-lm3TfFldZ0KZdp6YkgF5HDpzfPii7thoQ7ibqMuA,24930
260
+ meerschaum-2.9.0rc2.dist-info/NOTICE,sha256=OTA9Fcthjf5BRvWDDIcBC_xfLpeDV-RPZh3M-HQBRtQ,114
261
+ meerschaum-2.9.0rc2.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
262
+ meerschaum-2.9.0rc2.dist-info/entry_points.txt,sha256=5YBVzibw-0rNA_1VjB16z5GABsOGf-CDhW4yqH8C7Gc,88
263
+ meerschaum-2.9.0rc2.dist-info/top_level.txt,sha256=bNoSiDj0El6buocix-FRoAtJOeq1qOF5rRm2u9i7Q6A,11
264
+ meerschaum-2.9.0rc2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
265
+ meerschaum-2.9.0rc2.dist-info/RECORD,,