Skip to content

Commit a75e52d

Browse files
authored
Merge pull request #70 from d-v-b/feat/geoproj-extension
feat/geoproj extension
2 parents e1b8bca + 2c78bb4 commit a75e52d

File tree

8 files changed

+249
-9
lines changed

8 files changed

+249
-9
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Models for the GeoProj Zarr Convention
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing import Literal, Self, TypeGuard
8+
9+
from pydantic import BaseModel, Field, model_validator
10+
from typing_extensions import TypedDict
11+
12+
from eopf_geozarr.data_api.geozarr.projjson import ProjJSON
13+
14+
15+
def is_none(data: object) -> TypeGuard[None]:
16+
return data is None
17+
18+
19+
GEO_PROJ_UUID: Literal["f17cb550-5864-4468-aeb7-f3180cfb622f"] = (
20+
"f17cb550-5864-4468-aeb7-f3180cfb622f"
21+
)
22+
23+
24+
class GeoProjConvention(TypedDict):
25+
version: Literal["0.1.0"]
26+
schema: Literal[
27+
"https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v0.1.0/schema.json"
28+
]
29+
name: Literal["geo-proj"]
30+
description: Literal["Coordinate reference system information for geospatial data"]
31+
spec: Literal["https://github.com/zarr-experimental/geo-proj/blob/v0.1.0/README.md"]
32+
33+
34+
GeoProjConventions = TypedDict( # type: ignore[misc]
35+
"GeoProjConventions", {GEO_PROJ_UUID: GeoProjConvention}, closed=False
36+
)
37+
38+
39+
class GeoProj(BaseModel):
40+
code: str | None = Field(None, alias="proj:code", exclude_if=is_none)
41+
wkt2: str | None = Field(None, alias="proj:wkt2", exclude_if=is_none)
42+
projjson: ProjJSON | None = Field(None, alias="proj:projjson", exclude_if=is_none)
43+
spatial_dimensions: tuple[str, str] = Field(alias="proj:spatial_dimensions")
44+
transform: tuple[float, float, float, float, float, float] | None = Field(
45+
None, alias="proj:transform", exclude_if=is_none
46+
)
47+
bbox: tuple[float, float, float, float] | None = Field(
48+
None, alias="proj:bbox", exclude_if=is_none
49+
)
50+
shape: tuple[int, int] | None = Field(None, alias="proj:shape", exclude_if=is_none)
51+
52+
model_config = {"extra": "allow", "serialize_by_alias": True}
53+
54+
@model_validator(mode="after")
55+
def ensure_required_conditional_attributes(self) -> Self:
56+
if self.code is None and self.wkt2 is None and self.projjson is None:
57+
raise ValueError("One of 'code', 'wkt2', or 'projjson' must be provided.")
58+
return self

src/eopf_geozarr/data_api/geozarr/multiscales.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22

33
from typing import Literal, Self
44

5-
from pydantic import BaseModel, Field, model_validator
5+
from pydantic import BaseModel, model_validator
66
from pydantic.experimental.missing_sentinel import MISSING
77
from typing_extensions import TypedDict
88

99
ConventionID = Literal["d35379db-88df-4056-af3a-620245f8e347"]
1010

1111

12-
class MultiscaleConvention(BaseModel):
12+
class MultiscaleConvention(TypedDict):
1313
version: Literal["0.1.0"]
14-
schema_url: Literal[
14+
schema: Literal[
1515
"https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v0.1.0/schema.json"
16-
] = Field(alias="schema")
17-
name: Literal["multiscales"] = "multiscales"
18-
description: str = "Multiscale layout of zarr datasets"
19-
spec: Literal[
20-
"https://github.com/zarr-conventions/geo-proj/blob/v0.1.0/README.md"
21-
] = "https://github.com/zarr-conventions/geo-proj/blob/v0.1.0/README.md"
16+
]
17+
name: Literal["multiscales"]
18+
description: Literal["Multiscale layout of zarr datasets"]
19+
spec: Literal["https://github.com/zarr-conventions/geo-proj/blob/v0.1.0/README.md"]
2220

2321

