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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: portolan-python
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A lightweight Python implementation of the Portolan specification.
5
5
  Author: Portolan contributors
6
6
  License: Apache-2.0
@@ -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.0
12
- git push origin v0.1.0
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.0/portolan_python-0.1.0-py3-none-any.whl
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.0
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.0/portolan_python-0.1.0-py3-none-any.whl
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.0"
20
+ "portolan-python @ git+https://github.com/jemacchi/portolan-python.git@v0.1.5"
21
21
  ```
22
22
 
23
23
  ## List collections and assets
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "portolan-python"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "A lightweight Python implementation of the Portolan specification."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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
- for link in document.get("links", []):
221
- if not isinstance(link, dict) or link.get("rel") != "child":
222
- continue
223
- href = link.get("href")
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"] == "./roads/collection.json"
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=None, fp=None)
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)
@@ -468,7 +468,7 @@ wheels = [
468
468
 
469
469
  [[package]]
470
470
  name = "portolan-python"
471
- version = "0.1.4"
471
+ version = "0.1.5"
472
472
  source = { editable = "." }
473
473
 
474
474
  [package.optional-dependencies]