vecorel-cli 0.2.20__tar.gz → 0.3.1__tar.gz
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.
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/PKG-INFO +3 -1
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/pyproject.toml +14 -1
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_convert.py +243 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_convert_duckdb.py +365 -13
- vecorel_cli-0.3.1/tests/test_deflate64.py +68 -0
- vecorel_cli-0.3.1/tests/test_download.py +194 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_improve.py +25 -1
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/base.py +222 -74
- vecorel_cli-0.3.1/vecorel_cli/conversion/deflate64.py +54 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/duckdb.py +197 -35
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/improve.py +9 -1
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/util.py +51 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/PKG-INFO +3 -1
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/SOURCES.txt +2 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/requires.txt +2 -0
- vecorel_cli-0.2.20/tests/test_download.py +0 -59
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/LICENSE +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/README.md +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/setup.cfg +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_converters.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_create_geojson.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_create_geoparquet.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_create_stac.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_describe.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_encoding_auto.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_encoding_geojson.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_encoding_geoparquet.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_hilbert.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_jsonschema.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_merge.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_ops.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_rename_extension.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_validate.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_validate_schema.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/basecommand.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/logger.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/options.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/path_url.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/setup.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/util.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/const.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/admin.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/flatdict.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/convert.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/converters.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_geojson.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_geoparquet.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_jsonschema.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_stac.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/datasets/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/datasets/template.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/describe.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/auto.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/base.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/geojson.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/geoparquet.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/jsonschema/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/jsonschema/template.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/merge.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/parquet/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/parquet/geopandas.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/parquet/types.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/registry.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/rename_extension.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validate.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validate_schema.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/base.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/data.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/geojson.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/geoparquet.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/__init__.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/collection.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/extensions.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/hilbert.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/ops.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/schemas.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/typing.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/version.py +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/dependency_links.txt +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/entry_points.txt +0 -0
- {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vecorel-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI tools such as validation and file format conversion for vecorel.
|
|
5
5
|
Author: Matthias Mohr
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -24,6 +24,8 @@ Requires-Dist: shapely<3.0,>=2.1
|
|
|
24
24
|
Requires-Dist: numpy<3.0,>=2.0
|
|
25
25
|
Requires-Dist: pyarrow<24.0,>=21.0
|
|
26
26
|
Requires-Dist: py7zr<2.0,>=1.0
|
|
27
|
+
Requires-Dist: inflate64<2.0,>=1.0
|
|
28
|
+
Requires-Dist: multivolumefile<1.0,>=0.2.3
|
|
27
29
|
Requires-Dist: fsspec>=2025.7.0
|
|
28
30
|
Requires-Dist: jsonschema[format]<5.0,>=4.20
|
|
29
31
|
Requires-Dist: aiohttp<4.0,>=3.13.5
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vecorel-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "CLI tools such as validation and file format conversion for vecorel."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -30,6 +30,8 @@ dependencies = [
|
|
|
30
30
|
"numpy>=2.0,<3.0",
|
|
31
31
|
"pyarrow>=21.0,<24.0",
|
|
32
32
|
"py7zr>=1.0,<2.0",
|
|
33
|
+
"inflate64>=1.0,<2.0",
|
|
34
|
+
"multivolumefile>=0.2.3,<1.0",
|
|
33
35
|
"fsspec>=2025.7.0",
|
|
34
36
|
"jsonschema[format]>=4.20,<5.0",
|
|
35
37
|
"aiohttp>=3.13.5,<4.0",
|
|
@@ -71,6 +73,8 @@ shapely = ">=2.1,<3.0"
|
|
|
71
73
|
numpy = ">=2.0,<3.0"
|
|
72
74
|
pyarrow = ">=21.0,<24.0"
|
|
73
75
|
py7zr = ">=1.0,<2.0"
|
|
76
|
+
inflate64 = ">=1.0,<2.0"
|
|
77
|
+
multivolumefile = ">=0.2.3,<1.0"
|
|
74
78
|
unrar = ">=0.4"
|
|
75
79
|
|
|
76
80
|
[tool.pixi.pypi-dependencies]
|
|
@@ -87,6 +91,12 @@ GitPython = ">=3.0.0,<4.0.0"
|
|
|
87
91
|
[tool.pixi.feature.dev.pypi-dependencies]
|
|
88
92
|
build = ">=1.0.0,<2.0.0"
|
|
89
93
|
|
|
94
|
+
[tool.pixi.feature.py311.dependencies]
|
|
95
|
+
python = "3.11.*"
|
|
96
|
+
|
|
97
|
+
[tool.pixi.feature.py314.dependencies]
|
|
98
|
+
python = "3.14.*"
|
|
99
|
+
|
|
90
100
|
[tool.pixi.feature.s3.dependencies]
|
|
91
101
|
s3fs = ">=2025.7.0"
|
|
92
102
|
|
|
@@ -100,6 +110,9 @@ s3 = {features = ["s3"], solve-group = "default"}
|
|
|
100
110
|
gcs = {features = ["gcs"], solve-group = "default"}
|
|
101
111
|
cloud = {features = ["s3", "gcs"], solve-group = "default"}
|
|
102
112
|
all = {features = ["dev", "s3", "gcs"], solve-group = "default"}
|
|
113
|
+
# CI tests the oldest and newest supported Python; pixi, not setup-python, decides the version
|
|
114
|
+
dev-py311 = {features = ["dev", "py311"], solve-group = "py311"}
|
|
115
|
+
dev-py314 = {features = ["dev", "py314"], solve-group = "py314"}
|
|
103
116
|
|
|
104
117
|
[tool.pixi.tasks]
|
|
105
118
|
# Development tasks
|
|
@@ -127,6 +127,249 @@ def test_hilbert_sort_falls_back_without_area_of_use():
|
|
|
127
127
|
assert len(out) == 3
|
|
128
128
|
|
|
129
129
|
|
|
130
|
+
CONFIG = {
|
|
131
|
+
"id": "test",
|
|
132
|
+
"short_name": "Test",
|
|
133
|
+
"title": "Test dataset",
|
|
134
|
+
"description": "Test dataset",
|
|
135
|
+
"license": "CC0-1.0",
|
|
136
|
+
"columns": {
|
|
137
|
+
"geometry": "geometry",
|
|
138
|
+
"id": "id",
|
|
139
|
+
"name": "name",
|
|
140
|
+
},
|
|
141
|
+
"missing_schemas": {
|
|
142
|
+
"properties": {
|
|
143
|
+
"name": {"type": "string"},
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_rows_are_numbered_when_no_column_is_mapped_to_id(tmp_folder):
|
|
150
|
+
"""A converter that maps nothing to `id` gets the row number as id, counted
|
|
151
|
+
over all source files (vecorel/cli#47). A source column named `id` that is
|
|
152
|
+
mapped to another target must not be overwritten by the numbering."""
|
|
153
|
+
import shapely
|
|
154
|
+
|
|
155
|
+
files = {}
|
|
156
|
+
for index in range(2):
|
|
157
|
+
gdf = gpd.GeoDataFrame(
|
|
158
|
+
{
|
|
159
|
+
"id": [f"legacy-{index}-1", f"legacy-{index}-2"],
|
|
160
|
+
"name": ["a", "b"],
|
|
161
|
+
"geometry": [
|
|
162
|
+
shapely.box(index * 4, 0, index * 4 + 1, 1),
|
|
163
|
+
shapely.box(index * 4 + 2, 0, index * 4 + 3, 1),
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
crs="EPSG:4326",
|
|
167
|
+
)
|
|
168
|
+
path = tmp_folder / f"src_{index}.parquet"
|
|
169
|
+
gdf.to_parquet(path)
|
|
170
|
+
files[str(path)] = path.name
|
|
171
|
+
|
|
172
|
+
Converter = type(
|
|
173
|
+
"IndexConverter",
|
|
174
|
+
(BaseConverter,),
|
|
175
|
+
{
|
|
176
|
+
**CONFIG,
|
|
177
|
+
"columns": {"geometry": "geometry", "name": "name", "id": "legacy"},
|
|
178
|
+
"missing_schemas": {
|
|
179
|
+
"properties": {"name": {"type": "string"}, "legacy": {"type": "string"}}
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
dest = tmp_folder / "converted.parquet"
|
|
184
|
+
Converter().convert(dest, input_files=files)
|
|
185
|
+
|
|
186
|
+
result = gpd.read_parquet(dest).sort_values("id")
|
|
187
|
+
assert result["id"].tolist() == ["0", "1", "2", "3"]
|
|
188
|
+
assert result["legacy"].tolist() == ["legacy-0-1", "legacy-0-2", "legacy-1-1", "legacy-1-2"]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_id_is_composed_from_id_columns(tmp_folder):
|
|
192
|
+
"""`id_columns` joins the named columns into `id` after the column migrations
|
|
193
|
+
ran; integer-typed float columns must not render as '4.0'. It also wins over
|
|
194
|
+
a mapping to `id` (often inherited), which would otherwise produce two
|
|
195
|
+
columns named `id` after the rename — without overwriting a source column
|
|
196
|
+
named `id` that is mapped to another target."""
|
|
197
|
+
import shapely
|
|
198
|
+
|
|
199
|
+
gdf = gpd.GeoDataFrame(
|
|
200
|
+
{
|
|
201
|
+
"id": ["source-1", "source-2"],
|
|
202
|
+
"region": ["A", "B"],
|
|
203
|
+
"block": [7.0, 40.0], # float-typed integers, as nullable int columns often read
|
|
204
|
+
"name": ["a", "b"],
|
|
205
|
+
"geometry": [shapely.box(0, 0, 1, 1), shapely.box(2, 0, 3, 1)],
|
|
206
|
+
},
|
|
207
|
+
crs="EPSG:4326",
|
|
208
|
+
)
|
|
209
|
+
src = tmp_folder / "source.parquet"
|
|
210
|
+
gdf.to_parquet(src)
|
|
211
|
+
|
|
212
|
+
Converter = type(
|
|
213
|
+
"ComposedConverter",
|
|
214
|
+
(BaseConverter,),
|
|
215
|
+
{
|
|
216
|
+
**CONFIG,
|
|
217
|
+
# the mapping to `id` must lose against id_columns, but `source_id`
|
|
218
|
+
# must still receive the source's own `id` values
|
|
219
|
+
"columns": {**CONFIG["columns"], "id": ("id", "source_id")},
|
|
220
|
+
"missing_schemas": {
|
|
221
|
+
"properties": {"name": {"type": "string"}, "source_id": {"type": "string"}}
|
|
222
|
+
},
|
|
223
|
+
"id_columns": ("campaign", "region", "block"),
|
|
224
|
+
"id_separator": ":",
|
|
225
|
+
"column_additions": {"campaign": 2026.0}, # must format as 2026, not 2026.0
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
dest = tmp_folder / "converted.parquet"
|
|
229
|
+
Converter().convert(dest, input_files={str(src): "source.parquet"})
|
|
230
|
+
|
|
231
|
+
result = gpd.read_parquet(dest).sort_values("id")
|
|
232
|
+
assert result["id"].tolist() == ["2026:A:7", "2026:B:40"]
|
|
233
|
+
assert result["source_id"].tolist() == ["source-1", "source-2"]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_preserve_source_id_avoids_taken_names():
|
|
237
|
+
"""The name the source id moves to must be free in the frame and the mapping."""
|
|
238
|
+
import shapely
|
|
239
|
+
|
|
240
|
+
gdf = gpd.GeoDataFrame(
|
|
241
|
+
{"id": ["a"], "__source_id": ["b"], "geometry": [shapely.box(0, 0, 1, 1)]},
|
|
242
|
+
crs="EPSG:4326",
|
|
243
|
+
)
|
|
244
|
+
columns = {"id": "legacy", "__source_id": "other", "geometry": "geometry"}
|
|
245
|
+
|
|
246
|
+
gdf, columns = BaseConverter()._preserve_source_id(gdf, columns)
|
|
247
|
+
|
|
248
|
+
assert not gdf.columns.duplicated().any()
|
|
249
|
+
assert columns["__source_id"] == "other"
|
|
250
|
+
assert columns["__source_id_"] == "legacy"
|
|
251
|
+
assert gdf["__source_id_"].tolist() == ["a"]
|
|
252
|
+
assert gdf["__source_id"].tolist() == ["b"]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_suffix_duplicate_ids_avoids_existing_ids_and_compares_as_strings():
|
|
256
|
+
"""A numbered id must not collide with an id the source already carries
|
|
257
|
+
(x, x, x~1), and repeats hidden by mixed types (1 vs "1") must be caught:
|
|
258
|
+
the writer merges both into the string "1"."""
|
|
259
|
+
import shapely
|
|
260
|
+
|
|
261
|
+
from vecorel_cli.vecorel.util import suffix_duplicate_ids
|
|
262
|
+
|
|
263
|
+
box = shapely.box(0, 0, 1, 1)
|
|
264
|
+
gdf = gpd.GeoDataFrame({"id": ["x", "x", "x~1"], "geometry": [box] * 3}, crs="EPSG:4326")
|
|
265
|
+
gdf, count = suffix_duplicate_ids(gdf)
|
|
266
|
+
assert count == 2
|
|
267
|
+
assert gdf["id"].is_unique, gdf["id"].tolist()
|
|
268
|
+
|
|
269
|
+
gdf = gpd.GeoDataFrame({"id": [1, "1"], "geometry": [box] * 2}, crs="EPSG:4326")
|
|
270
|
+
gdf, count = suffix_duplicate_ids(gdf)
|
|
271
|
+
assert count == 2
|
|
272
|
+
assert sorted(gdf["id"]) == ["1~1", "1~2"]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_features_keep_their_row_when_repaired(tmp_folder):
|
|
276
|
+
"""Multi-part geometries are not split into one row per polygon: features keep
|
|
277
|
+
the geometry modeling of the source, so their ids and attributes stay 1:1 with
|
|
278
|
+
it. A repaired invalid polygon stays one feature as a MultiPolygon, and rows
|
|
279
|
+
without a polygonal geometry are dropped."""
|
|
280
|
+
import shapely
|
|
281
|
+
|
|
282
|
+
gdf = gpd.GeoDataFrame(
|
|
283
|
+
{
|
|
284
|
+
"id": [
|
|
285
|
+
"square",
|
|
286
|
+
"multi",
|
|
287
|
+
"bowtie",
|
|
288
|
+
"point",
|
|
289
|
+
"collection",
|
|
290
|
+
"debris",
|
|
291
|
+
"overlap",
|
|
292
|
+
"nested",
|
|
293
|
+
],
|
|
294
|
+
"name": ["a", "b", "c", "d", "e", "f", "g", "h"],
|
|
295
|
+
"geometry": [
|
|
296
|
+
shapely.Polygon([(0, 0), (0, 1), (1, 1), (1, 0)]),
|
|
297
|
+
shapely.MultiPolygon(
|
|
298
|
+
[
|
|
299
|
+
shapely.Polygon([(2, 0), (2, 1), (3, 1), (3, 0)]),
|
|
300
|
+
shapely.Polygon([(4, 0), (4, 1), (5, 1), (5, 0)]),
|
|
301
|
+
]
|
|
302
|
+
),
|
|
303
|
+
shapely.Polygon([(6, 0), (7, 1), (7, 0), (6, 1)]),
|
|
304
|
+
shapely.Point(10, 0),
|
|
305
|
+
# what make_valid() can emit: the polygonal part must survive alone
|
|
306
|
+
shapely.GeometryCollection(
|
|
307
|
+
[
|
|
308
|
+
shapely.Polygon([(8, 0), (8, 1), (9, 1), (9, 0)]),
|
|
309
|
+
shapely.LineString([(8, 2), (9, 2)]),
|
|
310
|
+
]
|
|
311
|
+
),
|
|
312
|
+
shapely.GeometryCollection([shapely.LineString([(10, 2), (11, 2)])]),
|
|
313
|
+
# members of a collection may overlap; the row must survive as a
|
|
314
|
+
# valid geometry instead of becoming an invalid MultiPolygon
|
|
315
|
+
shapely.GeometryCollection([shapely.box(12, 0, 14, 2), shapely.box(13, 1, 15, 3)]),
|
|
316
|
+
shapely.GeometryCollection(
|
|
317
|
+
[
|
|
318
|
+
shapely.GeometryCollection([shapely.box(16, 0, 17, 1)]),
|
|
319
|
+
shapely.LineString([(16, 2), (17, 2)]),
|
|
320
|
+
]
|
|
321
|
+
),
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
crs="EPSG:4326",
|
|
325
|
+
)
|
|
326
|
+
src = tmp_folder / "source.parquet"
|
|
327
|
+
gdf.to_parquet(src)
|
|
328
|
+
|
|
329
|
+
Converter = type("KeepConverter", (BaseConverter,), dict(CONFIG))
|
|
330
|
+
dest = tmp_folder / "converted.parquet"
|
|
331
|
+
Converter().convert(dest, input_files={str(src): "source.parquet"})
|
|
332
|
+
|
|
333
|
+
result = gpd.read_parquet(dest)
|
|
334
|
+
types = dict(zip(result["id"], result.geometry.geom_type))
|
|
335
|
+
assert sorted(types) == ["bowtie", "collection", "multi", "nested", "overlap", "square"]
|
|
336
|
+
assert types["square"] == "Polygon"
|
|
337
|
+
assert types["multi"] == "MultiPolygon"
|
|
338
|
+
assert types["bowtie"] == "MultiPolygon"
|
|
339
|
+
assert types["collection"] == "MultiPolygon"
|
|
340
|
+
assert types["nested"] == "MultiPolygon"
|
|
341
|
+
assert result.geometry.is_valid.all()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_duplicate_ids_get_numbered(tmp_folder):
|
|
345
|
+
"""id must be unique within a file; when the source repeats ids (here across
|
|
346
|
+
two input files), the repeats get a ~<n> suffix (vecorel/cli#47)."""
|
|
347
|
+
import shapely
|
|
348
|
+
|
|
349
|
+
files = {}
|
|
350
|
+
for index in range(2):
|
|
351
|
+
gdf = gpd.GeoDataFrame(
|
|
352
|
+
{
|
|
353
|
+
"id": ["same", f"other-{index}"],
|
|
354
|
+
"name": ["a", "b"],
|
|
355
|
+
"geometry": [
|
|
356
|
+
shapely.box(index * 4, 0, index * 4 + 1, 1),
|
|
357
|
+
shapely.box(index * 4 + 2, 0, index * 4 + 3, 1),
|
|
358
|
+
],
|
|
359
|
+
},
|
|
360
|
+
crs="EPSG:4326",
|
|
361
|
+
)
|
|
362
|
+
path = tmp_folder / f"dup_src_{index}.parquet"
|
|
363
|
+
gdf.to_parquet(path)
|
|
364
|
+
files[str(path)] = path.name
|
|
365
|
+
|
|
366
|
+
Converter = type("DupConverter", (BaseConverter,), dict(CONFIG))
|
|
367
|
+
dest = tmp_folder / "converted.parquet"
|
|
368
|
+
Converter().convert(dest, input_files=files)
|
|
369
|
+
|
|
370
|
+
assert sorted(gpd.read_parquet(dest)["id"]) == ["other-0", "other-1", "same~1", "same~2"]
|
|
371
|
+
|
|
372
|
+
|
|
130
373
|
def test_not_existing_converter(tmp_folder):
|
|
131
374
|
with pytest.raises(Exception, match="Converter 'not_existing' not found"):
|
|
132
375
|
converter = ConvertData("not_existing")
|