portolan-python 0.1.4__tar.gz → 0.1.5__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.
- {portolan_python-0.1.4 → portolan_python-0.1.5}/.coverage +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/PKG-INFO +1 -1
- {portolan_python-0.1.4 → portolan_python-0.1.5}/docs/distribution.md +4 -4
- {portolan_python-0.1.4 → portolan_python-0.1.5}/docs/examples.md +2 -2
- {portolan_python-0.1.4 → portolan_python-0.1.5}/pyproject.toml +1 -1
- {portolan_python-0.1.4 → portolan_python-0.1.5}/src/portolan/registry.py +46 -24
- {portolan_python-0.1.4 → portolan_python-0.1.5}/tests/test_registry_api.py +44 -2
- {portolan_python-0.1.4 → portolan_python-0.1.5}/uv.lock +1 -1
- {portolan_python-0.1.4 → portolan_python-0.1.5}/.github/workflows/release.yml +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/.gitignore +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/Makefile +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/README.md +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/docs/development.md +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/docs/scope.md +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/src/portolan/__init__.py +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/src/portolan/catalog.py +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/src/portolan/py.typed +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/src/portolan/validation.py +0 -0
- {portolan_python-0.1.4 → portolan_python-0.1.5}/tests/test_catalog_api.py +0 -0
|
Binary file
|
|
@@ -8,8 +8,8 @@ preferred package index for published versions.
|
|
|
8
8
|
Push a tag that matches the version in `pyproject.toml`:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
git tag v0.1.
|
|
12
|
-
git push origin v0.1.
|
|
11
|
+
git tag v0.1.5
|
|
12
|
+
git push origin v0.1.5
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
The release workflow runs the tests, builds both distributions, checks their
|
|
@@ -25,7 +25,7 @@ Install the wheel attached to a release:
|
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
27
|
python -m pip install \
|
|
28
|
-
https://github.com/jemacchi/portolan-python/releases/download/v0.1.
|
|
28
|
+
https://github.com/jemacchi/portolan-python/releases/download/v0.1.5/portolan_python-0.1.5-py3-none-any.whl
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
This path needs no package index account.
|
|
@@ -35,7 +35,7 @@ This path needs no package index account.
|
|
|
35
35
|
Install a published version from PyPI with:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
python -m pip install portolan-python==0.1.
|
|
38
|
+
python -m pip install portolan-python==0.1.5
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Omit the version constraint to install the latest published version.
|
|
@@ -10,14 +10,14 @@ GitHub:
|
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
python -m pip install \
|
|
13
|
-
https://github.com/jemacchi/portolan-python/releases/download/v0.1.
|
|
13
|
+
https://github.com/jemacchi/portolan-python/releases/download/v0.1.5/portolan_python-0.1.5-py3-none-any.whl
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
You can also install the source at a specific tag:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
python -m pip install \
|
|
20
|
-
"portolan-python @ git+https://github.com/jemacchi/portolan-python.git@v0.1.
|
|
20
|
+
"portolan-python @ git+https://github.com/jemacchi/portolan-python.git@v0.1.5"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## List collections and assets
|
|
@@ -215,32 +215,54 @@ def _write_catalog_tree(
|
|
|
215
215
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
216
216
|
if document.get("type") == "Collection":
|
|
217
217
|
document = _with_absolute_asset_hrefs(document_url, document)
|
|
218
|
+
|
|
219
|
+
links = document.get("links")
|
|
220
|
+
if isinstance(links, list):
|
|
221
|
+
rewritten_links: list[Any] = []
|
|
222
|
+
for link in links:
|
|
223
|
+
rewritten_link = link
|
|
224
|
+
if isinstance(link, dict) and link.get("rel") == "child":
|
|
225
|
+
href = link.get("href")
|
|
226
|
+
if isinstance(href, str):
|
|
227
|
+
child_url = urljoin(document_url, href)
|
|
228
|
+
child_target = _target_document_path(root_url, child_url, output_root)
|
|
229
|
+
owner = targets.get(child_target)
|
|
230
|
+
if owner is not None and owner != child_url:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"Registry documents map to the same local path: "
|
|
233
|
+
f"{owner}, {child_url}"
|
|
234
|
+
)
|
|
235
|
+
if child_url in visited:
|
|
236
|
+
if owner == child_url:
|
|
237
|
+
rewritten_link = {
|
|
238
|
+
**link,
|
|
239
|
+
"href": _relative_local_href(target, child_target),
|
|
240
|
+
}
|
|
241
|
+
else:
|
|
242
|
+
child = fetch_json(child_url)
|
|
243
|
+
if child.get("type") in {"Catalog", "Collection"}:
|
|
244
|
+
_write_catalog_tree(
|
|
245
|
+
child_url,
|
|
246
|
+
child,
|
|
247
|
+
root_url,
|
|
248
|
+
output_root,
|
|
249
|
+
fetch_json,
|
|
250
|
+
visited,
|
|
251
|
+
targets,
|
|
252
|
+
)
|
|
253
|
+
rewritten_link = {
|
|
254
|
+
**link,
|
|
255
|
+
"href": _relative_local_href(target, child_target),
|
|
256
|
+
}
|
|
257
|
+
rewritten_links.append(rewritten_link)
|
|
258
|
+
document = {**document, "links": rewritten_links}
|
|
259
|
+
|
|
218
260
|
target.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
|
|
219
261
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if not isinstance(href, str):
|
|
225
|
-
continue
|
|
226
|
-
child_url = urljoin(document_url, href)
|
|
227
|
-
if child_url in visited:
|
|
228
|
-
continue
|
|
229
|
-
child_target = _target_document_path(root_url, child_url, output_root)
|
|
230
|
-
owner = targets.get(child_target)
|
|
231
|
-
if owner is not None and owner != child_url:
|
|
232
|
-
raise ValueError(f"Registry documents map to the same local path: {owner}, {child_url}")
|
|
233
|
-
child = fetch_json(child_url)
|
|
234
|
-
if child.get("type") in {"Catalog", "Collection"}:
|
|
235
|
-
_write_catalog_tree(
|
|
236
|
-
child_url,
|
|
237
|
-
child,
|
|
238
|
-
root_url,
|
|
239
|
-
output_root,
|
|
240
|
-
fetch_json,
|
|
241
|
-
visited,
|
|
242
|
-
targets,
|
|
243
|
-
)
|
|
262
|
+
|
|
263
|
+
def _relative_local_href(parent_target: Path, child_target: Path) -> str:
|
|
264
|
+
relative_path = os.path.relpath(child_target, start=parent_target.parent)
|
|
265
|
+
return Path(relative_path).as_posix()
|
|
244
266
|
|
|
245
267
|
|
|
246
268
|
def _with_absolute_asset_hrefs(document_url: str, collection: JsonObject) -> JsonObject:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
from email.message import Message
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Any
|
|
8
9
|
from urllib.error import HTTPError
|
|
@@ -188,10 +189,51 @@ def test_download_registry_catalog_writes_snapshot_with_absolute_asset_hrefs(
|
|
|
188
189
|
collection = json.loads(
|
|
189
190
|
(catalog_root / "roads" / "collection.json").read_text(encoding="utf-8")
|
|
190
191
|
)
|
|
191
|
-
assert catalog["links"][0]["href"] == "
|
|
192
|
+
assert catalog["links"][0]["href"] == "roads/collection.json"
|
|
192
193
|
assert collection["assets"]["data"]["href"] == "https://example.test/demo/roads/roads.parquet"
|
|
193
194
|
|
|
194
195
|
|
|
196
|
+
def test_download_registry_catalog_rewrites_downloaded_children_to_local_hrefs(
|
|
197
|
+
tmp_path: Path,
|
|
198
|
+
) -> None:
|
|
199
|
+
root_url = "https://example.test/demo/catalog.json"
|
|
200
|
+
child_url = "https://example.test/demo/roads/collection.json"
|
|
201
|
+
item_url = "https://example.test/demo/roads/item.json"
|
|
202
|
+
responses = {
|
|
203
|
+
root_url: {
|
|
204
|
+
"type": "Catalog",
|
|
205
|
+
"id": "demo",
|
|
206
|
+
"links": [
|
|
207
|
+
{"rel": "self", "href": root_url},
|
|
208
|
+
{"rel": "child", "href": child_url},
|
|
209
|
+
{"rel": "child", "href": item_url},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
child_url: {
|
|
213
|
+
**_collection("roads", {}),
|
|
214
|
+
"links": [{"rel": "child", "href": root_url}],
|
|
215
|
+
},
|
|
216
|
+
item_url: {"type": "Feature", "id": "road-1"},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
catalog_root = download_registry_catalog(
|
|
220
|
+
root_url,
|
|
221
|
+
tmp_path,
|
|
222
|
+
fetch_json=lambda url: responses[url],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
catalog = json.loads((catalog_root / "catalog.json").read_text(encoding="utf-8"))
|
|
226
|
+
collection = json.loads(
|
|
227
|
+
(catalog_root / "roads" / "collection.json").read_text(encoding="utf-8")
|
|
228
|
+
)
|
|
229
|
+
assert catalog["links"] == [
|
|
230
|
+
{"rel": "self", "href": root_url},
|
|
231
|
+
{"rel": "child", "href": "roads/collection.json"},
|
|
232
|
+
{"rel": "child", "href": item_url},
|
|
233
|
+
]
|
|
234
|
+
assert collection["links"] == [{"rel": "child", "href": "../catalog.json"}]
|
|
235
|
+
|
|
236
|
+
|
|
195
237
|
def test_download_registry_catalog_skips_child_cycles(tmp_path: Path) -> None:
|
|
196
238
|
root_url = "https://example.test/demo/catalog.json"
|
|
197
239
|
child_url = "https://example.test/demo/roads/collection.json"
|
|
@@ -324,7 +366,7 @@ def test_download_registry_catalog_preserves_snapshot_when_child_fetch_fails(
|
|
|
324
366
|
"id": "demo",
|
|
325
367
|
"links": [{"rel": "child", "href": "./missing.json"}],
|
|
326
368
|
}
|
|
327
|
-
raise HTTPError(url, 503, "Unavailable", hdrs=
|
|
369
|
+
raise HTTPError(url, 503, "Unavailable", hdrs=Message(), fp=None)
|
|
328
370
|
|
|
329
371
|
with pytest.raises(HTTPError):
|
|
330
372
|
download_registry_catalog(root_url, tmp_path, fetch_json=fetch_json)
|
|
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
|