meerschaum 2.9.5__py3-none-any.whl → 3.0.0rc1__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.
- meerschaum/__init__.py +5 -2
- meerschaum/_internal/__init__.py +1 -0
- meerschaum/_internal/arguments/_parse_arguments.py +4 -4
- meerschaum/_internal/arguments/_parser.py +17 -1
- meerschaum/_internal/entry.py +6 -6
- meerschaum/_internal/shell/Shell.py +1 -1
- meerschaum/_internal/static.py +372 -0
- meerschaum/actions/api.py +12 -2
- meerschaum/actions/bootstrap.py +7 -7
- meerschaum/actions/edit.py +142 -18
- meerschaum/actions/register.py +137 -6
- meerschaum/actions/show.py +117 -29
- meerschaum/actions/stop.py +4 -1
- meerschaum/actions/sync.py +1 -1
- meerschaum/actions/tag.py +9 -8
- meerschaum/api/__init__.py +9 -2
- meerschaum/api/_events.py +39 -2
- meerschaum/api/_oauth2.py +118 -8
- meerschaum/api/_tokens.py +102 -0
- meerschaum/api/dash/__init__.py +0 -1
- meerschaum/api/dash/callbacks/custom.py +2 -2
- meerschaum/api/dash/callbacks/dashboard.py +102 -18
- meerschaum/api/dash/callbacks/plugins.py +0 -1
- meerschaum/api/dash/callbacks/register.py +1 -1
- meerschaum/api/dash/callbacks/settings/__init__.py +1 -0
- meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
- meerschaum/api/dash/callbacks/settings/tokens.py +388 -0
- meerschaum/api/dash/components.py +30 -8
- meerschaum/api/dash/keys.py +19 -93
- meerschaum/api/dash/pages/dashboard.py +1 -20
- meerschaum/api/dash/pages/settings/__init__.py +1 -0
- meerschaum/api/dash/pages/settings/password_reset.py +1 -1
- meerschaum/api/dash/pages/settings/tokens.py +55 -0
- meerschaum/api/dash/pipes.py +94 -59
- meerschaum/api/dash/sessions.py +12 -0
- meerschaum/api/dash/tokens.py +606 -0
- meerschaum/api/dash/websockets.py +1 -1
- meerschaum/api/dash/webterm.py +4 -0
- meerschaum/api/models/__init__.py +23 -3
- meerschaum/api/models/_actions.py +22 -0
- meerschaum/api/models/_pipes.py +85 -7
- meerschaum/api/models/_tokens.py +81 -0
- meerschaum/api/resources/templates/termpage.html +12 -0
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +3 -4
- meerschaum/api/routes/_connectors.py +3 -7
- meerschaum/api/routes/_jobs.py +14 -35
- meerschaum/api/routes/_login.py +49 -12
- meerschaum/api/routes/_misc.py +5 -10
- meerschaum/api/routes/_pipes.py +134 -111
- meerschaum/api/routes/_plugins.py +38 -28
- meerschaum/api/routes/_tokens.py +236 -0
- meerschaum/api/routes/_users.py +47 -35
- meerschaum/api/routes/_version.py +3 -3
- meerschaum/config/__init__.py +43 -20
- meerschaum/config/_default.py +32 -5
- meerschaum/config/_edit.py +28 -24
- meerschaum/config/_environment.py +1 -1
- meerschaum/config/_patch.py +6 -6
- meerschaum/config/_paths.py +5 -1
- meerschaum/config/_read_config.py +65 -34
- meerschaum/config/_sync.py +6 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +24 -5
- meerschaum/config/static.py +18 -0
- meerschaum/connectors/_Connector.py +10 -4
- meerschaum/connectors/__init__.py +4 -20
- meerschaum/connectors/api/_APIConnector.py +34 -6
- meerschaum/connectors/api/_actions.py +2 -2
- meerschaum/connectors/api/_jobs.py +1 -1
- meerschaum/connectors/api/_login.py +33 -7
- meerschaum/connectors/api/_misc.py +2 -2
- meerschaum/connectors/api/_pipes.py +15 -14
- meerschaum/connectors/api/_plugins.py +2 -2
- meerschaum/connectors/api/_request.py +1 -1
- meerschaum/connectors/api/_tokens.py +146 -0
- meerschaum/connectors/api/_users.py +70 -58
- meerschaum/connectors/instance/_InstanceConnector.py +83 -0
- meerschaum/connectors/instance/__init__.py +10 -0
- meerschaum/connectors/instance/_pipes.py +442 -0
- meerschaum/connectors/instance/_plugins.py +151 -0
- meerschaum/connectors/instance/_tokens.py +296 -0
- meerschaum/connectors/instance/_users.py +181 -0
- meerschaum/connectors/parse.py +4 -1
- meerschaum/connectors/sql/_SQLConnector.py +8 -5
- meerschaum/connectors/sql/_cli.py +12 -11
- meerschaum/connectors/sql/_create_engine.py +6 -154
- meerschaum/connectors/sql/_fetch.py +2 -18
- meerschaum/connectors/sql/_pipes.py +42 -31
- meerschaum/connectors/sql/_plugins.py +29 -0
- meerschaum/connectors/sql/_sql.py +8 -1
- meerschaum/connectors/sql/_users.py +29 -2
- meerschaum/connectors/sql/tables/__init__.py +1 -1
- meerschaum/connectors/valkey/_ValkeyConnector.py +2 -4
- meerschaum/connectors/valkey/_pipes.py +9 -10
- meerschaum/connectors/valkey/_plugins.py +2 -26
- meerschaum/core/Pipe/__init__.py +31 -14
- meerschaum/core/Pipe/_attributes.py +156 -58
- meerschaum/core/Pipe/_bootstrap.py +54 -24
- meerschaum/core/Pipe/_data.py +41 -1
- meerschaum/core/Pipe/_dtypes.py +29 -14
- meerschaum/core/Pipe/_edit.py +12 -4
- meerschaum/core/Pipe/_show.py +5 -5
- meerschaum/core/Pipe/_sync.py +48 -53
- meerschaum/core/Pipe/_verify.py +1 -1
- meerschaum/{plugins → core/Plugin}/_Plugin.py +9 -11
- meerschaum/core/Plugin/__init__.py +1 -1
- meerschaum/core/Token/_Token.py +221 -0
- meerschaum/core/Token/__init__.py +12 -0
- meerschaum/core/User/_User.py +34 -8
- meerschaum/core/User/__init__.py +9 -1
- meerschaum/core/__init__.py +1 -0
- meerschaum/jobs/_Job.py +3 -2
- meerschaum/jobs/__init__.py +3 -2
- meerschaum/jobs/systemd.py +1 -1
- meerschaum/models/__init__.py +35 -0
- meerschaum/models/pipes.py +247 -0
- meerschaum/models/tokens.py +38 -0
- meerschaum/models/users.py +26 -0
- meerschaum/plugins/__init__.py +22 -7
- meerschaum/plugins/bootstrap.py +2 -1
- meerschaum/utils/_get_pipes.py +68 -27
- meerschaum/utils/daemon/Daemon.py +2 -1
- meerschaum/utils/daemon/__init__.py +30 -2
- meerschaum/utils/dataframe.py +95 -14
- meerschaum/utils/dtypes/__init__.py +91 -18
- meerschaum/utils/dtypes/sql.py +44 -0
- meerschaum/utils/formatting/__init__.py +1 -1
- meerschaum/utils/formatting/_pipes.py +5 -4
- meerschaum/utils/formatting/_shell.py +11 -9
- meerschaum/utils/misc.py +237 -80
- meerschaum/utils/packages/__init__.py +3 -6
- meerschaum/utils/packages/_packages.py +34 -32
- meerschaum/utils/pipes.py +181 -0
- meerschaum/utils/process.py +1 -1
- meerschaum/utils/prompt.py +3 -1
- meerschaum/utils/schedule.py +1 -0
- meerschaum/utils/sql.py +114 -37
- meerschaum/utils/typing.py +1 -4
- meerschaum/utils/venv/_Venv.py +2 -2
- meerschaum/utils/venv/__init__.py +5 -7
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/METADATA +88 -80
- meerschaum-3.0.0rc1.dist-info/RECORD +282 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/WHEEL +1 -1
- meerschaum/api/models/_interfaces.py +0 -15
- meerschaum/api/models/_locations.py +0 -15
- meerschaum/api/models/_metrics.py +0 -15
- meerschaum/config/static/__init__.py +0 -186
- meerschaum-2.9.5.dist-info/RECORD +0 -263
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0rc1.dist-info}/zip-safe +0 -0
meerschaum/utils/dataframe.py
CHANGED
@@ -1003,6 +1003,7 @@ def enforce_dtypes(
|
|
1003
1003
|
attempt_cast_to_bytes,
|
1004
1004
|
attempt_cast_to_geometry,
|
1005
1005
|
coerce_timezone as _coerce_timezone,
|
1006
|
+
get_geometry_type_srid,
|
1006
1007
|
)
|
1007
1008
|
from meerschaum.utils.dtypes.sql import get_numeric_precision_scale
|
1008
1009
|
pandas = mrsm.attempt_import('pandas')
|
@@ -1028,11 +1029,11 @@ def enforce_dtypes(
|
|
1028
1029
|
for col, typ in dtypes.items()
|
1029
1030
|
if typ.startswith('numeric')
|
1030
1031
|
]
|
1031
|
-
|
1032
|
-
col
|
1032
|
+
geometry_cols_types_srids = {
|
1033
|
+
col: get_geometry_type_srid(typ, default_srid=0)
|
1033
1034
|
for col, typ in dtypes.items()
|
1034
1035
|
if typ.startswith('geometry') or typ.startswith('geography')
|
1035
|
-
|
1036
|
+
}
|
1036
1037
|
uuid_cols = [
|
1037
1038
|
col
|
1038
1039
|
for col, typ in dtypes.items()
|
@@ -1120,14 +1121,24 @@ def enforce_dtypes(
|
|
1120
1121
|
dprint(f"Checking for datetime conversion: {datetime_cols}")
|
1121
1122
|
for col in datetime_cols:
|
1122
1123
|
if col in df.columns:
|
1124
|
+
if debug:
|
1125
|
+
dprint(
|
1126
|
+
f"Data type for column '{col}' before timezone coersion: "
|
1127
|
+
f"{str(df[col].dtype)}"
|
1128
|
+
)
|
1123
1129
|
df[col] = _coerce_timezone(df[col], strip_utc=strip_timezone)
|
1130
|
+
if debug:
|
1131
|
+
dprint(
|
1132
|
+
f"Data type for column '{col}' after timezone coersion: "
|
1133
|
+
f"{str(df[col].dtype)}"
|
1134
|
+
)
|
1124
1135
|
|
1125
|
-
if
|
1136
|
+
if geometry_cols_types_srids:
|
1126
1137
|
geopandas = mrsm.attempt_import('geopandas')
|
1127
1138
|
if debug:
|
1128
|
-
dprint(f"Checking for geometry: {
|
1139
|
+
dprint(f"Checking for geometry: {list(geometry_cols_types_srids)}")
|
1129
1140
|
parsed_geom_cols = []
|
1130
|
-
for col in
|
1141
|
+
for col in geometry_cols_types_srids:
|
1131
1142
|
try:
|
1132
1143
|
df[col] = df[col].apply(attempt_cast_to_geometry)
|
1133
1144
|
parsed_geom_cols.append(col)
|
@@ -1139,10 +1150,19 @@ def enforce_dtypes(
|
|
1139
1150
|
if debug:
|
1140
1151
|
dprint(f"Converting to GeoDataFrame (geometry column: '{parsed_geom_cols[0]}')...")
|
1141
1152
|
try:
|
1142
|
-
|
1143
|
-
df.
|
1153
|
+
_, default_srid = geometry_cols_types_srids[parsed_geom_cols[0]]
|
1154
|
+
df = geopandas.GeoDataFrame(df, geometry=parsed_geom_cols[0], crs=default_srid)
|
1155
|
+
for col, (_, srid) in geometry_cols_types_srids.items():
|
1156
|
+
if srid:
|
1157
|
+
if debug:
|
1158
|
+
dprint(f"Setting '{col}' to SRID '{srid}'...")
|
1159
|
+
_ = df[col].set_crs(srid)
|
1160
|
+
if parsed_geom_cols[0] not in df.columns:
|
1161
|
+
df.rename_geometry(parsed_geom_cols[0], inplace=True)
|
1144
1162
|
except (ValueError, TypeError):
|
1145
|
-
|
1163
|
+
if debug:
|
1164
|
+
import traceback
|
1165
|
+
dprint(f"Failed to cast to GeoDataFrame:\n{traceback.format_exc()}")
|
1146
1166
|
|
1147
1167
|
df_dtypes = {c: str(t) for c, t in df.dtypes.items()}
|
1148
1168
|
if are_dtypes_equal(df_dtypes, pipe_pandas_dtypes):
|
@@ -1386,7 +1406,7 @@ def chunksize_to_npartitions(chunksize: Optional[int]) -> int:
|
|
1386
1406
|
|
1387
1407
|
def df_from_literal(
|
1388
1408
|
pipe: Optional[mrsm.Pipe] = None,
|
1389
|
-
literal: str = None,
|
1409
|
+
literal: Optional[str] = None,
|
1390
1410
|
debug: bool = False
|
1391
1411
|
) -> 'pd.DataFrame':
|
1392
1412
|
"""
|
@@ -1408,8 +1428,8 @@ def df_from_literal(
|
|
1408
1428
|
|
1409
1429
|
if pipe is None or literal is None:
|
1410
1430
|
error("Please provide a Pipe and a literal value")
|
1411
|
-
|
1412
|
-
|
1431
|
+
dt_col = pipe.columns.get('datetime', 'ts')
|
1432
|
+
val_col = pipe.get_val_column(debug=debug)
|
1413
1433
|
|
1414
1434
|
val = literal
|
1415
1435
|
if isinstance(literal, str):
|
@@ -1429,7 +1449,7 @@ def df_from_literal(
|
|
1429
1449
|
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
1430
1450
|
|
1431
1451
|
pd = import_pandas()
|
1432
|
-
return pd.DataFrame({
|
1452
|
+
return pd.DataFrame({dt_col: [now], val_col: [val]})
|
1433
1453
|
|
1434
1454
|
|
1435
1455
|
def get_first_valid_dask_partition(ddf: 'dask.dataframe.DataFrame') -> Union['pd.DataFrame', None]:
|
@@ -1533,9 +1553,14 @@ def query_df(
|
|
1533
1553
|
NA = pandas.NA
|
1534
1554
|
|
1535
1555
|
if params:
|
1556
|
+
proto_in_ex_params = get_in_ex_params(params)
|
1557
|
+
for key, (proto_in_vals, proto_ex_vals) in proto_in_ex_params.items():
|
1558
|
+
if proto_ex_vals:
|
1559
|
+
coerce_types = True
|
1560
|
+
break
|
1536
1561
|
params = params.copy()
|
1537
1562
|
for key, val in {k: v for k, v in params.items()}.items():
|
1538
|
-
if isinstance(val, (list, tuple)):
|
1563
|
+
if isinstance(val, (list, tuple, set)) or hasattr(val, 'astype'):
|
1539
1564
|
if None in val:
|
1540
1565
|
val = [item for item in val if item is not None] + [NA]
|
1541
1566
|
params[key] = val
|
@@ -1742,6 +1767,10 @@ def to_json(
|
|
1742
1767
|
bytes_cols = get_bytes_cols(df)
|
1743
1768
|
numeric_cols = get_numeric_cols(df)
|
1744
1769
|
geometry_cols = get_geometry_cols(df)
|
1770
|
+
geometry_cols_srids = {
|
1771
|
+
col: int((getattr(df[col].crs, 'srs', '') or '').split(':', maxsplit=1)[-1] or '0')
|
1772
|
+
for col in geometry_cols
|
1773
|
+
} if 'geodataframe' in str(type(df)).lower() else {}
|
1745
1774
|
if safe_copy and bool(uuid_cols or bytes_cols or geometry_cols or numeric_cols):
|
1746
1775
|
df = df.copy()
|
1747
1776
|
for col in uuid_cols:
|
@@ -1753,10 +1782,12 @@ def to_json(
|
|
1753
1782
|
with warnings.catch_warnings():
|
1754
1783
|
warnings.simplefilter("ignore")
|
1755
1784
|
for col in geometry_cols:
|
1785
|
+
srid = geometry_cols_srids.get(col, None) or None
|
1756
1786
|
df[col] = df[col].apply(
|
1757
1787
|
functools.partial(
|
1758
1788
|
serialize_geometry,
|
1759
1789
|
geometry_format=geometry_format,
|
1790
|
+
srid=srid,
|
1760
1791
|
)
|
1761
1792
|
)
|
1762
1793
|
return df.infer_objects(copy=False).fillna(pd.NA).to_json(
|
@@ -1766,3 +1797,53 @@ def to_json(
|
|
1766
1797
|
orient=orient,
|
1767
1798
|
**kwargs
|
1768
1799
|
)
|
1800
|
+
|
1801
|
+
|
1802
|
+
def to_simple_lines(df: 'pd.DataFrame') -> str:
|
1803
|
+
"""
|
1804
|
+
Serialize a Pandas Dataframe as lines of simple dictionaries.
|
1805
|
+
|
1806
|
+
Parameters
|
1807
|
+
----------
|
1808
|
+
df: pd.DataFrame
|
1809
|
+
The dataframe to serialize into simple lines text.
|
1810
|
+
|
1811
|
+
Returns
|
1812
|
+
-------
|
1813
|
+
A string of simple line dictionaries joined by newlines.
|
1814
|
+
"""
|
1815
|
+
from meerschaum.utils.misc import to_simple_dict
|
1816
|
+
if df is None or len(df) == 0:
|
1817
|
+
return ''
|
1818
|
+
|
1819
|
+
docs = df.to_dict(orient='records')
|
1820
|
+
return '\n'.join(to_simple_dict(doc) for doc in docs)
|
1821
|
+
|
1822
|
+
|
1823
|
+
def parse_simple_lines(data: str) -> 'pd.DataFrame':
|
1824
|
+
"""
|
1825
|
+
Parse simple lines text into a DataFrame.
|
1826
|
+
|
1827
|
+
Parameters
|
1828
|
+
----------
|
1829
|
+
data: str
|
1830
|
+
The simple lines text to parse into a DataFrame.
|
1831
|
+
|
1832
|
+
Returns
|
1833
|
+
-------
|
1834
|
+
A dataframe containing the rows serialized in `data`.
|
1835
|
+
"""
|
1836
|
+
from meerschaum.utils.misc import string_to_dict
|
1837
|
+
from meerschaum.utils.packages import import_pandas
|
1838
|
+
pd = import_pandas()
|
1839
|
+
lines = data.splitlines()
|
1840
|
+
try:
|
1841
|
+
docs = [string_to_dict(line) for line in lines]
|
1842
|
+
df = pd.DataFrame(docs)
|
1843
|
+
except Exception:
|
1844
|
+
df = None
|
1845
|
+
|
1846
|
+
if df is None:
|
1847
|
+
raise ValueError("Cannot parse simple lines into a dataframe.")
|
1848
|
+
|
1849
|
+
return df
|
@@ -309,7 +309,7 @@ def attempt_cast_to_geometry(value: Any) -> Any:
|
|
309
309
|
if isinstance(value, (dict, list)):
|
310
310
|
try:
|
311
311
|
return shapely.from_geojson(json.dumps(value))
|
312
|
-
except Exception
|
312
|
+
except Exception:
|
313
313
|
return value
|
314
314
|
|
315
315
|
value_is_wkt = geometry_is_wkt(value)
|
@@ -361,7 +361,7 @@ def value_is_null(value: Any) -> bool:
|
|
361
361
|
"""
|
362
362
|
Determine if a value is a null-like string.
|
363
363
|
"""
|
364
|
-
return str(value).lower() in ('none', 'nan', 'na', 'nat', '', '<na>')
|
364
|
+
return str(value).lower() in ('none', 'nan', 'na', 'nat', 'natz', '', '<na>')
|
365
365
|
|
366
366
|
|
367
367
|
def none_if_null(value: Any) -> Any:
|
@@ -455,7 +455,10 @@ def coerce_timezone(
|
|
455
455
|
|
456
456
|
if isinstance(dt, str):
|
457
457
|
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
458
|
-
|
458
|
+
try:
|
459
|
+
dt = dateutil_parser.parse(dt)
|
460
|
+
except Exception:
|
461
|
+
return dt
|
459
462
|
|
460
463
|
dt_is_series = hasattr(dt, 'dtype') and hasattr(dt, '__module__')
|
461
464
|
|
@@ -472,6 +475,8 @@ def coerce_timezone(
|
|
472
475
|
return dt
|
473
476
|
|
474
477
|
dt_series = to_datetime(dt, coerce_utc=False)
|
478
|
+
if dt_series.dt.tz is None:
|
479
|
+
dt_series = dt_series.dt.tz_localize(timezone.utc)
|
475
480
|
if strip_utc:
|
476
481
|
try:
|
477
482
|
if dt_series.dt.tz is not None:
|
@@ -502,25 +507,69 @@ def to_datetime(dt_val: Any, as_pydatetime: bool = False, coerce_utc: bool = Tru
|
|
502
507
|
dt_is_series = hasattr(dt_val, 'dtype') and hasattr(dt_val, '__module__')
|
503
508
|
pd = pandas if dd is None else dd
|
504
509
|
|
505
|
-
try:
|
506
|
-
new_dt_val = pd.to_datetime(dt_val, utc=True, format='ISO8601')
|
507
|
-
if as_pydatetime:
|
508
|
-
return new_dt_val.to_pydatetime()
|
509
|
-
return new_dt_val
|
510
|
-
except (pd.errors.OutOfBoundsDatetime, ValueError):
|
511
|
-
pass
|
512
|
-
|
513
510
|
def parse(x: Any) -> Any:
|
514
511
|
try:
|
515
512
|
return dateutil_parser.parse(x)
|
516
513
|
except Exception:
|
517
514
|
return x
|
518
515
|
|
516
|
+
if isinstance(dt_val, pd.Timestamp):
|
517
|
+
dt_val_to_return = dt_val if not as_pydatetime else dt_val.to_pydatetime()
|
518
|
+
return (
|
519
|
+
coerce_timezone(dt_val_to_return)
|
520
|
+
if coerce_utc
|
521
|
+
else dt_val_to_return
|
522
|
+
)
|
523
|
+
|
519
524
|
if dt_is_series:
|
520
|
-
|
525
|
+
changed_tz = False
|
526
|
+
original_tz = None
|
527
|
+
dtype = str(getattr(dt_val, 'dtype', 'object'))
|
528
|
+
if are_dtypes_equal(dtype, 'datetime') and 'utc' not in dtype.lower():
|
529
|
+
original_tz = dt_val.dt.tz
|
530
|
+
dt_val = dt_val.dt.tz_localize(timezone.utc)
|
531
|
+
changed_tz = True
|
532
|
+
dtype = str(getattr(dt_val, 'dtype', 'object'))
|
533
|
+
try:
|
534
|
+
new_dt_series = (
|
535
|
+
dt_val
|
536
|
+
if dtype == 'datetime64[ns, UTC]'
|
537
|
+
else dt_val.astype("datetime64[ns, UTC]")
|
538
|
+
)
|
539
|
+
except pd.errors.OutOfBoundsDatetime:
|
540
|
+
try:
|
541
|
+
new_dt_series = dt_val.astype("datetime64[ms, UTC]")
|
542
|
+
except Exception:
|
543
|
+
new_dt_series = None
|
544
|
+
except ValueError:
|
545
|
+
new_dt_series = None
|
546
|
+
except TypeError:
|
547
|
+
try:
|
548
|
+
new_dt_series = (
|
549
|
+
new_dt_series
|
550
|
+
if str(getattr(new_dt_series, 'dtype', None)) == 'datetime64[ns]'
|
551
|
+
else dt_val.astype("datetime64[ns]")
|
552
|
+
)
|
553
|
+
except Exception:
|
554
|
+
new_dt_series = None
|
555
|
+
|
556
|
+
if new_dt_series is None:
|
557
|
+
new_dt_series = dt_val.apply(lambda x: parse(str(x)))
|
558
|
+
|
521
559
|
if coerce_utc:
|
522
|
-
return coerce_timezone(
|
523
|
-
|
560
|
+
return coerce_timezone(new_dt_series)
|
561
|
+
|
562
|
+
if changed_tz:
|
563
|
+
new_dt_series = new_dt_series.dt.tz_localize(original_tz)
|
564
|
+
return new_dt_series
|
565
|
+
|
566
|
+
try:
|
567
|
+
new_dt_val = pd.to_datetime(dt_val, utc=True, format='ISO8601')
|
568
|
+
if as_pydatetime:
|
569
|
+
return new_dt_val.to_pydatetime()
|
570
|
+
return new_dt_val
|
571
|
+
except (pd.errors.OutOfBoundsDatetime, ValueError):
|
572
|
+
pass
|
524
573
|
|
525
574
|
new_dt_val = parse(dt_val)
|
526
575
|
if not coerce_utc:
|
@@ -541,6 +590,7 @@ def serialize_bytes(data: bytes) -> str:
|
|
541
590
|
def serialize_geometry(
|
542
591
|
geom: Any,
|
543
592
|
geometry_format: str = 'wkb_hex',
|
593
|
+
srid: Optional[int] = None,
|
544
594
|
) -> Union[str, Dict[str, Any], None]:
|
545
595
|
"""
|
546
596
|
Serialize geometry data as a hex-encoded well-known-binary string.
|
@@ -555,19 +605,30 @@ def serialize_geometry(
|
|
555
605
|
Accepted formats are `wkb_hex` (well-known binary hex string),
|
556
606
|
`wkt` (well-known text), and `geojson`.
|
557
607
|
|
608
|
+
srid: Optional[int], default None
|
609
|
+
If provided, use this as the source CRS when serializing to GeoJSON.
|
610
|
+
|
558
611
|
Returns
|
559
612
|
-------
|
560
613
|
A string containing the geometry data.
|
561
614
|
"""
|
562
615
|
if value_is_null(geom):
|
563
616
|
return None
|
564
|
-
shapely = mrsm.attempt_import(
|
617
|
+
shapely, shapely_ops, pyproj = mrsm.attempt_import(
|
618
|
+
'shapely', 'shapely.ops', 'pyproj',
|
619
|
+
lazy=False,
|
620
|
+
)
|
565
621
|
if geometry_format == 'geojson':
|
622
|
+
if srid:
|
623
|
+
transformer = pyproj.Transformer.from_crs(f"EPSG:{srid}", "EPSG:4326", always_xy=True)
|
624
|
+
geom = shapely_ops.transform(transformer.transform, geom)
|
566
625
|
geojson_str = shapely.to_geojson(geom)
|
567
626
|
return json.loads(geojson_str)
|
568
627
|
|
569
628
|
if hasattr(geom, 'wkb_hex'):
|
570
|
-
|
629
|
+
if geometry_format == "wkb_hex":
|
630
|
+
return shapely.to_wkb(geom, hex=True, include_srid=True)
|
631
|
+
return shapely.to_wkt(geom)
|
571
632
|
|
572
633
|
return str(geom)
|
573
634
|
|
@@ -576,10 +637,19 @@ def deserialize_geometry(geom_wkb: Union[str, bytes]):
|
|
576
637
|
"""
|
577
638
|
Deserialize a WKB string into a shapely geometry object.
|
578
639
|
"""
|
579
|
-
shapely = mrsm.attempt_import(lazy=False)
|
640
|
+
shapely = mrsm.attempt_import('shapely', lazy=False)
|
580
641
|
return shapely.wkb.loads(geom_wkb)
|
581
642
|
|
582
643
|
|
644
|
+
def project_geometry(geom, srid: int, to_srid: int = 4326):
|
645
|
+
"""
|
646
|
+
Project a shapely geometry object to a new CRS (SRID).
|
647
|
+
"""
|
648
|
+
pyproj, shapely_ops = mrsm.attempt_import('pyproj', 'shapely.ops', lazy=False)
|
649
|
+
transformer = pyproj.Transformer.from_crs(f"EPSG:{srid}", f"EPSG:{to_srid}", always_xy=True)
|
650
|
+
return shapely_ops.transform(transformer.transform, geom)
|
651
|
+
|
652
|
+
|
583
653
|
def deserialize_bytes_string(data: Optional[str], force_hex: bool = False) -> Union[bytes, None]:
|
584
654
|
"""
|
585
655
|
Given a serialized ASCII string of bytes data, return the original bytes.
|
@@ -652,7 +722,7 @@ def serialize_datetime(dt: datetime) -> Union[str, None]:
|
|
652
722
|
return dt.isoformat() + tz_suffix
|
653
723
|
|
654
724
|
|
655
|
-
def json_serialize_value(x: Any, default_to_str: bool = True) -> str:
|
725
|
+
def json_serialize_value(x: Any, default_to_str: bool = True) -> Union[str, None]:
|
656
726
|
"""
|
657
727
|
Serialize the given value to a JSON value. Accounts for datetimes, bytes, decimals, etc.
|
658
728
|
|
@@ -687,6 +757,9 @@ def json_serialize_value(x: Any, default_to_str: bool = True) -> str:
|
|
687
757
|
if value_is_null(x):
|
688
758
|
return None
|
689
759
|
|
760
|
+
if isinstance(x, (dict, list, tuple)):
|
761
|
+
return json.dumps(x, default=json_serialize_value, separators=(',', ':'))
|
762
|
+
|
690
763
|
return str(x) if default_to_str else x
|
691
764
|
|
692
765
|
|