spatial-polars 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ from .io import spatial_series_dtype, scan_spatial, read_spatial
2
+ from .spatialframe import SpatialFrame
3
+ from .spatialseries import SpatialSeries
4
+ from .spatialexpr import SpatialExpr
5
+
6
+ __all__ = [
7
+ "spatial_series_dtype",
8
+ "scan_spatial",
9
+ "read_spatial",
10
+ "SpatialFrame",
11
+ "SpatialSeries",
12
+ "SpatialExpr",
13
+ ]
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import polars as pl
7
+ from palettable.palette import Palette
8
+ from polars import selectors as cs
9
+ from matplotlib.colors import Colormap
10
+
11
+
12
+ def validate_col_exists_in_df(df: pl.DataFrame, col: Any):
13
+ if col not in df.columns:
14
+ # verify column exists in df
15
+ raise ValueError(
16
+ f"Input `{col=}` does not exist in frame. Please provide the name of a column from the dataframe ({df.columns})."
17
+ )
18
+
19
+
20
+ def validate_col_is_numeric(df: pl.DataFrame, col: Any):
21
+ if df[col].dtype.is_numeric() is False:
22
+ # verify cmap_col is a numeric dtype.
23
+ raise ValueError(
24
+ f"The '{col}' column is not a numeric column. Please provide a numeric column."
25
+ )
26
+
27
+
28
+ def validate_cmap_input(
29
+ df: pl.DataFrame,
30
+ cmap_col: Any,
31
+ cmap_type: Any,
32
+ cmap: Any,
33
+ alpha: Any,
34
+ normalize_cmap_col: Any,
35
+ ):
36
+ """
37
+ Validates the input combinations for color mapping options.
38
+ """
39
+ if cmap_col is None:
40
+ # no column provided is always good, use lonboard default color
41
+ return
42
+
43
+ validate_col_exists_in_df(df, cmap_col)
44
+
45
+ if cmap_type not in ["categorical", "continuous"]:
46
+ # verify cmap type is expected
47
+ raise ValueError(
48
+ f"`cmap_col` was provided, but {cmap_type=}. `c_map` must be `categorical` or `continuous` when `cmap_col` is provided."
49
+ )
50
+
51
+ if cmap_type == "continuous":
52
+ if isinstance(cmap, (Palette, Colormap)) is False:
53
+ # verify continuous cmaps get a cmap or appropriate type.
54
+ raise ValueError(
55
+ f"`{cmap_type=}` is `continuous`, but {cmap=}. Please provide a palettable.palette.Palette or matplotlib.colors.Colormap when using `cmap_type`=`continuous`."
56
+ )
57
+
58
+ validate_col_is_numeric(df, cmap_col)
59
+
60
+ if cmap_type == "categorical":
61
+ if isinstance(cmap, dict | None) is False:
62
+ # verify categorical cmaps get a cmap or appropriate type.
63
+ raise ValueError(
64
+ "`cmap_type` is `categorical`, but `cmap` is not a dictionary or None. Please provide a dictionary or None when using `cmap_type`=`categorical`."
65
+ )
66
+
67
+ if isinstance(cmap, dict):
68
+ # verify values in dataframe column all exist in cmap dictionary keys
69
+ try:
70
+ cmap_keys_s = pl.Series("cmap_keys", list(cmap.keys()))
71
+ except Exception:
72
+ raise ValueError(
73
+ "`cmap_type` is `categorical` and `cmap` is a dictionary, but there was an error converting the dictionary keys to a polars series. Please provide a dictionary with keys that are all the appropriate datatype for the data in the column."
74
+ )
75
+
76
+ col_vals_and_keys_df = (
77
+ df[cmap_col]
78
+ .unique()
79
+ .to_frame()
80
+ .join(
81
+ cmap_keys_s.to_frame().with_columns(
82
+ pl.col("cmap_keys").cast(df[cmap_col].dtype),
83
+ pl.lit(True).alias("exists_in_keys"),
84
+ ),
85
+ how="left",
86
+ left_on=cmap_col,
87
+ right_on="cmap_keys",
88
+ )
89
+ )
90
+ if col_vals_and_keys_df["exists_in_keys"].is_null().any():
91
+ missing_values = col_vals_and_keys_df.filter(
92
+ pl.col("exists_in_keys").is_null()
93
+ )[cmap_col].to_list()
94
+ raise ValueError(
95
+ f"`cmap` dictionary keys do not include all values in the data. Please provide colors for the following values {missing_values}."
96
+ )
97
+
98
+ # verify values have three or four items
99
+ try:
100
+ cmap_vals_df = pl.DataFrame(list(cmap.values()), orient="row")
101
+ except Exception:
102
+ raise ValueError(
103
+ "`cmap` dictionary values could not be converted to a dataframe with three or four integer columns for color codes. Please provide RGB[A] color codes for all values."
104
+ )
105
+
106
+ col_count = cmap_vals_df.shape[1]
107
+ if col_count not in [3, 4]:
108
+ raise ValueError(
109
+ "`cmap` dictionary values could not be converted to a dataframe with three or four integer columns for color codes. Please provide RGB[A] color codes for all values."
110
+ )
111
+
112
+ if col_count == 3:
113
+ cmap_vals_df.columns = ["R", "G", "B"]
114
+ elif col_count == 4:
115
+ cmap_vals_df.columns = ["R", "G", "B", "A"]
116
+
117
+ # verify all RGB[A] values are between 0-255
118
+ oob_rgba_df = cmap_vals_df.select(cmap_keys_s, pl.all()).filter(
119
+ pl.any_horizontal(
120
+ ~cs.by_name("R", "G", "B", "A", require_all=False).is_between(
121
+ 0, 255, closed="both"
122
+ )
123
+ )
124
+ )
125
+
126
+ if len(oob_rgba_df) > 0:
127
+ raise ValueError(
128
+ f"`cmap` dictionary values for color codes were not all between 0 and 255. Please provide number between 0-255 for all RGB[A] color codes. Bad values were: {oob_rgba_df}"
129
+ )
130
+
131
+
132
+ def validate_width_and_radius_input(df: pl.DataFrame, width: Any):
133
+ if width is None:
134
+ # None is always good
135
+ return
136
+
137
+ elif isinstance(width, float | int):
138
+ # numbers are always good
139
+ return
140
+ elif isinstance(width, np.ndarray):
141
+ if np.issubdtype(width.dtype, np.number) is False:
142
+ # validate numpy array dtype is numeric
143
+ raise ValueError(
144
+ "Input `width` is a numpy array that is not numeric. If using a numpy array for the width, the datatype of the array must be numeric."
145
+ )
146
+
147
+ width_len = width.shape[0]
148
+ df_len = len(df)
149
+ if width_len != df_len:
150
+ # validate numpy array has the same number of items as the df
151
+ raise ValueError(
152
+ f"Input `width` is a numpy array with a different number of elements than the frame (array: {width_len:,} frame: {df_len:,}). If using a numpy array for the width, the the array must have the same number of elements as the frame has rows."
153
+ )
154
+
155
+ elif isinstance(width, str):
156
+ validate_col_exists_in_df(df, width)
157
+ validate_col_is_numeric(df, width)
158
+
159
+ else:
160
+ raise Exception(
161
+ f"Input `width` '{width}' is a '{type(width)}', which is not allowed. Please provide: None, a number, a column name, or a numpy array for the width."
162
+ )
spatial_polars/io.py ADDED
@@ -0,0 +1,459 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from typing import Iterator, Optional, TYPE_CHECKING
4
+
5
+ import polars as pl
6
+ import pyogrio
7
+ import pyproj
8
+ import shapely
9
+ import pyarrow.parquet as _pq
10
+ import json
11
+
12
+ from polars.io.plugins import register_io_source
13
+
14
+ if TYPE_CHECKING:
15
+ from io import BytesIO
16
+
17
+ __all__ = [
18
+ "spatial_series_dtype",
19
+ "scan_spatial",
20
+ "read_spatial",
21
+ ]
22
+
23
+ spatial_series_dtype = pl.Struct({"wkb_geometry": pl.Binary, "crs": pl.Categorical})
24
+
25
+ PYOGRIO_POLARS_DTYPES = {
26
+ "int8": pl.Int8,
27
+ "int16": pl.Int16,
28
+ "int32": pl.Int32,
29
+ "int": pl.Int64,
30
+ "int64": pl.Int64,
31
+ "uint8": pl.UInt8,
32
+ "uint16": pl.UInt16,
33
+ "uint32": pl.UInt32,
34
+ "uint": pl.UInt64,
35
+ "uint64": pl.UInt64,
36
+ "bool": pl.Boolean,
37
+ "float32": pl.Float32,
38
+ "float": pl.Float64,
39
+ "float64": pl.Float64,
40
+ "datetime64[D]": pl.Date,
41
+ "datetime64[us]": pl.Datetime("us"),
42
+ "datetime64[ns]": pl.Datetime("ns"),
43
+ "datetime64[ms]": pl.Datetime("ms"),
44
+ "datetime64": pl.Datetime("ms"), # ms??
45
+ "object": pl.String,
46
+ }
47
+
48
+
49
+ def scan_spatial(
50
+ path_or_buffer: str | Path | BytesIO,
51
+ layer: Optional[str | int] = None,
52
+ encoding: Optional[str] = None,
53
+ bbox: Optional[tuple[float, float, float, float]] = None,
54
+ mask: Optional[shapely.Polygon] = None,
55
+ ) -> pl.LazyFrame:
56
+ r"""
57
+ Scans a data source [supported by pyogrio](https://pyogrio.readthedocs.io/en/stable/supported_formats.html) or a geoparquet file to produce a polars LazyFrame.
58
+
59
+ Note
60
+ ----
61
+ Although geoparquet is supported, this implementation, in its current state, leaves a lot to be desired.
62
+
63
+ Parameters
64
+ ----------
65
+
66
+ path_or_buffer
67
+ A dataset path or URI, raw buffer, or file-like object with a read method.
68
+
69
+ layer
70
+ If an integer is provided, it corresponds to the index of the layer with the
71
+ data source. If a string is provided, it must match the name of the layer in
72
+ the data source. Defaults to first layer in data source.
73
+
74
+ encoding
75
+ If present, will be used as the encoding for reading string values from the
76
+ data source. By default will automatically try to detect the native encoding
77
+ and decode to UTF-8.
78
+
79
+ bbox
80
+ If present, will be used to filter records whose geometry intersects this
81
+ box. This must be in the same CRS as the dataset. If GEOS is present and
82
+ used by GDAL, only geometries that intersect this bbox will be returned;
83
+ if GEOS is not available or not used by GDAL, all geometries with bounding
84
+ boxes that intersect this bbox will be returned. Cannot be combined with mask
85
+ keyword. Tuple should be in the format of (xmin, ymin, xmax, ymax).
86
+
87
+ mask
88
+ If present, will be used to filter records whose geometry intersects this
89
+ geometry. This must be in the same CRS as the dataset. If GEOS is present
90
+ and used by GDAL, only geometries that intersect this geometry will be
91
+ returned; if GEOS is not available or not used by GDAL, all geometries with
92
+ bounding boxes that intersect the bounding box of this geometry will be
93
+ returned. Requires Shapely >= 2.0. Cannot be combined with bbox keyword.
94
+
95
+ Examples
96
+ --------
97
+ **Scanning a layer from a geopackage:**
98
+
99
+ >>> my_geopackage = r"c:\data\hiking_club.gpkg"
100
+ >>> lf = scan_spatial(my_geopackage, layer="hike")
101
+ >>> lf
102
+ naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
103
+ PYTHON SCAN []
104
+ PROJECT */4 COLUMNS
105
+
106
+ **Scanning a shapefile:**
107
+
108
+ >>> from spatial_polars import scan_spatial
109
+ >>> my_shapefile = r"c:\data\roads.shp"
110
+ >>> lf = scan_spatial(my_shapefile)
111
+ >>> lf
112
+ naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
113
+ PYTHON SCAN []
114
+ PROJECT */11 COLUMNS
115
+
116
+ **Scanning a shapefile from within a zipped directory:**
117
+
118
+ >>> zipped_shapefiles = r"C:\data\illinois-latest-free.shp.zip"
119
+ >>> lf = scan_spatial(zipped_shapefiles, layer="gis_osm_roads_free_1")
120
+ >>> lf
121
+ naive plan: (run LazyFrame.explain(optimized=True) to see the optimized plan)
122
+ PYTHON SCAN []
123
+ PROJECT */11 COLUMNS
124
+ """
125
+ if isinstance(path_or_buffer, (str, Path)) and str(path_or_buffer).endswith(
126
+ ".parquet"
127
+ ):
128
+ # TODO look into libgdal-arrow-parquet from conda forge
129
+ # https://pyogrio.readthedocs.io/en/latest/install.html#conda-forge
130
+ schema = pl.scan_parquet(path_or_buffer).collect_schema()
131
+ if schema.get("geometry") is not None:
132
+ schema["geometry"] = spatial_series_dtype
133
+ if bbox is not None:
134
+ mask = shapely.Polygon(shapely.box(*bbox))
135
+
136
+ def source_generator(
137
+ with_columns: list[str] | None,
138
+ predicate: pl.Expr | None,
139
+ n_rows: int | None,
140
+ batch_size: int | None,
141
+ ) -> Iterator[pl.DataFrame]:
142
+ """
143
+ Generator function that creates the source.
144
+ This function will be registered as IO source.
145
+ """
146
+ if mask is not None:
147
+ if "geometry" not in with_columns:
148
+ with_columns.append("geometry")
149
+
150
+ tab = _pq.read_table(path_or_buffer)
151
+ tab_metadata = tab.schema.metadata if tab.schema.metadata else {}
152
+ if b"geo" in tab_metadata:
153
+ geo_meta = json.loads(tab_metadata[b"geo"])
154
+ else:
155
+ geo_meta = {}
156
+ geom_col = geo_meta["primary_column"]
157
+ crs_wkt = pyproj.CRS(geo_meta["columns"][geom_col]["crs"]).to_wkt()
158
+
159
+ if batch_size is None:
160
+ batch_size = 10000
161
+
162
+ if with_columns is None:
163
+ read_geometry = True
164
+ elif "geometry" in with_columns:
165
+ read_geometry = True
166
+ else:
167
+ read_geometry = False
168
+
169
+ lf = pl.scan_parquet(path_or_buffer)
170
+
171
+ if with_columns is not None:
172
+ lf = lf.select(with_columns)
173
+
174
+ if predicate is not None:
175
+ lf = lf.filter(predicate)
176
+
177
+ previous_max = 0
178
+ while n_rows is None or n_rows > 0:
179
+ batch = lf.slice(previous_max, previous_max + batch_size).collect()
180
+ if batch.height is None or batch.height == 0:
181
+ break
182
+ if n_rows is not None and n_rows <= 0:
183
+ break
184
+
185
+ if read_geometry:
186
+ # get the geometries from the batch
187
+ geometries = batch[0:n_rows][geom_col]
188
+ shapely_goms = shapely.from_wkb(geometries)
189
+ geometries = shapely.to_wkb(shapely_goms)
190
+
191
+ # create the dataframe with the non geometry columns
192
+ # then add struct column with the WKB geometries/CRS
193
+ df = pl.DataFrame(batch[0:n_rows].drop(geom_col)).with_columns(
194
+ pl.struct(
195
+ pl.Series("wkb_geometry", geometries, dtype=pl.Binary),
196
+ pl.lit(crs_wkt, dtype=pl.Categorical).alias("crs"),
197
+ ).alias("geometry")
198
+ )
199
+ else:
200
+ df = pl.DataFrame(batch[0:n_rows])
201
+ previous_max += df.height
202
+
203
+ if n_rows is not None:
204
+ n_rows -= df.height
205
+
206
+ if predicate is not None:
207
+ df = df.filter(predicate)
208
+
209
+ if mask is not None:
210
+ df = df.filter(pl.col("geometry").spatial.intersects(mask))
211
+ if mask is not None:
212
+ if "geometry" not in with_columns:
213
+ df = df.drop("geometry")
214
+
215
+ yield df
216
+
217
+ else:
218
+ # not geoparquet
219
+ layer_info = pyogrio.read_info(path_or_buffer, layer=layer, encoding=encoding)
220
+ schema = dict(
221
+ zip(
222
+ layer_info["fields"],
223
+ [PYOGRIO_POLARS_DTYPES[dt] for dt in layer_info["dtypes"]],
224
+ )
225
+ )
226
+ if layer_info.get("geometry_type"):
227
+ schema["geometry"] = spatial_series_dtype
228
+
229
+ def source_generator(
230
+ with_columns: list[str] | None,
231
+ predicate: pl.Expr | None,
232
+ n_rows: int | None,
233
+ batch_size: int | None,
234
+ ) -> Iterator[pl.DataFrame]:
235
+ """
236
+ Generator function that creates the source.
237
+ This function will be registered as IO source.
238
+ """
239
+
240
+ if batch_size is None:
241
+ batch_size = 100
242
+
243
+ if with_columns is None:
244
+ read_geometry = True
245
+ elif "geometry" in with_columns:
246
+ read_geometry = True
247
+ with_columns.remove("geometry")
248
+ else:
249
+ read_geometry = False
250
+
251
+ with pyogrio.open_arrow(
252
+ path_or_buffer,
253
+ layer=layer,
254
+ encoding=encoding,
255
+ columns=with_columns,
256
+ read_geometry=read_geometry,
257
+ force_2d=False,
258
+ bbox=bbox,
259
+ mask=mask,
260
+ batch_size=batch_size,
261
+ use_pyarrow=True,
262
+ ) as source:
263
+ meta, reader = source
264
+
265
+ # extract the crs from the metadata
266
+ crs_wkt = pyproj.CRS(meta["crs"]).to_wkt()
267
+
268
+ geom_col = meta["geometry_name"] or "wkb_geometry"
269
+
270
+ while n_rows is None or n_rows > 0:
271
+ for batch in reader:
272
+ if n_rows is not None and n_rows <= 0:
273
+ break
274
+
275
+ if read_geometry:
276
+ # get the geometries from the batch
277
+ geometries = batch[geom_col][0:n_rows]
278
+ shapely_goms = shapely.from_wkb(geometries)
279
+ geometries = shapely.to_wkb(shapely_goms)
280
+
281
+ print(batch)
282
+
283
+ # create the dataframe with the non geometry columns
284
+ # then add struct column with the WKB geometries/CRS
285
+ df = pl.DataFrame(
286
+ batch[0:n_rows].drop_columns(geom_col)
287
+ ).with_columns(
288
+ pl.struct(
289
+ pl.Series(
290
+ "wkb_geometry", geometries, dtype=pl.Binary
291
+ ),
292
+ pl.lit(crs_wkt, dtype=pl.Categorical).alias("crs"),
293
+ ).alias("geometry")
294
+ )
295
+ else:
296
+ df = pl.DataFrame(batch[0:n_rows])
297
+
298
+ if n_rows is not None:
299
+ n_rows -= df.height
300
+
301
+ if predicate is not None:
302
+ df = df.filter(predicate)
303
+
304
+ yield df
305
+ if n_rows is None or n_rows <= 0:
306
+ break
307
+
308
+ return register_io_source(io_source=source_generator, schema=schema)
309
+
310
+
311
+ def read_spatial(
312
+ path_or_buffer: str | Path | BytesIO,
313
+ layer: Optional[str | int] = None,
314
+ encoding: Optional[str] = None,
315
+ bbox: Optional[tuple[float, float, float, float]] = None,
316
+ mask: Optional[shapely.Polygon] = None,
317
+ ) -> pl.DataFrame:
318
+ r"""
319
+ Reads a spatial data source [supported by pyogrio](https://pyogrio.readthedocs.io/en/stable/supported_formats.html) to produce a polars DataFrame.
320
+
321
+ Note
322
+ ----
323
+ Although geoparquet is supported, this implementation, in its current state, leaves a lot to be desired.
324
+
325
+ Parameters
326
+ ----------
327
+
328
+ path_or_buffer
329
+ A dataset path or URI, raw buffer, or file-like object with a read method.
330
+
331
+ layer
332
+ If an integer is provided, it corresponds to the index of the layer with the
333
+ data source. If a string is provided, it must match the name of the layer in
334
+ the data source. Defaults to first layer in data source.
335
+
336
+ encoding
337
+ If present, will be used as the encoding for reading string values from the
338
+ data source. By default will automatically try to detect the native encoding
339
+ and decode to UTF-8.
340
+
341
+ bbox
342
+ If present, will be used to filter records whose geometry intersects this
343
+ box. This must be in the same CRS as the dataset. If GEOS is present and
344
+ used by GDAL, only geometries that intersect this bbox will be returned;
345
+ if GEOS is not available or not used by GDAL, all geometries with bounding
346
+ boxes that intersect this bbox will be returned. Cannot be combined with mask
347
+ keyword. Tuple should be in the format of (xmin, ymin, xmax, ymax).
348
+
349
+ mask
350
+ If present, will be used to filter records whose geometry intersects this
351
+ geometry. This must be in the same CRS as the dataset. If GEOS is present
352
+ and used by GDAL, only geometries that intersect this geometry will be
353
+ returned; if GEOS is not available or not used by GDAL, all geometries with
354
+ bounding boxes that intersect the bounding box of this geometry will be
355
+ returned. Requires Shapely >= 2.0. Cannot be combined with bbox keyword.
356
+
357
+ Examples
358
+ --------
359
+ **Scanning a layer from a geopackage:**
360
+
361
+ >>> from spatial_polars import read_spatial
362
+ >>> my_geopackage = r"c:\data\hiking_club.gpkg"
363
+ >>> df = read_spatial(my_geopackage, layer="hike")
364
+ >>> df
365
+ shape: (31, 4)
366
+ ┌─────────────────────────────────┬────────────┬──────────┬─────────────────────────────────┐
367
+ │ LOCATION ┆ DATE ┆ DISTANCE ┆ geometry │
368
+ │ --- ┆ --- ┆ --- ┆ --- │
369
+ │ str ┆ date ┆ f64 ┆ struct[2] │
370
+ ╞═════════════════════════════════╪════════════╪══════════╪═════════════════════════════════╡
371
+ │ Watershed Nature Center ┆ 2023-01-14 ┆ 1.25 ┆ {b"\x01\x02\x00\x00\x00\xd8\x0… │
372
+ │ Ellis Island ┆ 2023-03-11 ┆ 2.25 ┆ {b"\x01\x02\x00\x00\x00\x82\x0… │
373
+ │ Cahokia Mounds State Historic … ┆ 2023-02-04 ┆ 1.75 ┆ {b"\x01\x02\x00\x00\x00\xb1\x0… │
374
+ │ Willoughby Heritage Farm ┆ 2022-12-03 ┆ 0.75 ┆ {b"\x01\x02\x00\x00\x00\xef\x0… │
375
+ │ Pere Marquette State Park ┆ 2022-10-15 ┆ 1.0 ┆ {b"\x01\x02\x00\x00\x002\x02\x… │
376
+ │ … ┆ … ┆ … ┆ … │
377
+ │ Haunted Glen Carbon ┆ 2024-10-19 ┆ 1.75 ┆ {b"\x01\x02\x00\x00\x00\x82\x0… │
378
+ │ Watershed Nature Center ┆ 2024-10-08 ┆ 1.0 ┆ {b"\x01\x02\x00\x00\x000\x00\x… │
379
+ │ Beaver Dam State Park ┆ 2024-10-26 ┆ 2.0 ┆ {b"\x01\x02\x00\x00\x00\xc4\x0… │
380
+ │ Willoughby Heritage Farm ┆ 2024-12-07 ┆ 1.5 ┆ {b"\x01\x02\x00\x00\x00>\x02\x… │
381
+ │ Cahokia Mounds State Historic … ┆ 2025-03-08 ┆ 1.75 ┆ {b"\x01\x02\x00\x00\x00\xeb\x0… │
382
+ └─────────────────────────────────┴────────────┴──────────┴─────────────────────────────────┘
383
+
384
+ **Scanning a shapefile:**
385
+
386
+ >>> my_shapefile = r"c:\data\roads.shp"
387
+ >>> df = read_spatial(my_shapefile)
388
+ >>> df
389
+ shape: (1_662_837, 11)
390
+ ┌────────────┬──────┬─────────────┬─────────────┬───┬───────┬────────┬────────┬────────────────────┐
391
+ │ osm_id ┆ code ┆ fclass ┆ name ┆ … ┆ layer ┆ bridge ┆ tunnel ┆ geometry │
392
+ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
393
+ │ str ┆ i32 ┆ str ┆ str ┆ ┆ i64 ┆ str ┆ str ┆ struct[2] │
394
+ ╞════════════╪══════╪═════════════╪═════════════╪═══╪═══════╪════════╪════════╪════════════════════╡
395
+ │ 4265057 ┆ 5114 ┆ secondary ┆ 55th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
396
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x03\x0… │
397
+ │ 4265058 ┆ 5114 ┆ secondary ┆ Fairview ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
398
+ │ ┆ ┆ ┆ Avenue ┆ ┆ ┆ ┆ ┆ 0\x00\x0e\x0… │
399
+ │ 4267607 ┆ 5114 ┆ secondary ┆ 31st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
400
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
401
+ │ 4271616 ┆ 5115 ┆ tertiary ┆ 59th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
402
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x15\x0… │
403
+ │ 4275365 ┆ 5122 ┆ residential ┆ 61st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
404
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00"\x00\x… │
405
+ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
406
+ │ 1370383592 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
407
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
408
+ │ 1370383593 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
409
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x07\x0… │
410
+ │ 1370383594 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
411
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x1c\x0… │
412
+ │ 1370383595 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
413
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x0b\x0… │
414
+ │ 1370398885 ┆ 5141 ┆ service ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
415
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
416
+ └────────────┴──────┴─────────────┴─────────────┴───┴───────┴────────┴────────┴────────────────────┘
417
+
418
+ **Scanning a shapefile from within a zipped directory:**
419
+
420
+ >>> zipped_shapefiles = r"C:\data\illinois-latest-free.shp.zip"
421
+ >>> df = read_spatial(zipped_shapefiles, layer="gis_osm_roads_free_1")
422
+ >>> df
423
+ shape: (1_662_837, 11)
424
+ ┌────────────┬──────┬─────────────┬─────────────┬───┬───────┬────────┬────────┬────────────────────┐
425
+ │ osm_id ┆ code ┆ fclass ┆ name ┆ … ┆ layer ┆ bridge ┆ tunnel ┆ geometry │
426
+ │ --- ┆ --- ┆ --- ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
427
+ │ str ┆ i32 ┆ str ┆ str ┆ ┆ i64 ┆ str ┆ str ┆ struct[2] │
428
+ ╞════════════╪══════╪═════════════╪═════════════╪═══╪═══════╪════════╪════════╪════════════════════╡
429
+ │ 4265057 ┆ 5114 ┆ secondary ┆ 55th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
430
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x03\x0… │
431
+ │ 4265058 ┆ 5114 ┆ secondary ┆ Fairview ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
432
+ │ ┆ ┆ ┆ Avenue ┆ ┆ ┆ ┆ ┆ 0\x00\x0e\x0… │
433
+ │ 4267607 ┆ 5114 ┆ secondary ┆ 31st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
434
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
435
+ │ 4271616 ┆ 5115 ┆ tertiary ┆ 59th Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
436
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x15\x0… │
437
+ │ 4275365 ┆ 5122 ┆ residential ┆ 61st Street ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
438
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00"\x00\x… │
439
+ │ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … ┆ … │
440
+ │ 1370383592 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
441
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
442
+ │ 1370383593 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
443
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x07\x0… │
444
+ │ 1370383594 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
445
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x1c\x0… │
446
+ │ 1370383595 ┆ 5153 ┆ footway ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
447
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x0b\x0… │
448
+ │ 1370398885 ┆ 5141 ┆ service ┆ null ┆ … ┆ 0 ┆ F ┆ F ┆ {b"\x01\x02\x00\x0 │
449
+ │ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ 0\x00\x02\x0… │
450
+ └────────────┴──────┴─────────────┴─────────────┴───┴───────┴────────┴────────┴────────────────────┘
451
+
452
+ """
453
+ return scan_spatial(
454
+ path_or_buffer=path_or_buffer,
455
+ layer=layer,
456
+ encoding=encoding,
457
+ bbox=bbox,
458
+ mask=mask,
459
+ ).collect(engine="streaming")