portolan-python 0.1.6__tar.gz → 0.1.7__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.6
3
+ Version: 0.1.7
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.6
12
- git push origin v0.1.6
11
+ git tag v0.1.7
12
+ git push origin v0.1.7
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.6/portolan_python-0.1.6-py3-none-any.whl
28
+ https://github.com/jemacchi/portolan-python/releases/download/v0.1.7/portolan_python-0.1.7-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.6
38
+ python -m pip install portolan-python==0.1.7
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.6/portolan_python-0.1.6-py3-none-any.whl
13
+ https://github.com/jemacchi/portolan-python/releases/download/v0.1.7/portolan_python-0.1.7-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.6"
20
+ "portolan-python @ git+https://github.com/jemacchi/portolan-python.git@v0.1.7"
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.6"
3
+ version = "0.1.7"
4
4
  description = "A lightweight Python implementation of the Portolan specification."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import math
6
7
  import os
7
8
  import shutil
8
9
  import tempfile
@@ -31,6 +32,14 @@ class RegistryCatalogEntry:
31
32
  url: str
32
33
  title: str | None = None
33
34
  status: str | None = None
35
+ bbox: tuple[float, float, float, float] | None = None
36
+ licenses: tuple[str, ...] = ()
37
+ collection_count: int | None = None
38
+ feature_count: int | None = None
39
+ total_size_bytes: int | None = None
40
+ updated: str | None = None
41
+ logo_url: str | None = None
42
+ failure_reason: str | None = None
34
43
 
35
44
 
