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.
Files changed (87) hide show
  1. {vecorel_cli-0.4.1/vecorel_cli.egg-info → vecorel_cli-0.4.2}/PKG-INFO +1 -1
  2. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/pyproject.toml +1 -1
  3. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_convert_duckdb.py +15 -0
  4. vecorel_cli-0.4.2/tests/test_per_file.py +95 -0
  5. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/base.py +14 -8
  6. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/duckdb.py +6 -10
  7. vecorel_cli-0.4.2/vecorel_cli/conversion/per_file.py +53 -0
  8. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/util.py +11 -1
  9. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2/vecorel_cli.egg-info}/PKG-INFO +1 -1
  10. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/SOURCES.txt +2 -0
  11. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/LICENSE +0 -0
  12. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/README.md +0 -0
  13. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/setup.cfg +0 -0
  14. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_convert.py +0 -0
  15. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_converters.py +0 -0
  16. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_create_geojson.py +0 -0
  17. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_create_geoparquet.py +0 -0
  18. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_create_stac.py +0 -0
  19. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_deflate64.py +0 -0
  20. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_describe.py +0 -0
  21. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_download.py +0 -0
  22. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_encoding_auto.py +0 -0
  23. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_encoding_geojson.py +0 -0
  24. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_encoding_geoparquet.py +0 -0
  25. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_hilbert.py +0 -0
  26. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_improve.py +0 -0
  27. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_jsonschema.py +0 -0
  28. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_merge.py +0 -0
  29. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_ops.py +0 -0
  30. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_rename_extension.py +0 -0
  31. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_validate.py +0 -0
  32. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/tests/test_validate_schema.py +0 -0
  33. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/__init__.py +0 -0
  34. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/basecommand.py +0 -0
  35. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/__init__.py +0 -0
  36. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/logger.py +0 -0
  37. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/options.py +0 -0
  38. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/path_url.py +0 -0
  39. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/setup.py +0 -0
  40. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/cli/util.py +0 -0
  41. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/const.py +0 -0
  42. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/__init__.py +0 -0
  43. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/admin.py +0 -0
  44. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/deflate64.py +0 -0
  45. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/conversion/flatdict.py +0 -0
  46. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/convert.py +0 -0
  47. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/converters.py +0 -0
  48. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_geojson.py +0 -0
  49. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_geoparquet.py +0 -0
  50. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_jsonschema.py +0 -0
  51. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/create_stac.py +0 -0
  52. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/datasets/__init__.py +0 -0
  53. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/datasets/template.py +0 -0
  54. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/describe.py +0 -0
  55. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/__init__.py +0 -0
  56. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/auto.py +0 -0
  57. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/base.py +0 -0
  58. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/geojson.py +0 -0
  59. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/encoding/geoparquet.py +0 -0
  60. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/improve.py +0 -0
  61. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/jsonschema/__init__.py +0 -0
  62. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/jsonschema/template.py +0 -0
  63. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/merge.py +0 -0
  64. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/parquet/__init__.py +0 -0
  65. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/parquet/geopandas.py +0 -0
  66. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/parquet/types.py +0 -0
  67. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/registry.py +0 -0
  68. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/rename_extension.py +0 -0
  69. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validate.py +0 -0
  70. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validate_schema.py +0 -0
  71. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/__init__.py +0 -0
  72. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/base.py +0 -0
  73. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/data.py +0 -0
  74. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/geojson.py +0 -0
  75. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/validation/geoparquet.py +0 -0
  76. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/__init__.py +0 -0
  77. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/collection.py +0 -0
  78. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/extensions.py +0 -0
  79. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/hilbert.py +0 -0
  80. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/ops.py +0 -0
  81. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/schemas.py +0 -0
  82. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/typing.py +0 -0
  83. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli/vecorel/version.py +0 -0
  84. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/dependency_links.txt +0 -0
  85. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/entry_points.txt +0 -0
  86. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/vecorel_cli.egg-info/requires.txt +0 -0
  87. {vecorel_cli-0.4.1 → vecorel_cli-0.4.2}/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.4.1
3
+ Version: 0.4.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vecorel-cli"
7
- version = "0.4.1"
7
+ version = "0.4.2"
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"
@@ -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
- if input_files is not None and isinstance(input_files, dict) and len(input_files) > 0:
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
- if input_files is not None and isinstance(input_files, dict) and len(input_files) > 0:
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, compression, collection_json],
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, compression, collection_json, row_group_size, per_collection
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
- compression,
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
- return HTTPFileSystem(client_kwargs={**client_kwargs, "headers": headers}, **kwargs)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vecorel-cli
3
- Version: 0.4.1
3
+ Version: 0.4.2
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
@@ -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