vecorel-cli 0.4.1__tar.gz → 0.4.2__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.4.1/vecorel_cli.egg-info → vecorel_cli-0.4.2}/PKG-INFO +1 -1
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/pyproject.toml +1 -1
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_convert_duckdb.py +15 -0
- vecorel_cli-0.4.2/tests/test_per_file.py +95 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/base.py +14 -8
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/duckdb.py +6 -10
- vecorel_cli-0.4.2/vecorel_cli/conversion/per_file.py +53 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/util.py +11 -1
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2/vecorel_cli.egg-info}/PKG-INFO +1 -1
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/SOURCES.txt +2 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/LICENSE +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/README.md +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/setup.cfg +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_convert.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_converters.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_create_geojson.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_create_geoparquet.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_create_stac.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_deflate64.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_describe.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_download.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_encoding_auto.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_encoding_geojson.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_encoding_geoparquet.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_hilbert.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_improve.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_jsonschema.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_merge.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_ops.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_rename_extension.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_validate.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_validate_schema.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/basecommand.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/logger.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/options.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/path_url.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/setup.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/util.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/const.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/admin.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/deflate64.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/flatdict.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/convert.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/converters.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_geojson.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_geoparquet.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_jsonschema.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_stac.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/datasets/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/datasets/template.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/describe.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/auto.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/base.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/geojson.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/geoparquet.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/improve.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/jsonschema/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/jsonschema/template.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/merge.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/parquet/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/parquet/geopandas.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/parquet/types.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/registry.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/rename_extension.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validate.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validate_schema.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/base.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/data.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/geojson.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/geoparquet.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/__init__.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/collection.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/extensions.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/hilbert.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/ops.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/schemas.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/typing.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/version.py +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/dependency_links.txt +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/entry_points.txt +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/requires.txt +0 -0
- {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/top_level.txt +0 -0
|
@@ -10,6 +10,7 @@ import pytest
|
|
|
10
10
|
import shapely
|
|
11
11
|
from loguru import logger
|
|
12
12
|
|
|
13
|
+
from vecorel_cli.const import COMPRESSION_METHODS
|
|
13
14
|
from vecorel_cli.conversion import duckdb as duckdb_module
|
|
14
15
|
from vecorel_cli.conversion.base import BaseConverter
|
|
15
16
|
from vecorel_cli.conversion.duckdb import DuckDBBaseConverter, _constants_table
|
|
@@ -563,6 +564,20 @@ def test_merge_parquet(tmp_folder):
|
|
|
563
564
|
assert ValidateData().validate(dest, num=100, schema_map={}).errors == []
|
|
564
565
|
|
|
565
566
|
|
|
567
|
+
@pytest.mark.parametrize("compression", COMPRESSION_METHODS)
|
|
568
|
+
def test_every_compression_method_is_written(tmp_folder, compression):
|
|
569
|
+
src = _source_file(tmp_folder)
|
|
570
|
+
part = tmp_folder / "part.parquet"
|
|
571
|
+
Converter().convert(part, input_files={src: "source.parquet"}, compression=compression)
|
|
572
|
+
dest = tmp_folder / "merged.parquet"
|
|
573
|
+
Converter().merge_parquet([part], dest, compression=compression)
|
|
574
|
+
|
|
575
|
+
for path in (part, dest):
|
|
576
|
+
with pq.ParquetFile(path) as pf:
|
|
577
|
+
codec = pf.metadata.row_group(0).column(0).compression
|
|
578
|
+
assert codec == ("UNCOMPRESSED" if compression == "none" else compression.upper())
|
|
579
|
+
|
|
580
|
+
|
|
566
581
|
def test_merge_parquet_sees_an_id_that_repeats_across_parts(tmp_folder, capsys):
|
|
567
582
|
"""A per-part check cannot see this; a check over the merge can."""
|
|
568
583
|
parts = []
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import geopandas as gpd
|
|
5
|
+
import multivolumefile
|
|
6
|
+
import py7zr
|
|
7
|
+
import shapely
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from vecorel_cli.conversion.base import BaseConverter
|
|
11
|
+
from vecorel_cli.conversion.per_file import PerFileConverterMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Converter(PerFileConverterMixin, BaseConverter):
|
|
15
|
+
id = "test"
|
|
16
|
+
short_name = "Test"
|
|
17
|
+
title = "Test dataset"
|
|
18
|
+
description = "Test dataset"
|
|
19
|
+
license = "CC0-1.0"
|
|
20
|
+
variants = {"2024": None, "2025": None}
|
|
21
|
+
columns = {"geometry": "geometry", "id": "id", "region": "region"}
|
|
22
|
+
missing_schemas = {"properties": {"region": {"type": "string"}}}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _source(folder, region):
|
|
26
|
+
gdf = gpd.GeoDataFrame(
|
|
27
|
+
{
|
|
28
|
+
"id": [f"{region}-1", f"{region}-2"],
|
|
29
|
+
"region": [region, region],
|
|
30
|
+
"geometry": [shapely.box(0, 0, 1, 1), shapely.box(2, 0, 3, 1)],
|
|
31
|
+
},
|
|
32
|
+
crs="EPSG:4326",
|
|
33
|
+
)
|
|
34
|
+
path = folder / f"{region}.parquet"
|
|
35
|
+
gdf.to_parquet(path)
|
|
36
|
+
return path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _capture_log():
|
|
40
|
+
# loguru's default sink is bound to stderr at import, so point it at stdout
|
|
41
|
+
logger.remove()
|
|
42
|
+
logger.add(sys.stdout, format="{message}", level="DEBUG", colorize=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_each_source_is_converted_on_its_own_and_merged(tmp_path, capsys):
|
|
46
|
+
sources = [_source(tmp_path, region) for region in ("a", "b")]
|
|
47
|
+
output = tmp_path / "out" / "merged.parquet"
|
|
48
|
+
_capture_log()
|
|
49
|
+
|
|
50
|
+
Converter().convert(str(output), input_files={str(s): s.name for s in sources})
|
|
51
|
+
|
|
52
|
+
merged = gpd.read_parquet(output)
|
|
53
|
+
assert sorted(merged["id"]) == ["a-1", "a-2", "b-1", "b-2"]
|
|
54
|
+
# constant within each part, so it only survives the merge as a column
|
|
55
|
+
assert sorted(merged["region"].unique()) == ["a", "b"]
|
|
56
|
+
assert [p.name for p in output.parent.iterdir()] == ["merged.parquet"] # the parts are gone
|
|
57
|
+
|
|
58
|
+
log = capsys.readouterr().out
|
|
59
|
+
assert log.count("Using user provided input file(s)") == 1
|
|
60
|
+
assert log.count("No variant given") == 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_a_single_source_is_converted_directly(tmp_path):
|
|
64
|
+
source = _source(tmp_path, "a")
|
|
65
|
+
output = tmp_path / "single.parquet"
|
|
66
|
+
|
|
67
|
+
Converter().convert(str(output), input_files={str(source): source.name})
|
|
68
|
+
|
|
69
|
+
assert sorted(gpd.read_parquet(output)["id"]) == ["a-1", "a-2"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_the_volumes_of_a_7z_archive_are_converted_together(tmp_path):
|
|
73
|
+
archived = _source(tmp_path, "b")
|
|
74
|
+
measure = io.BytesIO()
|
|
75
|
+
with py7zr.SevenZipFile(measure, "w") as archive:
|
|
76
|
+
archive.write(archived, archived.name)
|
|
77
|
+
volume_size = len(measure.getvalue()) // 3 + 1
|
|
78
|
+
with multivolumefile.MultiVolume(
|
|
79
|
+
str(tmp_path / "b.7z"), mode="wb", volume=volume_size, ext_digits=3
|
|
80
|
+
) as volume:
|
|
81
|
+
with py7zr.SevenZipFile(volume, "w") as archive:
|
|
82
|
+
archive.write(archived, archived.name)
|
|
83
|
+
volumes = sorted(tmp_path.glob("b.7z.*"))
|
|
84
|
+
assert len(volumes) > 1, "the archive must span several volumes for this test"
|
|
85
|
+
archived.unlink()
|
|
86
|
+
|
|
87
|
+
source = _source(tmp_path, "a")
|
|
88
|
+
output = tmp_path / "merged.parquet"
|
|
89
|
+
# only the first volume names the target file
|
|
90
|
+
input_files = {str(source): source.name}
|
|
91
|
+
input_files |= {str(v): ([archived.name] if i == 0 else []) for i, v in enumerate(volumes)}
|
|
92
|
+
|
|
93
|
+
Converter().convert(str(output), input_files=input_files)
|
|
94
|
+
|
|
95
|
+
assert sorted(gpd.read_parquet(output)["id"]) == ["a-1", "a-2", "b-1", "b-2"]
|
|
@@ -22,6 +22,7 @@ import py7zr
|
|
|
22
22
|
import rarfile
|
|
23
23
|
import shapely
|
|
24
24
|
from fsspec import AbstractFileSystem
|
|
25
|
+
from fsspec.exceptions import FSTimeoutError
|
|
25
26
|
from fsspec.implementations.local import LocalFileSystem
|
|
26
27
|
from geopandas import GeoDataFrame
|
|
27
28
|
|
|
@@ -443,11 +444,13 @@ class BaseConverter(LoggerMixin):
|
|
|
443
444
|
try:
|
|
444
445
|
with cache_fs.open(part_file, mode="wb") as file:
|
|
445
446
|
stream_file(source_fs, uri, file)
|
|
446
|
-
except BaseException:
|
|
447
|
+
except BaseException as e:
|
|
447
448
|
try:
|
|
448
449
|
cache_fs.rm(part_file)
|
|
449
450
|
except FileNotFoundError:
|
|
450
451
|
pass
|
|
452
|
+
if isinstance(e, FSTimeoutError):
|
|
453
|
+
raise TimeoutError(f"Download timed out: {uri}") from e
|
|
451
454
|
raise
|
|
452
455
|
cache_fs.mv(part_file, cache_file)
|
|
453
456
|
|
|
@@ -513,6 +516,15 @@ class BaseConverter(LoggerMixin):
|
|
|
513
516
|
raise ValueError(f"Unknown variant '{self.variant}', choose from {opts}")
|
|
514
517
|
return urls
|
|
515
518
|
|
|
519
|
+
def get_input_urls(self, input_files=None):
|
|
520
|
+
if input_files is not None and isinstance(input_files, dict) and len(input_files) > 0:
|
|
521
|
+
self.warning("Using user provided input file(s) instead of the pre-defined file(s)")
|
|
522
|
+
return input_files
|
|
523
|
+
urls = self.get_urls()
|
|
524
|
+
if urls is None:
|
|
525
|
+
raise ValueError("No input files provided")
|
|
526
|
+
return urls
|
|
527
|
+
|
|
516
528
|
def get_data(
|
|
517
529
|
self, paths: list[tuple[str, str]], **kwargs
|
|
518
530
|
) -> Generator[tuple[GeoDataFrame, str, str, Optional[str]]]:
|
|
@@ -655,13 +667,7 @@ class BaseConverter(LoggerMixin):
|
|
|
655
667
|
if directory:
|
|
656
668
|
os.makedirs(directory, exist_ok=True)
|
|
657
669
|
|
|
658
|
-
|
|
659
|
-
self.warning("Using user provided input file(s) instead of the pre-defined file(s)")
|
|
660
|
-
urls = input_files
|
|
661
|
-
else:
|
|
662
|
-
urls = self.get_urls()
|
|
663
|
-
if urls is None:
|
|
664
|
-
raise ValueError("No input files provided")
|
|
670
|
+
urls = self.get_input_urls(input_files)
|
|
665
671
|
|
|
666
672
|
self.info("Getting file(s) if not cached yet")
|
|
667
673
|
paths = self.download_files(urls, cache)
|
|
@@ -116,13 +116,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
116
116
|
if directory:
|
|
117
117
|
os.makedirs(directory, exist_ok=True)
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
self.warning("Using user provided input file(s) instead of the pre-defined file(s)")
|
|
121
|
-
urls = input_files
|
|
122
|
-
else:
|
|
123
|
-
urls = self.get_urls()
|
|
124
|
-
if urls is None:
|
|
125
|
-
raise ValueError("No input files provided")
|
|
119
|
+
urls = self.get_input_urls(input_files)
|
|
126
120
|
|
|
127
121
|
self.info("Getting file(s) if not cached yet")
|
|
128
122
|
# Without a cache DuckDB reads remote parquet files in place; anything else, such as
|
|
@@ -368,6 +362,8 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
368
362
|
compression = compression or "zstd"
|
|
369
363
|
if compression == "zstd" and compression_level is None:
|
|
370
364
|
compression_level = 15
|
|
365
|
+
# DuckDB calls "none" "uncompressed"; the postprocess (Arrow) takes the original name
|
|
366
|
+
copy_compression = "uncompressed" if compression == "none" else compression
|
|
371
367
|
geoparquet_version = geoparquet_version or "1.1.0"
|
|
372
368
|
row_group_size = GeoParquet.row_group_size
|
|
373
369
|
if isinstance(output_file, Path):
|
|
@@ -561,12 +557,12 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
561
557
|
}}
|
|
562
558
|
)
|
|
563
559
|
""",
|
|
564
|
-
[output_file,
|
|
560
|
+
[output_file, copy_compression, collection_json],
|
|
565
561
|
)
|
|
566
562
|
|
|
567
563
|
if "id" in selected_targets and suffix_duplicate_ids:
|
|
568
564
|
self._suffix_duplicate_ids_in_file(
|
|
569
|
-
con, output_file,
|
|
565
|
+
con, output_file, copy_compression, collection_json, row_group_size, per_collection
|
|
570
566
|
)
|
|
571
567
|
|
|
572
568
|
# Sort against the same CRS-derived Hilbert grid as the
|
|
@@ -591,7 +587,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
591
587
|
con,
|
|
592
588
|
output_file,
|
|
593
589
|
keys_path,
|
|
594
|
-
|
|
590
|
+
copy_compression,
|
|
595
591
|
collection_json,
|
|
596
592
|
final_group_size,
|
|
597
593
|
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tempfile import TemporaryDirectory
|
|
3
|
+
|
|
4
|
+
from .duckdb import DuckDBBaseConverter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PerFileConverterMixin:
|
|
8
|
+
"""Converts each source on its own and merges the parts, for sources that do not fit
|
|
9
|
+
into memory together. The ids must be unique across the sources, and the parts need
|
|
10
|
+
about the size of the output in free disk space next to it."""
|
|
11
|
+
|
|
12
|
+
_part_urls = None
|
|
13
|
+
|
|
14
|
+
def get_input_urls(self, input_files=None):
|
|
15
|
+
if self._part_urls is not None:
|
|
16
|
+
return self._part_urls
|
|
17
|
+
return super().get_input_urls(input_files)
|
|
18
|
+
|
|
19
|
+
def convert(self, output_file, cache=None, input_files=None, variant=None, **kwargs) -> str:
|
|
20
|
+
self.select_variant(variant)
|
|
21
|
+
urls = self.get_input_urls(input_files)
|
|
22
|
+
sources = self._split_sources(urls)
|
|
23
|
+
# the variant is passed on as selected, so that its default is not reported per part
|
|
24
|
+
try:
|
|
25
|
+
if len(sources) <= 1:
|
|
26
|
+
self._part_urls = urls
|
|
27
|
+
return super().convert(output_file, cache=cache, variant=self.variant, **kwargs)
|
|
28
|
+
|
|
29
|
+
directory = os.path.dirname(output_file) or "."
|
|
30
|
+
os.makedirs(directory, exist_ok=True)
|
|
31
|
+
with TemporaryDirectory(dir=directory) as parts_folder:
|
|
32
|
+
parts = []
|
|
33
|
+
for index, source in enumerate(sources):
|
|
34
|
+
self.info(f"Converting source {index + 1}/{len(sources)}: {next(iter(source))}")
|
|
35
|
+
part = os.path.join(parts_folder, f"part_{index}.parquet")
|
|
36
|
+
self._part_urls = source
|
|
37
|
+
super().convert(part, cache=cache, variant=self.variant, **kwargs)
|
|
38
|
+
parts.append(part)
|
|
39
|
+
|
|
40
|
+
self.info(f"Merging {len(parts)} parts into {output_file}")
|
|
41
|
+
merge_options = ("compression", "compression_level", "geoparquet_version")
|
|
42
|
+
return DuckDBBaseConverter().merge_parquet(
|
|
43
|
+
parts, output_file, **{k: kwargs[k] for k in merge_options if k in kwargs}
|
|
44
|
+
)
|
|
45
|
+
finally:
|
|
46
|
+
self._part_urls = None
|
|
47
|
+
|
|
48
|
+
def _split_sources(self, urls) -> list:
|
|
49
|
+
if isinstance(urls, str):
|
|
50
|
+
return [urls]
|
|
51
|
+
# the volumes of a multi-volume 7z archive can only be extracted together
|
|
52
|
+
regular, volume_groups = self._group_multivolume_7z(urls)
|
|
53
|
+
return [{uri: target} for uri, target in regular.items()] + list(volume_groups.values())
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Optional, Union
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
|
|
8
|
+
import aiohttp
|
|
8
9
|
import yaml
|
|
9
10
|
from fsspec import AbstractFileSystem
|
|
10
11
|
from fsspec.implementations.http import HTTPFileSystem
|
|
@@ -131,6 +132,10 @@ def merge_headers(*headers: dict) -> dict:
|
|
|
131
132
|
return merged
|
|
132
133
|
|
|
133
134
|
|
|
135
|
+
HTTP_CONNECT_TIMEOUT = 60
|
|
136
|
+
HTTP_READ_TIMEOUT = 300
|
|
137
|
+
|
|
138
|
+
|
|
134
139
|
def get_fs(url_or_path: Union[str, Path, URL], **kwargs) -> AbstractFileSystem:
|
|
135
140
|
"""Choose fsspec filesystem by sniffing input url"""
|
|
136
141
|
if isinstance(url_or_path, Path):
|
|
@@ -142,7 +147,12 @@ def get_fs(url_or_path: Union[str, Path, URL], **kwargs) -> AbstractFileSystem:
|
|
|
142
147
|
if parsed.scheme in ("http", "https"):
|
|
143
148
|
client_kwargs = kwargs.pop("client_kwargs", {})
|
|
144
149
|
headers = merge_headers({"User-Agent": USER_AGENT}, client_kwargs.get("headers", {}))
|
|
145
|
-
|
|
150
|
+
timeout = client_kwargs.get("timeout") or aiohttp.ClientTimeout(
|
|
151
|
+
total=None, sock_connect=HTTP_CONNECT_TIMEOUT, sock_read=HTTP_READ_TIMEOUT
|
|
152
|
+
)
|
|
153
|
+
return HTTPFileSystem(
|
|
154
|
+
client_kwargs={**client_kwargs, "headers": headers, "timeout": timeout}, **kwargs
|
|
155
|
+
)
|
|
146
156
|
|
|
147
157
|
if parsed.scheme == "s3":
|
|
148
158
|
from s3fs import S3FileSystem
|
|
@@ -18,6 +18,7 @@ tests/test_improve.py
|
|
|
18
18
|
tests/test_jsonschema.py
|
|
19
19
|
tests/test_merge.py
|
|
20
20
|
tests/test_ops.py
|
|
21
|
+
tests/test_per_file.py
|
|
21
22
|
tests/test_rename_extension.py
|
|
22
23
|
tests/test_validate.py
|
|
23
24
|
tests/test_validate_schema.py
|
|
@@ -55,6 +56,7 @@ vecorel_cli/conversion/base.py
|
|
|
55
56
|
vecorel_cli/conversion/deflate64.py
|
|
56
57
|
vecorel_cli/conversion/duckdb.py
|
|
57
58
|
vecorel_cli/conversion/flatdict.py
|
|
59
|
+
vecorel_cli/conversion/per_file.py
|
|
58
60
|
vecorel_cli/datasets/__init__.py
|
|
59
61
|
vecorel_cli/datasets/template.py
|
|
60
62
|
vecorel_cli/encoding/__init__.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|