36
45
  def load_registry_entries(
@@ -58,12 +67,24 @@ def load_registry_entries(
58
67
  if catalog_ids is not None and registry_id not in catalog_ids:
59
68
  continue
60
69
  title = link.get("title")
70
+ licenses = link.get("portolan_registry:licenses")
71
+ logo = link.get("portolan_registry:logo")
61
72
  entries.append(
62
73
  RegistryCatalogEntry(
63
74
  id=registry_id,
64
75
  url=urljoin(registry_url, href),
65
76
  title=title if isinstance(title, str) else None,
66
77
  status=status if isinstance(status, str) else None,
78
+ bbox=_horizontal_bbox(link.get("bbox")),
79
+ licenses=tuple(sorted(key for key in licenses if isinstance(key, str)))
80
+ if isinstance(licenses, dict)
81
+ else (),
82
+ collection_count=_optional_int(link.get("portolan_registry:collection_count")),
83
+ feature_count=_optional_int(link.get("portolan_registry:feature_count")),
84
+ total_size_bytes=_optional_int(link.get("portolan_registry:total_size_bytes")),
85
+ updated=_optional_str(link.get("portolan_registry:updated")),
86
+ logo_url=_optional_str(logo.get("href")) if isinstance(logo, dict) else None,
87
+ failure_reason=_optional_str(link.get("portolan_registry:failure_reason")),
67
88
  )
68
89
  )
69
90
  if limit is not None and len(entries) >= limit:
@@ -71,6 +92,28 @@ def load_registry_entries(
71
92
  return entries
72
93
 
73
94
 
95
+ def _optional_int(value: object) -> int | None:
96
+ return value if isinstance(value, int) and not isinstance(value, bool) else None
97
+
98
+
99
+ def _optional_str(value: object) -> str | None:
100
+ return value if isinstance(value, str) and value else None
101
+
102
+
103
+ def _horizontal_bbox(value: object) -> tuple[float, float, float, float] | None:
104
+ if not isinstance(value, (list, tuple)) or len(value) < 4 or len(value) % 2:
105
+ return None
106
+ if not all(
107
+ isinstance(coordinate, (int, float))
108
+ and not isinstance(coordinate, bool)
109
+ and math.isfinite(coordinate)
110
+ for coordinate in value
111
+ ):
112
+ return None
113
+ half = len(value) // 2
114
+ return (float(value[0]), float(value[1]), float(value[half]), float(value[half + 1]))
115
+
116
+
74
117
  def download_registry_catalog(
75
118
  catalog_url: str,
76
119
  output_dir: Path,
@@ -40,8 +40,19 @@ def test_load_registry_entries_filters_to_valid_children() -> None:
40
40
  "rel": "child",
41
41
  "href": "https://example.test/a/catalog.json",
42
42
  "title": "A",
43
+ "bbox": [-71.0, -35.0, -70.0, -34.0],
43
44
  "portolan_registry:id": "catalog-a",
44
45
  "portolan_registry:status": "valid",
46
+ "portolan_registry:licenses": {"ODbL-1.0": 2, "CC-BY-4.0": 1},
47
+ "portolan_registry:collection_count": 3,
48
+ "portolan_registry:feature_count": 42,
49
+ "portolan_registry:total_size_bytes": 2048,
50
+ "portolan_registry:updated": "2026-10-08T00:00:00Z",
51
+ "portolan_registry:logo": {
52
+ "href": "https://example.test/a/logo.png",
53
+ "type": "image/png",
54
+ },
55
+ "portolan_registry:failure_reason": None,
45
56
  },
46
57
  {
47
58
  "rel": "child",
@@ -59,6 +70,48 @@ def test_load_registry_entries_filters_to_valid_children() -> None:
59
70
  assert [(entry.id, entry.url, entry.title, entry.status) for entry in entries] == [
60
71
  ("catalog-a", "https://example.test/a/catalog.json", "A", "valid")
61
72
  ]
73
+ assert entries[0].bbox == (-71.0, -35.0, -70.0, -34.0)
74
+ assert entries[0].licenses == ("CC-BY-4.0", "ODbL-1.0")
75
+ assert entries[0].collection_count == 3
76
+ assert entries[0].feature_count == 42
77
+ assert entries[0].total_size_bytes == 2048
78
+ assert entries[0].updated == "2026-10-08T00:00:00Z"
79
+ assert entries[0].logo_url == "https://example.test/a/logo.png"
80
+ assert entries[0].failure_reason is None
81
+
82
+
83
+ def test_load_registry_entries_ignores_malformed_optional_metadata() -> None:
84
+ registry = {
85
+ "links": [
86
+ {
87
+ "rel": "child",
88
+ "href": "https://example.test/a/catalog.json",
89
+ "bbox": [True, -35.0, -70.0, -34.0],
90
+ "portolan_registry:id": "catalog-a",
91
+ "portolan_registry:status": "valid",
92
+ "portolan_registry:licenses": ["not", "a", "mapping"],
93
+ "portolan_registry:collection_count": True,
94
+ "portolan_registry:feature_count": "many",
95
+ "portolan_registry:total_size_bytes": 1.5,
96
+ "portolan_registry:updated": 7,
97
+ "portolan_registry:logo": {"href": 9},
98
+ "portolan_registry:failure_reason": [],
99
+ }
100
+ ]
101
+ }
102
+
103
+ (entry,) = load_registry_entries(
104
+ "https://registry.test/catalogs.json", fetch_json=lambda url: registry
105
+ )
106
+
107
+ assert entry.bbox is None
108
+ assert entry.licenses == ()
109
+ assert entry.collection_count is None
110
+ assert entry.feature_count is None
111
+ assert entry.total_size_bytes is None
112
+ assert entry.updated is None
113
+ assert entry.logo_url is None
114
+ assert entry.failure_reason is None
62
115
 
63
116
 
64
117
  def test_load_registry_entries_resolves_relative_child_urls() -> None:
@@ -468,7 +468,7 @@ wheels = [
468
468
 
469
469
  [[package]]
470
470
  name = "portolan-python"
471
- version = "0.1.6"
471
+ version = "0.1.7"
472
472
  source = { editable = "." }
473
473
 
474
474
  [package.optional-dependencies]