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.
Files changed (86) hide show
  1. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/PKG-INFO +3 -1
  2. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/pyproject.toml +14 -1
  3. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_convert.py +243 -0
  4. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_convert_duckdb.py +365 -13
  5. vecorel_cli-0.3.1/tests/test_deflate64.py +68 -0
  6. vecorel_cli-0.3.1/tests/test_download.py +194 -0
  7. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_improve.py +25 -1
  8. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/base.py +222 -74
  9. vecorel_cli-0.3.1/vecorel_cli/conversion/deflate64.py +54 -0
  10. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/duckdb.py +197 -35
  11. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/improve.py +9 -1
  12. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/util.py +51 -0
  13. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/PKG-INFO +3 -1
  14. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/SOURCES.txt +2 -0
  15. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/requires.txt +2 -0
  16. vecorel_cli-0.2.20/tests/test_download.py +0 -59
  17. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/LICENSE +0 -0
  18. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/README.md +0 -0
  19. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/setup.cfg +0 -0
  20. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_converters.py +0 -0
  21. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_create_geojson.py +0 -0
  22. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_create_geoparquet.py +0 -0
  23. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_create_stac.py +0 -0
  24. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_describe.py +0 -0
  25. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_encoding_auto.py +0 -0
  26. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_encoding_geojson.py +0 -0
  27. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_encoding_geoparquet.py +0 -0
  28. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_hilbert.py +0 -0
  29. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_jsonschema.py +0 -0
  30. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_merge.py +0 -0
  31. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_ops.py +0 -0
  32. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_rename_extension.py +0 -0
  33. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_validate.py +0 -0
  34. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/tests/test_validate_schema.py +0 -0
  35. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/__init__.py +0 -0
  36. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/basecommand.py +0 -0
  37. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/__init__.py +0 -0
  38. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/logger.py +0 -0
  39. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/options.py +0 -0
  40. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/path_url.py +0 -0
  41. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/setup.py +0 -0
  42. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/cli/util.py +0 -0
  43. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/const.py +0 -0
  44. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/__init__.py +0 -0
  45. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/admin.py +0 -0
  46. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/conversion/flatdict.py +0 -0
  47. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/convert.py +0 -0
  48. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/converters.py +0 -0
  49. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_geojson.py +0 -0
  50. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_geoparquet.py +0 -0
  51. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_jsonschema.py +0 -0
  52. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/create_stac.py +0 -0
  53. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/datasets/__init__.py +0 -0
  54. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/datasets/template.py +0 -0
  55. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/describe.py +0 -0
  56. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/__init__.py +0 -0
  57. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/auto.py +0 -0
  58. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/base.py +0 -0
  59. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/geojson.py +0 -0
  60. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/encoding/geoparquet.py +0 -0
  61. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/jsonschema/__init__.py +0 -0
  62. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/jsonschema/template.py +0 -0
  63. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/merge.py +0 -0
  64. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/parquet/__init__.py +0 -0
  65. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/parquet/geopandas.py +0 -0
  66. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/parquet/types.py +0 -0
  67. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/registry.py +0 -0
  68. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/rename_extension.py +0 -0
  69. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validate.py +0 -0
  70. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validate_schema.py +0 -0
  71. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/__init__.py +0 -0
  72. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/base.py +0 -0
  73. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/data.py +0 -0
  74. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/geojson.py +0 -0
  75. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/validation/geoparquet.py +0 -0
  76. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/__init__.py +0 -0
  77. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/collection.py +0 -0
  78. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/extensions.py +0 -0
  79. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/hilbert.py +0 -0
  80. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/ops.py +0 -0
  81. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/schemas.py +0 -0
  82. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/typing.py +0 -0
  83. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli/vecorel/version.py +0 -0
  84. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/dependency_links.txt +0 -0
  85. {vecorel_cli-0.2.20 → vecorel_cli-0.3.1}/vecorel_cli.egg-info/entry_points.txt +0 -0
  86. {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.2.20
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.2.20"
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")