paleolimbot commented on code in PR #1052: URL: https://github.com/apache/sedona-db/pull/1052#discussion_r3653833470
########## python/sedonadb-geopandas/python/sedonadb_geopandas/_frame.py: ########## @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-style GeoDataFrame backed by a lazy SedonaDB frame.""" + +from sedonadb_geopandas._series import GeoSeries, Series + + +class GeoDataFrame: + """A lazy SedonaDB frame in the shape of a ``geopandas.GeoDataFrame``. + + Wraps a SedonaDB ``DataFrame`` and tracks the active geometry column. + Row selection, column access, and geometry operations mirror GeoPandas but + build a query rather than computing eagerly; call ``to_geopandas()`` to + materialize. + """ + + def __init__(self, df, geometry="geometry"): + self._df = df + self._geometry_name = geometry + + @property + def geometry(self): + """The active geometry column as a :class:`GeoSeries`.""" + return GeoSeries(self._df, self._df[self._geometry_name], self._geometry_name) + + @property + def crs(self): + """The CRS of the active geometry column (via a zero-row materialization).""" + return self._df.limit(0).to_pandas().crs + + @property + def columns(self): + """Column names, mirroring ``GeoDataFrame.columns``.""" + return list(self._df.schema.names) + + def __getitem__(self, key): + # Boolean mask -> row filter (gdf[gdf["pop"] > 1000]). + if isinstance(key, Series): + return GeoDataFrame(self._df.filter(key._expr), self._geometry_name) + + # Column subset -> GeoDataFrame (keeps the geometry column if selected). + if isinstance(key, list): + return GeoDataFrame(self._df.select(*key), self._geometry_name) + + # Single column -> (Geo)Series. + if isinstance(key, str): + expr = self._df[key] + if key == self._geometry_name: + return GeoSeries(self._df, expr, key) + return Series(self._df, expr, key) + + raise TypeError( + f"GeoDataFrame indices must be a column name, list of names, or " + f"boolean mask, not {type(key).__name__}" + ) + + def to_crs(self, crs): + """Reproject the geometry column to ``crs`` (``ST_Transform``).""" + from sedonadb.expr import lit + + transformed = self._df[self._geometry_name].geo.transform(lit(str(crs))) + new_df = self._df.mutate(**{self._geometry_name: transformed}) + return GeoDataFrame(new_df, self._geometry_name) + + def to_geopandas(self): + """Execute and return a ``geopandas.GeoDataFrame``.""" + return self._df.to_pandas() + + # Alias: results carry geometry, so this returns a GeoDataFrame too. + to_pandas = to_geopandas + + def __len__(self): + return self._df.count() + + def __repr__(self): + return repr(self.to_geopandas()) Review Comment: You probably want to make this a bit cheaper since it gets called by IDEs and such (even possibly omitting any collect whatsoever). The jupyter rich-text version of this could probably do a limit + collect to GeoPandas. ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_series.py: ########## @@ -0,0 +1,109 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""pandas/GeoPandas-style Series backed by a SedonaDB expression.""" + + +def _operand(other): + """Unwrap a Series to its expression; pass scalars through unchanged.""" + return other._expr if isinstance(other, Series) else other + + +class Series: + """A single column of a lazy SedonaDB frame, in the shape of a pandas Series. + + A ``Series`` pairs a source SedonaDB ``DataFrame`` with an expression over + its columns. Comparisons produce boolean ``Series`` usable as a filter mask + (``gdf[gdf["pop"] > 1000]``). Nothing is computed until ``to_pandas()`` / + display. + """ + + def __init__(self, df, expr, name): + self._df = df + self._expr = expr + self._name = name + + # -- element-wise comparisons -> boolean mask -------------------------- + def __gt__(self, other): + return Series(self._df, self._expr > _operand(other), self._name) + + def __ge__(self, other): + return Series(self._df, self._expr >= _operand(other), self._name) + + def __lt__(self, other): + return Series(self._df, self._expr < _operand(other), self._name) + + def __le__(self, other): + return Series(self._df, self._expr <= _operand(other), self._name) + + def __eq__(self, other): + return Series(self._df, self._expr == _operand(other), self._name) + + def __ne__(self, other): + return Series(self._df, self._expr != _operand(other), self._name) + + # -- boolean composition of masks -------------------------------------- + def __and__(self, other): + return Series(self._df, self._expr & _operand(other), self._name) + + def __or__(self, other): + return Series(self._df, self._expr | _operand(other), self._name) + + def __invert__(self): + return Series(self._df, ~self._expr, self._name) + + __hash__ = None + + # -- materialization --------------------------------------------------- + def to_pandas(self): + """Execute and return this column as a pandas (or GeoPandas) Series.""" + return self._df.select(self._expr.alias(self._name)).to_pandas()[self._name] + + def __repr__(self): + return repr(self.to_pandas()) Review Comment: You probably want to leave this as spitting out the expression (perhaps with a suggestion to collect and then extract the series) ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_frame.py: ########## @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-style GeoDataFrame backed by a lazy SedonaDB frame.""" + +from sedonadb_geopandas._series import GeoSeries, Series + + +class GeoDataFrame: + """A lazy SedonaDB frame in the shape of a ``geopandas.GeoDataFrame``. + + Wraps a SedonaDB ``DataFrame`` and tracks the active geometry column. + Row selection, column access, and geometry operations mirror GeoPandas but + build a query rather than computing eagerly; call ``to_geopandas()`` to + materialize. + """ + + def __init__(self, df, geometry="geometry"): + self._df = df + self._geometry_name = geometry + + @property + def geometry(self): + """The active geometry column as a :class:`GeoSeries`.""" + return GeoSeries(self._df, self._df[self._geometry_name], self._geometry_name) + + @property + def crs(self): + """The CRS of the active geometry column (via a zero-row materialization).""" + return self._df.limit(0).to_pandas().crs + + @property + def columns(self): + """Column names, mirroring ``GeoDataFrame.columns``.""" + return list(self._df.schema.names) + + def __getitem__(self, key): + # Boolean mask -> row filter (gdf[gdf["pop"] > 1000]). + if isinstance(key, Series): + return GeoDataFrame(self._df.filter(key._expr), self._geometry_name) + + # Column subset -> GeoDataFrame (keeps the geometry column if selected). + if isinstance(key, list): + return GeoDataFrame(self._df.select(*key), self._geometry_name) + + # Single column -> (Geo)Series. + if isinstance(key, str): + expr = self._df[key] + if key == self._geometry_name: + return GeoSeries(self._df, expr, key) + return Series(self._df, expr, key) + + raise TypeError( + f"GeoDataFrame indices must be a column name, list of names, or " + f"boolean mask, not {type(key).__name__}" + ) + + def to_crs(self, crs): + """Reproject the geometry column to ``crs`` (``ST_Transform``).""" + from sedonadb.expr import lit + + transformed = self._df[self._geometry_name].geo.transform(lit(str(crs))) Review Comment: ```suggestion from sedonadb.expr import lit transformed = self._df[self._geometry_name].geo.transform(lit(crs)) ``` (in theory lit already knows about CRS objects) ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_series.py: ########## @@ -0,0 +1,109 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""pandas/GeoPandas-style Series backed by a SedonaDB expression.""" + + +def _operand(other): + """Unwrap a Series to its expression; pass scalars through unchanged.""" + return other._expr if isinstance(other, Series) else other + + +class Series: + """A single column of a lazy SedonaDB frame, in the shape of a pandas Series. + + A ``Series`` pairs a source SedonaDB ``DataFrame`` with an expression over + its columns. Comparisons produce boolean ``Series`` usable as a filter mask + (``gdf[gdf["pop"] > 1000]``). Nothing is computed until ``to_pandas()`` / + display. + """ + + def __init__(self, df, expr, name): + self._df = df + self._expr = expr + self._name = name Review Comment: I haven't spent enough time with Pandas knockoffs to know if this is sufficient to represent a series. (Seems reasonable to me but I haven't spent much time here!) ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_series.py: ########## @@ -0,0 +1,109 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""pandas/GeoPandas-style Series backed by a SedonaDB expression.""" + + +def _operand(other): + """Unwrap a Series to its expression; pass scalars through unchanged.""" + return other._expr if isinstance(other, Series) else other Review Comment: A few gotchas could happen here worth erroring for or catching somehow - An actual Pandas `Series` or numpy array. These will probably fail with no change because SedonaDB usually rejects literals that are len != 1 I think, but the error would be clearer if you check for these in advance. It's natural somebody might want to interact with a regular GeoPandas dataframe and this dataframe in the same code. - A SedonaDB expression you might want to handle as is here ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_series.py: ########## @@ -0,0 +1,109 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""pandas/GeoPandas-style Series backed by a SedonaDB expression.""" + + +def _operand(other): + """Unwrap a Series to its expression; pass scalars through unchanged.""" + return other._expr if isinstance(other, Series) else other + + +class Series: + """A single column of a lazy SedonaDB frame, in the shape of a pandas Series. + + A ``Series`` pairs a source SedonaDB ``DataFrame`` with an expression over + its columns. Comparisons produce boolean ``Series`` usable as a filter mask + (``gdf[gdf["pop"] > 1000]``). Nothing is computed until ``to_pandas()`` / + display. + """ + + def __init__(self, df, expr, name): + self._df = df + self._expr = expr + self._name = name + + # -- element-wise comparisons -> boolean mask -------------------------- + def __gt__(self, other): + return Series(self._df, self._expr > _operand(other), self._name) + + def __ge__(self, other): + return Series(self._df, self._expr >= _operand(other), self._name) + + def __lt__(self, other): + return Series(self._df, self._expr < _operand(other), self._name) + + def __le__(self, other): + return Series(self._df, self._expr <= _operand(other), self._name) + + def __eq__(self, other): + return Series(self._df, self._expr == _operand(other), self._name) + + def __ne__(self, other): + return Series(self._df, self._expr != _operand(other), self._name) + + # -- boolean composition of masks -------------------------------------- + def __and__(self, other): + return Series(self._df, self._expr & _operand(other), self._name) + + def __or__(self, other): + return Series(self._df, self._expr | _operand(other), self._name) + + def __invert__(self): + return Series(self._df, ~self._expr, self._name) + + __hash__ = None + + # -- materialization --------------------------------------------------- + def to_pandas(self): + """Execute and return this column as a pandas (or GeoPandas) Series.""" + return self._df.select(self._expr.alias(self._name)).to_pandas()[self._name] + + def __repr__(self): + return repr(self.to_pandas()) + + +class GeoSeries(Series): + """A geometry column, in the shape of a ``geopandas.GeoSeries``. + + Element-wise geometry operations (``buffer``, ``centroid``, …) return a new + ``GeoSeries``; measures (``area``, ``length``) return a numeric ``Series``. + Each delegates to the corresponding ``ST_*`` function via SedonaDB's ``.geo`` + accessor. + """ + + def buffer(self, distance): + """Buffer each geometry by ``distance`` (``ST_Buffer``).""" + return GeoSeries(self._df, self._expr.geo.buffer(distance), self._name) Review Comment: I am not sure how well the IDE complete will work if you do this, but you could consider trying to assign `.__doc__` (or generating it) from the geo accessor documentation to avoid repeating it (incompletely) here. (I'm trying to get as much use as possible out of our markdown docs, and as much incentive as possible to make them great) ########## python/sedonadb-geopandas/pyproject.toml: ########## @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "sedonadb-geopandas" +version = "0.4.0" +description = "GeoPandas-compatible API on top of SedonaDB" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ Review Comment: Do you need `geopandas` as a hard dependency here? (Doing so increases the dependency footprint by quite a bit) Realistically this needs `pyarrow` (for literals) and `sedonadb-expr` (for the `.geo` accessor) ########## python/sedonadb-geopandas/python/sedonadb_geopandas/__init__.py: ########## @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-compatible API on top of SedonaDB. + +This package provides :class:`GeoDataFrame` / :class:`GeoSeries` wrappers whose +methods mirror GeoPandas but delegate to a lazy SedonaDB engine. It is a +compatibility layer, not a drop-in replacement: see the package README for the +intentional differences (laziness, no row index, immutability). +""" Review Comment: Worth adding EXPERIMENTAL to this (and all class docs) for merging the first version of this. ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_frame.py: ########## @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-style GeoDataFrame backed by a lazy SedonaDB frame.""" + +from sedonadb_geopandas._series import GeoSeries, Series + + +class GeoDataFrame: + """A lazy SedonaDB frame in the shape of a ``geopandas.GeoDataFrame``. + + Wraps a SedonaDB ``DataFrame`` and tracks the active geometry column. + Row selection, column access, and geometry operations mirror GeoPandas but + build a query rather than computing eagerly; call ``to_geopandas()`` to + materialize. + """ + + def __init__(self, df, geometry="geometry"): + self._df = df + self._geometry_name = geometry + + @property + def geometry(self): + """The active geometry column as a :class:`GeoSeries`.""" + return GeoSeries(self._df, self._df[self._geometry_name], self._geometry_name) + + @property + def crs(self): + """The CRS of the active geometry column (via a zero-row materialization).""" + return self._df.limit(0).to_pandas().crs + + @property + def columns(self): + """Column names, mirroring ``GeoDataFrame.columns``.""" + return list(self._df.schema.names) + + def __getitem__(self, key): + # Boolean mask -> row filter (gdf[gdf["pop"] > 1000]). + if isinstance(key, Series): + return GeoDataFrame(self._df.filter(key._expr), self._geometry_name) + + # Column subset -> GeoDataFrame (keeps the geometry column if selected). + if isinstance(key, list): + return GeoDataFrame(self._df.select(*key), self._geometry_name) + + # Single column -> (Geo)Series. + if isinstance(key, str): + expr = self._df[key] + if key == self._geometry_name: + return GeoSeries(self._df, expr, key) + return Series(self._df, expr, key) + + raise TypeError( + f"GeoDataFrame indices must be a column name, list of names, or " + f"boolean mask, not {type(key).__name__}" + ) + + def to_crs(self, crs): + """Reproject the geometry column to ``crs`` (``ST_Transform``).""" + from sedonadb.expr import lit + + transformed = self._df[self._geometry_name].geo.transform(lit(str(crs))) + new_df = self._df.mutate(**{self._geometry_name: transformed}) + return GeoDataFrame(new_df, self._geometry_name) + + def to_geopandas(self): + """Execute and return a ``geopandas.GeoDataFrame``.""" + return self._df.to_pandas() + + # Alias: results carry geometry, so this returns a GeoDataFrame too. + to_pandas = to_geopandas Review Comment: You probably want to handle the case where there's no geometry, also? Many pipelines pass through a stage where geometry is dropped and re-added (or dropped completely). ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_frame.py: ########## @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-style GeoDataFrame backed by a lazy SedonaDB frame.""" + +from sedonadb_geopandas._series import GeoSeries, Series + + +class GeoDataFrame: + """A lazy SedonaDB frame in the shape of a ``geopandas.GeoDataFrame``. + + Wraps a SedonaDB ``DataFrame`` and tracks the active geometry column. + Row selection, column access, and geometry operations mirror GeoPandas but + build a query rather than computing eagerly; call ``to_geopandas()`` to + materialize. + """ + + def __init__(self, df, geometry="geometry"): + self._df = df + self._geometry_name = geometry + + @property + def geometry(self): + """The active geometry column as a :class:`GeoSeries`.""" + return GeoSeries(self._df, self._df[self._geometry_name], self._geometry_name) + + @property + def crs(self): + """The CRS of the active geometry column (via a zero-row materialization).""" + return self._df.limit(0).to_pandas().crs Review Comment: I think you are better off with `self._df.schema.field(self._geometry_name).type.crs` here ########## python/sedonadb-geopandas/python/sedonadb_geopandas/_frame.py: ########## @@ -0,0 +1,90 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-style GeoDataFrame backed by a lazy SedonaDB frame.""" + +from sedonadb_geopandas._series import GeoSeries, Series + + +class GeoDataFrame: + """A lazy SedonaDB frame in the shape of a ``geopandas.GeoDataFrame``. + + Wraps a SedonaDB ``DataFrame`` and tracks the active geometry column. + Row selection, column access, and geometry operations mirror GeoPandas but + build a query rather than computing eagerly; call ``to_geopandas()`` to + materialize. + """ + + def __init__(self, df, geometry="geometry"): + self._df = df + self._geometry_name = geometry Review Comment: We have an internal heuristic for picking the geometry column name that we should use for the default here (see `.to_geopandas()`). For a non-default value, probably worth making sure it's actually a geometry type. I'm not sure about geography (probably would be fine to leave it since most of the functionality will still work) Should this use `default_context().create_data_frame(df)` here? That will surface an error for something not dataframe-able. ########## python/sedonadb-geopandas/python/sedonadb_geopandas/__init__.py: ########## @@ -0,0 +1,51 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""GeoPandas-compatible API on top of SedonaDB. + +This package provides :class:`GeoDataFrame` / :class:`GeoSeries` wrappers whose +methods mirror GeoPandas but delegate to a lazy SedonaDB engine. It is a +compatibility layer, not a drop-in replacement: see the package README for the +intentional differences (laziness, no row index, immutability). +""" + +from sedonadb_geopandas._context import default_context +from sedonadb_geopandas._frame import GeoDataFrame +from sedonadb_geopandas._series import GeoSeries, Series + +__all__ = ["GeoDataFrame", "GeoSeries", "Series", "from_geopandas"] + + +def from_geopandas(data, *, context=None, geometry=None): + """Load a ``geopandas.GeoDataFrame`` into a SedonaDB-backed ``GeoDataFrame``. + + Args: + data: A ``geopandas.GeoDataFrame`` (or any object accepted by Review Comment: The double quotes are rEST here but we're using markdown (also for other docs) ########## python/sedonadb-geopandas/tests/test_geopandas_compat.py: ########## @@ -0,0 +1,97 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import geopandas as gpd +import pytest + +import sedonadb_geopandas as sgpd +from sedonadb_geopandas import GeoDataFrame, GeoSeries, Series + + [email protected] +def cities(): + return gpd.GeoDataFrame( + {"name": ["A", "B", "C"], "pop": [100, 200, 300]}, + geometry=gpd.GeoSeries.from_wkt(["POINT (0 0)", "POINT (1 1)", "POINT (5 5)"]), + crs="EPSG:4326", + ) + + Review Comment: These tests are great for now, but you will probably get some mileage out of a testing framework like ```python def assert_geopandas_expr_equal(gdf, op): sgdf = sgpd.from_geopandas(gdf) gdf_result = op(gdf) sgdf_result = op(sgdf) geopandas.assert_geodataframe_equal(sgdf_result, gdf_result) ``` ...and throw a corpus of derived GeoPandas code at it. That will probably work better once you have a few more components in place. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
