vecorel-cli 0.4.0__tar.gz → 0.4.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.4.0/vecorel_cli.egg-info → vecorel_cli-0.4.1}/PKG-INFO +1 -1
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/pyproject.toml +1 -1
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_convert_duckdb.py +43 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_download.py +54 -1
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/conversion/base.py +8 -1
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/conversion/duckdb.py +23 -11
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/util.py +12 -1
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1/vecorel_cli.egg-info}/PKG-INFO +1 -1
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/LICENSE +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/README.md +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/setup.cfg +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_convert.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_converters.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_create_geojson.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_create_geoparquet.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_create_stac.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_deflate64.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_describe.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_encoding_auto.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_encoding_geojson.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_encoding_geoparquet.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_hilbert.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_improve.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_jsonschema.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_merge.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_ops.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_rename_extension.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_validate.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/tests/test_validate_schema.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/basecommand.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/cli/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/cli/logger.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/cli/options.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/cli/path_url.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/cli/setup.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/cli/util.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/const.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/conversion/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/conversion/admin.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/conversion/deflate64.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/conversion/flatdict.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/convert.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/converters.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/create_geojson.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/create_geoparquet.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/create_jsonschema.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/create_stac.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/datasets/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/datasets/template.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/describe.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/encoding/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/encoding/auto.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/encoding/base.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/encoding/geojson.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/encoding/geoparquet.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/improve.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/jsonschema/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/jsonschema/template.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/merge.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/parquet/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/parquet/geopandas.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/parquet/types.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/registry.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/rename_extension.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validate.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validate_schema.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validation/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validation/base.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validation/data.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validation/geojson.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/validation/geoparquet.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/__init__.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/collection.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/extensions.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/hilbert.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/ops.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/schemas.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/typing.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli/vecorel/version.py +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli.egg-info/SOURCES.txt +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli.egg-info/dependency_links.txt +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli.egg-info/entry_points.txt +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli.egg-info/requires.txt +0 -0
- {vecorel_cli-0.4.0 → vecorel_cli-0.4.1}/vecorel_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import re
|
|
3
3
|
import sys
|
|
4
|
+
import zipfile
|
|
4
5
|
|
|
5
6
|
import geopandas as gpd
|
|
6
7
|
import numpy as np
|
|
@@ -109,6 +110,48 @@ def test_duckdb_converter(tmp_folder):
|
|
|
109
110
|
assert validation.errors == []
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
def test_duckdb_converter_extracts_an_archive_without_a_cache(tmp_folder):
|
|
114
|
+
src = _source_file(tmp_folder)
|
|
115
|
+
archive = tmp_folder / "source.zip"
|
|
116
|
+
with zipfile.ZipFile(archive, "w") as zf:
|
|
117
|
+
zf.write(src, "source.parquet")
|
|
118
|
+
dest = tmp_folder / "converted.parquet"
|
|
119
|
+
|
|
120
|
+
Converter().convert(dest, input_files={str(archive): ["source.parquet"]})
|
|
121
|
+
|
|
122
|
+
assert sorted(gpd.read_parquet(dest)["id"]) == ["bowtie", "multi", "square", "with_z"]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_duckdb_converter_ignores_hive_style_folders(tmp_folder):
|
|
126
|
+
source_folder = tmp_folder / "region=north"
|
|
127
|
+
source_folder.mkdir()
|
|
128
|
+
src = _source_file(source_folder)
|
|
129
|
+
dest_folder = tmp_folder / "year=2024"
|
|
130
|
+
dest_folder.mkdir()
|
|
131
|
+
dest = dest_folder / "converted.parquet"
|
|
132
|
+
|
|
133
|
+
Converter().convert(dest, input_files={src: "source.parquet"})
|
|
134
|
+
|
|
135
|
+
with pq.ParquetFile(dest) as pf:
|
|
136
|
+
names = pf.schema_arrow.names
|
|
137
|
+
assert "year" not in names
|
|
138
|
+
assert "region" not in names
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.parametrize(
|
|
142
|
+
"urls, in_place",
|
|
143
|
+
[
|
|
144
|
+
("https://host/data.parquet?token=abc", True),
|
|
145
|
+
("https://host/data.geoparquet", True),
|
|
146
|
+
({"https://host/x?id=1": "fields.parquet"}, True),
|
|
147
|
+
({"https://host/data.zip": ["fields.parquet"]}, False),
|
|
148
|
+
("https://host/data.fgb", False),
|
|
149
|
+
],
|
|
150
|
+
)
|
|
151
|
+
def test_only_parquet_is_read_in_place(urls, in_place):
|
|
152
|
+
assert duckdb_module._readable_in_place(urls) is in_place
|
|
153
|
+
|
|
154
|
+
|
|
112
155
|
def test_duckdb_converter_numbers_rows_without_an_id_mapping(tmp_folder):
|
|
113
156
|
src = _source_file(tmp_folder)
|
|
114
157
|
dest = tmp_folder / "converted.parquet"
|
|
@@ -7,7 +7,7 @@ from fsspec.implementations.local import LocalFileSystem
|
|
|
7
7
|
|
|
8
8
|
import vecorel_cli.conversion.base as base
|
|
9
9
|
from vecorel_cli.conversion.base import BaseConverter
|
|
10
|
-
from vecorel_cli.vecorel.util import stream_file
|
|
10
|
+
from vecorel_cli.vecorel.util import get_fs, merge_headers, stream_file
|
|
11
11
|
|
|
12
12
|
URI = "https://example.invalid/data/fields.gml"
|
|
13
13
|
|
|
@@ -192,3 +192,56 @@ def test_multivolume_7z_is_downloaded_and_extracted(explicit_cache, tmp_folder,
|
|
|
192
192
|
# every volume was cached under its own name
|
|
193
193
|
for part in parts:
|
|
194
194
|
assert (tmp_folder / part).exists()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_request_headers_go_with_every_download(tmp_folder, monkeypatch):
|
|
198
|
+
seen = []
|
|
199
|
+
source_fs = _FakeSourceFS(b"body", {})
|
|
200
|
+
|
|
201
|
+
def fake_get_fs(uri, **kwargs):
|
|
202
|
+
if str(uri).startswith(("http://", "https://")):
|
|
203
|
+
seen.append(kwargs.get("client_kwargs"))
|
|
204
|
+
return source_fs
|
|
205
|
+
return LocalFileSystem()
|
|
206
|
+
|
|
207
|
+
monkeypatch.setattr(base, "get_fs", fake_get_fs)
|
|
208
|
+
HeaderConverter = type(
|
|
209
|
+
"HeaderConverter", (BaseConverter,), {"request_headers": {"X-Test": "1"}}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
HeaderConverter().download_files(URI, cache_folder=str(tmp_folder))
|
|
213
|
+
|
|
214
|
+
assert seen == [{"headers": {"X-Test": "1"}}]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_request_headers_merge_with_explicit_client_options(tmp_folder, monkeypatch):
|
|
218
|
+
seen = []
|
|
219
|
+
source_fs = _FakeSourceFS(b"body", {})
|
|
220
|
+
|
|
221
|
+
def fake_get_fs(uri, **kwargs):
|
|
222
|
+
if str(uri).startswith(("http://", "https://")):
|
|
223
|
+
seen.append(kwargs.get("client_kwargs"))
|
|
224
|
+
return source_fs
|
|
225
|
+
return LocalFileSystem()
|
|
226
|
+
|
|
227
|
+
monkeypatch.setattr(base, "get_fs", fake_get_fs)
|
|
228
|
+
HeaderConverter = type(
|
|
229
|
+
"HeaderConverter", (BaseConverter,), {"request_headers": {"X-Test": "1", "X-Kept": "k"}}
|
|
230
|
+
)
|
|
231
|
+
client_kwargs = {"trust_env": True, "headers": {"x-test": "2"}}
|
|
232
|
+
|
|
233
|
+
HeaderConverter().download_files(URI, cache_folder=str(tmp_folder), client_kwargs=client_kwargs)
|
|
234
|
+
|
|
235
|
+
assert seen == [{"trust_env": True, "headers": {"X-Kept": "k", "x-test": "2"}}]
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_headers_merge_case_insensitively():
|
|
239
|
+
merged = merge_headers(
|
|
240
|
+
{"User-Agent": "default", "X-Test": "1"}, {"user-agent": "mine", "x-test": "2"}
|
|
241
|
+
)
|
|
242
|
+
assert merged == {"user-agent": "mine", "x-test": "2"}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_a_converter_user_agent_replaces_the_default():
|
|
246
|
+
fs = get_fs("https://example.invalid/x", client_kwargs={"headers": {"user-agent": "browser"}})
|
|
247
|
+
assert fs.client_kwargs["headers"] == {"user-agent": "browser"}
|
|
@@ -33,7 +33,7 @@ from ..vecorel.collection import Collection
|
|
|
33
33
|
from ..vecorel.hilbert import hilbert_sort_geodataframe
|
|
34
34
|
from ..vecorel.schemas import Schemas
|
|
35
35
|
from ..vecorel.typing import Sources
|
|
36
|
-
from ..vecorel.util import get_fs, name_from_uri, stream_file, suffix_duplicate_ids
|
|
36
|
+
from ..vecorel.util import get_fs, merge_headers, name_from_uri, stream_file, suffix_duplicate_ids
|
|
37
37
|
from .deflate64 import deflate64_zip
|
|
38
38
|
from .flatdict import FlatDict
|
|
39
39
|
|
|
@@ -58,6 +58,8 @@ class BaseConverter(LoggerMixin):
|
|
|
58
58
|
data_access: str = ""
|
|
59
59
|
open_options: dict = {}
|
|
60
60
|
avoid_range_request: bool = False
|
|
61
|
+
# Sent with every download, e.g. a browser User-Agent for a server that refuses others
|
|
62
|
+
request_headers: dict[str, str] = {}
|
|
61
63
|
variants: dict[str, Sources] = {}
|
|
62
64
|
variant: Optional[str] = None
|
|
63
65
|
|
|
@@ -365,6 +367,11 @@ class BaseConverter(LoggerMixin):
|
|
|
365
367
|
|
|
366
368
|
if self.avoid_range_request and "block_size" not in kwargs:
|
|
367
369
|
kwargs["block_size"] = 0
|
|
370
|
+
if self.request_headers:
|
|
371
|
+
# headers passed in explicitly win over the converter's
|
|
372
|
+
client_kwargs = kwargs.get("client_kwargs") or {}
|
|
373
|
+
headers = merge_headers(self.request_headers, client_kwargs.get("headers", {}))
|
|
374
|
+
kwargs["client_kwargs"] = {**client_kwargs, "headers": headers}
|
|
368
375
|
|
|
369
376
|
# Multi-volume 7z archives (.7z.001, .7z.002, ...) are one 7z stream split
|
|
370
377
|
# across several URIs; they must be downloaded and extracted together.
|
|
@@ -21,7 +21,7 @@ from ..vecorel.ops import (
|
|
|
21
21
|
report,
|
|
22
22
|
warn_missing_required,
|
|
23
23
|
)
|
|
24
|
-
from ..vecorel.util import find_differing_crs
|
|
24
|
+
from ..vecorel.util import find_differing_crs, name_from_uri
|
|
25
25
|
from .base import BaseConverter
|
|
26
26
|
|
|
27
27
|
|
|
@@ -32,6 +32,15 @@ def _sql_path(path) -> str:
|
|
|
32
32
|
return f"'{escaped}'"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def _readable_in_place(urls) -> bool:
|
|
36
|
+
if isinstance(urls, str):
|
|
37
|
+
urls = {urls: name_from_uri(urls)}
|
|
38
|
+
return all(
|
|
39
|
+
isinstance(target, str) and target.lower().endswith((".parquet", ".geoparquet"))
|
|
40
|
+
for target in urls.values()
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
35
44
|
def _sql_name(name) -> str:
|
|
36
45
|
escaped = str(name).replace('"', '""')
|
|
37
46
|
return f'"{escaped}"'
|
|
@@ -116,7 +125,9 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
116
125
|
raise ValueError("No input files provided")
|
|
117
126
|
|
|
118
127
|
self.info("Getting file(s) if not cached yet")
|
|
119
|
-
|
|
128
|
+
# Without a cache DuckDB reads remote parquet files in place; anything else, such as
|
|
129
|
+
# an archive, is downloaded to a temporary folder as the GeoDataFrame codepath does
|
|
130
|
+
if cache or not _readable_in_place(urls):
|
|
120
131
|
request_args = {}
|
|
121
132
|
if self.avoid_range_request:
|
|
122
133
|
request_args["block_size"] = 0
|
|
@@ -140,7 +151,8 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
140
151
|
available = {
|
|
141
152
|
row[0]: row[1]
|
|
142
153
|
for row in con.execute(
|
|
143
|
-
"DESCRIBE SELECT * FROM read_parquet(?, union_by_name=true)",
|
|
154
|
+
"DESCRIBE SELECT * FROM read_parquet(?, union_by_name=true, hive_partitioning=false)",
|
|
155
|
+
[sources],
|
|
144
156
|
).fetchall()
|
|
145
157
|
}
|
|
146
158
|
|
|
@@ -190,7 +202,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
190
202
|
# migrated type so integer-valued floats format the same way
|
|
191
203
|
expr = f"({self.column_migrations[c]})"
|
|
192
204
|
sql_type = con.execute(
|
|
193
|
-
f"DESCRIBE SELECT {expr} AS part FROM read_parquet(?, union_by_name=true)",
|
|
205
|
+
f"DESCRIBE SELECT {expr} AS part FROM read_parquet(?, union_by_name=true, hive_partitioning=false)",
|
|
194
206
|
[sources],
|
|
195
207
|
).fetchone()[1]
|
|
196
208
|
parts.append(self._stringify_sql(expr, sql_type, c))
|
|
@@ -260,7 +272,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
260
272
|
sources_sql = "[" + ",".join(_sql_path(path) for path in sources) + "]"
|
|
261
273
|
source_query = f"""
|
|
262
274
|
SELECT {selection}
|
|
263
|
-
FROM read_parquet({sources_sql}, union_by_name=true)
|
|
275
|
+
FROM read_parquet({sources_sql}, union_by_name=true, hive_partitioning=false)
|
|
264
276
|
{where}
|
|
265
277
|
"""
|
|
266
278
|
|
|
@@ -618,7 +630,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
618
630
|
f"""
|
|
619
631
|
SELECT coalesce(sum(n), 0), coalesce(sum(n) FILTER (WHERE n > 1), 0)
|
|
620
632
|
FROM (
|
|
621
|
-
SELECT count(*) AS n FROM read_parquet({_sql_path(output_file)})
|
|
633
|
+
SELECT count(*) AS n FROM read_parquet({_sql_path(output_file)}, hive_partitioning=false)
|
|
622
634
|
WHERE "id" IS NOT NULL GROUP BY {key}
|
|
623
635
|
)
|
|
624
636
|
"""
|
|
@@ -648,7 +660,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
648
660
|
AS VARCHAR)
|
|
649
661
|
ELSE CAST("id" AS VARCHAR)
|
|
650
662
|
END AS "id")
|
|
651
|
-
FROM read_parquet({_sql_path(output_file)}, file_row_number=true)
|
|
663
|
+
FROM read_parquet({_sql_path(output_file)}, file_row_number=true, hive_partitioning=false)
|
|
652
664
|
) TO ? (
|
|
653
665
|
FORMAT parquet,
|
|
654
666
|
ROW_GROUP_SIZE {row_group_size},
|
|
@@ -765,7 +777,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
765
777
|
columns.append(_sql_name(name))
|
|
766
778
|
per_part = per_part or coalesce or bool(constants)
|
|
767
779
|
query = f"SELECT {', '.join(columns)}"
|
|
768
|
-
source = f"read_parquet({_sql_path(path)})"
|
|
780
|
+
source = f"read_parquet({_sql_path(path)}, hive_partitioning=false)"
|
|
769
781
|
if constants:
|
|
770
782
|
# A registered table rather than SQL literals, so arrays, objects and
|
|
771
783
|
# temporal values keep their types
|
|
@@ -783,7 +795,7 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
783
795
|
# nothing to add to any part, so a single scan over all of them
|
|
784
796
|
sources = "[" + ",".join(_sql_path(path) for path in paths) + "]"
|
|
785
797
|
columns = ", ".join(_sql_name(name) for name in union)
|
|
786
|
-
source_query = f"SELECT {columns} FROM read_parquet({sources}, union_by_name=true)"
|
|
798
|
+
source_query = f"SELECT {columns} FROM read_parquet({sources}, union_by_name=true, hive_partitioning=false)"
|
|
787
799
|
targets = [row[0] for row in con.execute(f"DESCRIBE {source_query}").fetchall()]
|
|
788
800
|
|
|
789
801
|
return self.write_query(
|
|
@@ -850,10 +862,10 @@ class DuckDBBaseConverter(BaseConverter):
|
|
|
850
862
|
):
|
|
851
863
|
directory = os.path.dirname(output_file) or "."
|
|
852
864
|
tmp_path = None
|
|
853
|
-
query = f"SELECT d.* FROM read_parquet({_sql_path(output_file)}) d"
|
|
865
|
+
query = f"SELECT d.* FROM read_parquet({_sql_path(output_file)}, hive_partitioning=false) d"
|
|
854
866
|
if keys_path:
|
|
855
867
|
query += f"""
|
|
856
|
-
POSITIONAL JOIN read_parquet({_sql_path(keys_path)}) k
|
|
868
|
+
POSITIONAL JOIN read_parquet({_sql_path(keys_path)}, hive_partitioning=false) k
|
|
857
869
|
ORDER BY k.hilbert, k.ordinal
|
|
858
870
|
"""
|
|
859
871
|
try:
|
|
@@ -120,6 +120,17 @@ def _expected_content_length(f) -> Optional[int]:
|
|
|
120
120
|
return None
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def merge_headers(*headers: dict) -> dict:
|
|
124
|
+
"""Merge HTTP headers; names are case-insensitive, and a later one replaces an earlier."""
|
|
125
|
+
merged = {}
|
|
126
|
+
for group in headers:
|
|
127
|
+
for name, value in group.items():
|
|
128
|
+
for existing in [k for k in merged if k.lower() == name.lower()]:
|
|
129
|
+
del merged[existing]
|
|
130
|
+
merged[name] = value
|
|
131
|
+
return merged
|
|
132
|
+
|
|
133
|
+
|
|
123
134
|
def get_fs(url_or_path: Union[str, Path, URL], **kwargs) -> AbstractFileSystem:
|
|
124
135
|
"""Choose fsspec filesystem by sniffing input url"""
|
|
125
136
|
if isinstance(url_or_path, Path):
|
|
@@ -130,7 +141,7 @@ def get_fs(url_or_path: Union[str, Path, URL], **kwargs) -> AbstractFileSystem:
|
|
|
130
141
|
|
|
131
142
|
if parsed.scheme in ("http", "https"):
|
|
132
143
|
client_kwargs = kwargs.pop("client_kwargs", {})
|
|
133
|
-
headers = {"User-Agent": USER_AGENT,
|
|
144
|
+
headers = merge_headers({"User-Agent": USER_AGENT}, client_kwargs.get("headers", {}))
|
|
134
145
|
return HTTPFileSystem(client_kwargs={**client_kwargs, "headers": headers}, **kwargs)
|
|
135
146
|
|
|
136
147
|
if parsed.scheme == "s3":
|
|
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
|