ondio 0.2.3__tar.gz → 0.2.4__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.
- {ondio-0.2.3 → ondio-0.2.4}/PKG-INFO +1 -1
- {ondio-0.2.3 → ondio-0.2.4}/ondio/backends/aws.py +3 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/backends/gcs.py +3 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/backends/local.py +17 -7
- {ondio-0.2.3 → ondio-0.2.4}/ondio/dispatcher.py +5 -1
- {ondio-0.2.3 → ondio-0.2.4}/ondio/registry.py +3 -2
- {ondio-0.2.3 → ondio-0.2.4}/pyproject.toml +1 -1
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_aws_backend.py +21 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_gcs_backend.py +11 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_local_dispatcher.py +47 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_registry.py +36 -1
- {ondio-0.2.3 → ondio-0.2.4}/.github/workflows/publish.yml +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/.github/workflows/tests.yml +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/.gitignore +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/LICENSE.md +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/README.md +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/docs/architecture.svg +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/__init__.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/_log.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/backends/http.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/backends/protocol.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/flac.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/parquet.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/ondio/types.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/conftest.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_download_files.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_flac.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_http_backend.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_log.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_parquet.py +0 -0
- {ondio-0.2.3 → ondio-0.2.4}/tests/unit/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ondio
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Uniform IO of audio data and bioacoustic model results across S3, GCS, HTTP, and local filesystems
|
|
5
5
|
Author-email: Michael Catchen <mdcatchen@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -18,6 +18,9 @@ def _split(uri: str) -> tuple[str, str]:
|
|
|
18
18
|
parsed = urlparse(uri)
|
|
19
19
|
if parsed.scheme != "s3" or not parsed.netloc:
|
|
20
20
|
raise ValueError(f"not an s3:// URI: {uri!r}")
|
|
21
|
+
# urlparse would drop the text after ? or #, so the key would not be the one given.
|
|
22
|
+
if "?" in uri or "#" in uri:
|
|
23
|
+
raise ValueError(f"s3:// URIs with a query or fragment are not supported: {uri!r}")
|
|
21
24
|
return parsed.netloc, parsed.path.lstrip("/")
|
|
22
25
|
|
|
23
26
|
|
|
@@ -15,6 +15,9 @@ def _split(uri: str) -> tuple[str, str]:
|
|
|
15
15
|
parsed = urlparse(uri)
|
|
16
16
|
if parsed.scheme != "gs" or not parsed.netloc:
|
|
17
17
|
raise ValueError(f"not a gs:// URI: {uri!r}")
|
|
18
|
+
# urlparse would drop the text after ? or #, so the key would not be the one given.
|
|
19
|
+
if "?" in uri or "#" in uri:
|
|
20
|
+
raise ValueError(f"gs:// URIs with a query or fragment are not supported: {uri!r}")
|
|
18
21
|
return parsed.netloc, parsed.path.lstrip("/")
|
|
19
22
|
|
|
20
23
|
|
|
@@ -12,13 +12,23 @@ from urllib.parse import unquote, urlparse
|
|
|
12
12
|
from ondio.backends.protocol import _matches_filters
|
|
13
13
|
from ondio.types import AuthError, ObjectExistsError, ObjectNotFoundError, OndioError
|
|
14
14
|
|
|
15
|
+
def _is_file_uri(uri: str) -> bool:
|
|
16
|
+
return urlparse(uri).scheme.lower() == "file"
|
|
17
|
+
|
|
18
|
+
|
|
15
19
|
def _to_path(uri: str) -> Path:
|
|
16
|
-
if uri
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
if not _is_file_uri(uri):
|
|
21
|
+
return Path(uri)
|
|
22
|
+
# The forms below would be read as a different path: urlparse drops a query or
|
|
23
|
+
# fragment, and without `//` the rest of the URI is a relative path.
|
|
24
|
+
if not uri.lower().startswith("file://"):
|
|
25
|
+
raise ValueError(f"file: URIs must start with file://: {uri!r}")
|
|
26
|
+
if "?" in uri or "#" in uri:
|
|
27
|
+
raise ValueError(f"file:// URIs with a query or fragment are not supported: {uri!r}")
|
|
28
|
+
parsed = urlparse(uri)
|
|
29
|
+
if parsed.netloc not in ("", "localhost"):
|
|
30
|
+
raise ValueError(f"file:// URIs with a remote host are not supported: {uri!r}")
|
|
31
|
+
return Path(unquote(parsed.path))
|
|
22
32
|
|
|
23
33
|
def _wrap_oserror(uri: str, exc: OSError) -> Exception:
|
|
24
34
|
if isinstance(exc, PermissionError):
|
|
@@ -118,7 +128,7 @@ class LocalBackend:
|
|
|
118
128
|
required_prefix: str | None = None,
|
|
119
129
|
required_ext: str | None = None,
|
|
120
130
|
) -> list[str]:
|
|
121
|
-
as_file_uri = uri_prefix
|
|
131
|
+
as_file_uri = _is_file_uri(uri_prefix)
|
|
122
132
|
matches = (
|
|
123
133
|
p
|
|
124
134
|
for p in self._iter_matches(uri_prefix, recursive=recursive)
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
Every function accepts `**kwargs`, forwarded to the backend constructor (see
|
|
4
4
|
[`get_backend`][ondio.registry.get_backend]) — not to the individual operation.
|
|
5
5
|
URIs are fully qualified: `s3://…`, `gs://…`, `http(s)://…`, `file://…`, or a
|
|
6
|
-
plain local path.
|
|
6
|
+
plain local path. A `file:`, `s3://` or `gs://` URI with `?` or `#` is refused with
|
|
7
|
+
`ValueError`, since the text after it would be dropped. A local file whose name has
|
|
8
|
+
`?` or `#` is reached with `%3F` and `%23` in a `file://` URI, or with a plain local
|
|
9
|
+
path, which is used as written. S3 and GCS keys are not decoded, so a key with `?`
|
|
10
|
+
or `#` cannot be reached.
|
|
7
11
|
"""
|
|
8
12
|
|
|
9
13
|
from __future__ import annotations
|
|
@@ -49,8 +49,9 @@ def locate(uri: str) -> Location:
|
|
|
49
49
|
are used as written.
|
|
50
50
|
|
|
51
51
|
Raises:
|
|
52
|
-
ValueError: For a relative local path, a `file
|
|
53
|
-
|
|
52
|
+
ValueError: For a relative local path, a `file:` URI that does not start
|
|
53
|
+
with `file://`, a `file://` URI with a remote host, an `s3://` or `gs://`
|
|
54
|
+
URI with no bucket, or a `file:`, `s3://` or `gs://` URI with `?` or `#`.
|
|
54
55
|
UnsupportedOperationError: For an HTTP URL, which ondio cannot write.
|
|
55
56
|
UnknownPlatformError: For an unknown scheme.
|
|
56
57
|
"""
|
|
@@ -13,7 +13,7 @@ license = "BSD-3-Clause"
|
|
|
13
13
|
license-files = ["LICENSE.md"]
|
|
14
14
|
authors = [{ name = "Michael Catchen", email = "mdcatchen@gmail.com" }]
|
|
15
15
|
requires-python = ">= 3.11"
|
|
16
|
-
version = "0.2.
|
|
16
|
+
version = "0.2.4"
|
|
17
17
|
|
|
18
18
|
[project.optional-dependencies]
|
|
19
19
|
gcs = ["google-cloud-storage"]
|
|
@@ -292,3 +292,24 @@ def test_an_s3_location_is_the_key_ondio_writes(prefix):
|
|
|
292
292
|
client = boto3.client("s3", region_name="us-east-1")
|
|
293
293
|
body = client.get_object(Bucket=store.removeprefix("s3://"), Key=key)["Body"].read()
|
|
294
294
|
assert body == b"payload"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestRefusedUris:
|
|
298
|
+
@pytest.fixture(params=["?b", "#b", "?", "#"])
|
|
299
|
+
def uri(self, prefix, request):
|
|
300
|
+
return f"{prefix}/a{request.param}"
|
|
301
|
+
|
|
302
|
+
def test_read_refuses_a_query_or_fragment(self, prefix, uri):
|
|
303
|
+
ondio.write(f"{prefix}/a", b"payload")
|
|
304
|
+
with pytest.raises(ValueError):
|
|
305
|
+
ondio.read(uri)
|
|
306
|
+
|
|
307
|
+
@pytest.mark.parametrize("operation", [ondio.create, ondio.write])
|
|
308
|
+
def test_writes_refuse_a_query_or_fragment(self, prefix, uri, operation):
|
|
309
|
+
with pytest.raises(ValueError):
|
|
310
|
+
operation(uri, b"payload")
|
|
311
|
+
assert ondio.list_files(f"{prefix}/") == []
|
|
312
|
+
|
|
313
|
+
def test_an_encoded_question_mark_is_part_of_the_key(self, prefix):
|
|
314
|
+
ondio.write(f"{prefix}/a%3Fb", b"payload")
|
|
315
|
+
assert ondio.list_files(f"{prefix}/") == [f"{prefix}/a%3Fb"]
|
|
@@ -268,3 +268,14 @@ class TestErrors:
|
|
|
268
268
|
def test_create_is_unsupported(backend):
|
|
269
269
|
with pytest.raises(UnsupportedOperationError):
|
|
270
270
|
backend.create(f"{PREFIX}/result.json", b"x")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@pytest.mark.parametrize("suffix", ["?b", "#b", "?", "#"])
|
|
274
|
+
def test_a_query_or_fragment_is_refused(backend, store, suffix):
|
|
275
|
+
store["a"] = b"payload"
|
|
276
|
+
uri = f"{PREFIX}/a{suffix}"
|
|
277
|
+
with pytest.raises(ValueError):
|
|
278
|
+
backend.read(uri)
|
|
279
|
+
with pytest.raises(ValueError):
|
|
280
|
+
backend.write(uri, b"other")
|
|
281
|
+
assert store["a"] == b"payload"
|
|
@@ -63,6 +63,53 @@ class TestReadWrite:
|
|
|
63
63
|
assert ondio.locate(uri) == Location("file://", str(target).removeprefix("/"))
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
|
|
67
|
+
class TestRefusedUris:
|
|
68
|
+
@pytest.fixture(params=["?x", "#x", "?", "#"])
|
|
69
|
+
def suffix(self, request):
|
|
70
|
+
return request.param
|
|
71
|
+
|
|
72
|
+
def test_read_refuses_a_query_or_fragment(self, tmp_path, suffix):
|
|
73
|
+
target = tmp_path / "out"
|
|
74
|
+
target.write_bytes(b"payload")
|
|
75
|
+
with pytest.raises(ValueError):
|
|
76
|
+
ondio.read(target.as_uri() + suffix)
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize("operation", [ondio.create, ondio.write])
|
|
79
|
+
def test_writes_refuse_a_query_or_fragment(self, tmp_path, suffix, operation):
|
|
80
|
+
with pytest.raises(ValueError):
|
|
81
|
+
operation((tmp_path / "out").as_uri() + suffix, b"payload")
|
|
82
|
+
assert list(tmp_path.iterdir()) == []
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize("scheme", ["file:", "FILE:"])
|
|
85
|
+
def test_a_file_uri_without_two_slashes_is_refused(self, tmp_path, monkeypatch, scheme):
|
|
86
|
+
monkeypatch.chdir(tmp_path)
|
|
87
|
+
uri = scheme + str(tmp_path / "out")
|
|
88
|
+
for operation in (ondio.read, ondio.locate):
|
|
89
|
+
with pytest.raises(ValueError):
|
|
90
|
+
operation(uri)
|
|
91
|
+
for operation in (ondio.create, ondio.write):
|
|
92
|
+
with pytest.raises(ValueError):
|
|
93
|
+
operation(uri, b"payload")
|
|
94
|
+
assert list(tmp_path.iterdir()) == []
|
|
95
|
+
|
|
96
|
+
def test_a_bare_path_keeps_question_mark_and_hash(self, tmp_path):
|
|
97
|
+
target = tmp_path / "a?b#c"
|
|
98
|
+
ondio.create(str(target), b"payload")
|
|
99
|
+
assert target.read_bytes() == b"payload"
|
|
100
|
+
assert ondio.read(str(target)) == b"payload"
|
|
101
|
+
assert ondio.locate(str(target)) == Location("file://", str(target).removeprefix("/"))
|
|
102
|
+
|
|
103
|
+
def test_an_encoded_question_mark_reaches_the_file(self, tmp_path):
|
|
104
|
+
target = tmp_path / "a?b"
|
|
105
|
+
target.write_bytes(b"payload")
|
|
106
|
+
assert ondio.read(target.as_uri()) == b"payload"
|
|
107
|
+
|
|
108
|
+
def test_an_uppercase_scheme_lists_file_uris(self, tree):
|
|
109
|
+
uri = "FILE://" + tree.as_uri().removeprefix("file://")
|
|
110
|
+
assert ondio.list_files(uri) == ondio.list_files(tree.as_uri())
|
|
111
|
+
|
|
112
|
+
|
|
66
113
|
class TestReadRange:
|
|
67
114
|
@pytest.fixture
|
|
68
115
|
def uri(self, tmp_path):
|
|
@@ -75,6 +75,14 @@ def test_create_refuses_unsupported_schemes_before_backend_setup(uri, setup_erro
|
|
|
75
75
|
("file:///data/%6fut", Location("file://", "data/out")),
|
|
76
76
|
("file:///data/%2e%2e/other", Location("file://", "data/../other")),
|
|
77
77
|
("file:///data/a%2F..%2Fother", Location("file://", "data/a/../other")),
|
|
78
|
+
("file:///data/a%20b", Location("file://", "data/a b")),
|
|
79
|
+
# A ? or # reaches a local file name only percent-encoded, or in a bare path.
|
|
80
|
+
("file:///data/a%3Fb%23c", Location("file://", "data/a?b#c")),
|
|
81
|
+
("FILE:///data/out", Location("file://", "data/out")),
|
|
82
|
+
("/data/a?b#c", Location("file://", "data/a?b#c")),
|
|
83
|
+
# The same text in a cloud key stays as written.
|
|
84
|
+
("s3://bucket/a%3Fb", Location("s3://bucket", "a%3Fb")),
|
|
85
|
+
("gs://bucket/a%3Fb", Location("gs://bucket", "a%3Fb")),
|
|
78
86
|
],
|
|
79
87
|
)
|
|
80
88
|
def test_locate_names_the_store_and_the_path_ondio_reads(uri, location):
|
|
@@ -99,7 +107,9 @@ def test_forms_of_one_local_folder_locate_the_same_place(uri):
|
|
|
99
107
|
("uri", "error"),
|
|
100
108
|
[
|
|
101
109
|
("relative/path", ValueError),
|
|
102
|
-
("file:/data/out", ValueError),
|
|
110
|
+
("file:/data/out", ValueError),
|
|
111
|
+
("file:data/out", ValueError),
|
|
112
|
+
("FILE:/data/out", ValueError),
|
|
103
113
|
("file://host/data/out", ValueError),
|
|
104
114
|
("s3:///key", ValueError),
|
|
105
115
|
("gs:///key", ValueError),
|
|
@@ -110,3 +120,28 @@ def test_forms_of_one_local_folder_locate_the_same_place(uri):
|
|
|
110
120
|
def test_locate_refuses_a_location_with_no_fixed_place(uri, error):
|
|
111
121
|
with pytest.raises(error):
|
|
112
122
|
locate(uri)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"uri",
|
|
127
|
+
[
|
|
128
|
+
"file:///data/out?x",
|
|
129
|
+
"file:///data/out#x",
|
|
130
|
+
"file:///data/out?",
|
|
131
|
+
"file:///data/out#",
|
|
132
|
+
"file://localhost/data/out?x",
|
|
133
|
+
"FILE:///data/out?x",
|
|
134
|
+
"s3://bucket/a?b",
|
|
135
|
+
"s3://bucket/a#b",
|
|
136
|
+
"s3://bucket/a?",
|
|
137
|
+
"s3://bucket/a#",
|
|
138
|
+
"gs://bucket/a?b",
|
|
139
|
+
"gs://bucket/a#b",
|
|
140
|
+
"gs://bucket/a?",
|
|
141
|
+
"gs://bucket/a#",
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
def test_locate_refuses_a_query_or_fragment(uri):
|
|
145
|
+
# urlparse would drop the text after ? or #, so ondio would name another path.
|
|
146
|
+
with pytest.raises(ValueError):
|
|
147
|
+
locate(uri)
|
|
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
|