2422
MultiscaleConventions = TypedDict( # type: ignore[misc]

tests/test_data_api/conftest.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import difflib
34
import json
45
import re
56
from pathlib import Path
@@ -201,3 +202,41 @@ def _load_multiscales_examples() -> dict[str, dict[str, object]]:
201202

202203

203204
MULTISCALES_EXAMPLES = _load_multiscales_examples()
205+
206+
207+
def _load_json_examples(
208+
*, prefix: Path, glob_str: str = "*.json"
209+
) -> dict[str, dict[str, object]]:
210+
"""
211+
Loads JSON examples from a prefix / directory. By default all files ending with .json are collected.
212+
"""
213+
return {path.name: json.loads(path.read_text()) for path in prefix.glob(glob_str)}
214+
215+
216+
GEOPROJ_EXAMPLES = _load_json_examples(
217+
prefix=Path(__file__).parent / "geoproj_examples"
218+
)
219+
220+
221+
def view_json_diff(
222+
a: dict[str, object],
223+
b: dict[str, object],
224+
*,
225+
sort_keys: bool = True,
226+
indent: int = 2,
227+
) -> str:
228+
"""
229+
Generate a human-readable diff between two JSON objects
230+
"""
231+
a_str = json.dumps(a, indent=indent, sort_keys=sort_keys)
232+
b_str = json.dumps(b, indent=indent, sort_keys=sort_keys)
233+
234+
# difflib.unified_diff returns an iterable of lines
235+
diff = difflib.unified_diff(
236+
a_str.splitlines(keepends=True),
237+
b_str.splitlines(keepends=True),
238+
fromfile="expected",
239+
tofile="actual",
240+
lineterm="",
241+
)
242+
return "".join(diff)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"zarr_format": 3,
3+
"node_type": "array",
4+
"attributes": {
5+
"zarr_conventions_version": "0.1.0",
6+
"zarr_conventions": {
7+
"f17cb550-5864-4468-aeb7-f3180cfb622f": {
8+
"version": "0.1.0",
9+
"schema": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v0.1.0/schema.json",
10+
"name": "geo-proj",
11+
"description": "Coordinate reference system information for geospatial data",
12+
"spec": "https://github.com/zarr-experimental/geo-proj/blob/v0.1.0/README.md"
13+
}
14+
},
15+
"proj:code": "EPSG:26711",
16+
"proj:spatial_dimensions": ["Y", "X"],
17+
"proj:transform": [
18+
60.0,
19+
0.0,
20+
440720.0,
21+
0.0,
22+
-60.0,
23+
3750120.0
24+
]
25+
}
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"zarr_format": 3,
3+
"node_type": "array",
4+
"attributes": {
5+
"zarr_conventions_version": "0.1.0",
6+
"zarr_conventions": {
7+
"f17cb550-5864-4468-aeb7-f3180cfb622f": {
8+
"version": "0.1.0",
9+
"schema": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v0.1.0/schema.json",
10+
"name": "geo-proj",
11+
"description": "Coordinate reference system information for geospatial data",
12+
"spec": "https://github.com/zarr-experimental/geo-proj/blob/v0.1.0/README.md"
13+
}
14+
},
15+
"proj:code": "EPSG:3857",
16+
"proj:spatial_dimensions": ["Y", "X"],
17+
"proj:bbox": [
18+
-20037508.342789244,
19+
-20037508.342789244,
20+
20037508.342789244,
21+
20037508.342789244
22+
],
23+
"proj:transform": [
24+
156543.03392804097,
25+
0.0,
26+
-20037508.342789244,
27+
0.0,
28+
-156543.03392804097,
29+
20037508.342789244
30+
]
31+
}
32+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"zarr_format": 3,
3+
"node_type": "group",
4+
"attributes": {
5+
"zarr_conventions_version": "0.1.0",
6+
"zarr_conventions": {
7+
"d35379db-88df-4056-af3a-620245f8e347": {
8+
"version": "0.1.0",
9+
"schema": "https://raw.githubusercontent.com/zarr-experimental/multiscales/refs/tags/v0.1.0/schema.json",
10+
"name": "multiscales",
11+
"description": "Multiscale layout of zarr datasets",
12+
"spec": "https://github.com/zarr-experimental/multiscales/blob/v0.1.0/README.md"
13+
},
14+
"f17cb550-5864-4468-aeb7-f3180cfb622f": {
15+
"version": "0.1.0",
16+
"schema": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v0.1.0/schema.json",
17+
"name": "geo-proj",
18+
"description": "Coordinate reference system information for geospatial data",
19+
"spec": "https://github.com/zarr-experimental/geo-proj/blob/v0.1.0/README.md"
20+
}
21+
},
22+
"multiscales": {
23+
"layout": [
24+
{
25+
"group": "r10m",
26+
"proj:shape": [1200, 1200],
27+
"proj:transform": [10.0, 0.0, 500000.0, 0.0, -10.0, 5000000.0]
28+
},
29+
{
30+
"group": "r20m",
31+
"from_group": "r10m",
32+
"scale": [2.0, 2.0],
33+
"proj:shape": [600, 600],
34+
"proj:transform": [20.0, 0.0, 500000.0, 0.0, -20.0, 5000000.0]
35+
},
36+
{
37+
"group": "r60m",
38+
"from_group": "r10m",
39+
"scale": [6.0, 6.0],
40+
"proj:shape": [200, 200],
41+
"proj:transform": [60.0, 0.0, 500000.0, 0.0, -60.0, 5000000.0]
42+
}
43+
]
44+
},
45+
"proj:code": "EPSG:32633",
46+
"proj:spatial_dimensions": ["Y", "X"],
47+
"proj:bbox": [500000.0, 4900000.0, 600000.0, 5000000.0]
48+
}
49+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"zarr_format": 3,
3+
"node_type": "array",
4+
"attributes": {
5+
"zarr_conventions_version": "0.1.0",
6+
"zarr_conventions": {
7+
"f17cb550-5864-4468-aeb7-f3180cfb622f": {
8+
"version": "0.1.0",
9+
"schema": "https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v0.1.0/schema.json",
10+
"name": "geo-proj",
11+
"description": "Coordinate reference system information for geospatial data",
12+
"spec": "https://github.com/zarr-experimental/geo-proj/blob/v0.1.0/README.md"
13+
}
14+
},
15+
"proj:wkt2": "PROJCRS[\"WGS 84 / UTM zone 33N\",BASEGEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]]],CONVERSION[\"UTM zone 33N\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],PARAMETER[\"Longitude of natural origin\",15,ANGLEUNIT[\"degree\",0.0174532925199433]],PARAMETER[\"Scale factor at natural origin\",0.9996,SCALEUNIT[\"unity\",1]],PARAMETER[\"False easting\",500000,LENGTHUNIT[\"metre\",1]],PARAMETER[\"False northing\",0,LENGTHUNIT[\"metre\",1]]],CS[Cartesian,2],AXIS[\"easting\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],AXIS[\"northing\",north,ORDER[2],LENGTHUNIT[\"metre\",1]]]",
16+
"proj:spatial_dimensions": ["Y", "X"],
17+
"proj:transform": [30.0, 0.0, 500000.0, 0.0, -30.0, 5000000.0]
18+
}
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import annotations
2+
3+
import jsondiff
4+
import pytest
5+
from pydantic_zarr.core import tuplify_json
6+
7+
from eopf_geozarr.data_api.geozarr.geoproj import GeoProj
8+
from tests.test_data_api.conftest import GEOPROJ_EXAMPLES, view_json_diff
9+
10+
11+
@pytest.mark.parametrize("json_example", GEOPROJ_EXAMPLES.items(), ids=lambda v: v[0])
12+
def test_geoproj_roundtrip(json_example: tuple[str, dict[str, object]]) -> None:
13+
_, value = json_example
14+
value_tup = tuplify_json(value)
15+
attrs_json = value_tup["attributes"]
16+
model = GeoProj(**attrs_json)
17+
observed = model.model_dump()
18+
expected = attrs_json
19+
assert jsondiff.diff(expected, observed) == {}, view_json_diff(expected, observed)

0 commit comments

Comments
 (0)