Skip to content

Commit ff603f4

Browse files
authored
feat: add typed dicts (#101)
Closes #95
1 parent 14f0a7d commit ff603f4

File tree

8 files changed

+280
-14
lines changed

8 files changed

+280
-14
lines changed

docs/api/stac.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# STAC
2+
3+
Typed dictionaries for STAC entities.
4+
5+
::: rustac.Catalog
6+
::: rustac.Collection
7+
::: rustac.Item
8+
::: rustac.ItemCollection

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ nav:
2525
- migrate: api/migrate.md
2626
- read: api/read.md
2727
- search: api/search.md
28+
- stac: api/stac.md
2829
- version: api/version.md
2930
- walk: api/walk.md
3031
- write: api/write.md

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ asyncio_mode = "auto"
4646
asyncio_default_fixture_loop_scope = "function"
4747

4848
[tool.ruff]
49-
exclude = ["docs/examples/example_*.py"]
49+
exclude = ["python/rustac/__init__.py", "docs/examples/example_*.py"]
5050

5151
[dependency-groups]
5252
dev = [
@@ -88,7 +88,8 @@ requires = ["maturin>=1.7,<2.0"]
8888
build-backend = "maturin"
8989

9090
[tool.maturin]
91+
python-source = "python"
9192
strip = true
92-
opt-level = "z" # TODO compare with "s"
93+
opt-level = "z" # TODO compare with "s"
9394
lto = true
9495
codegen-units = 1

python/rustac/__init__.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
from __future__ import annotations
2+
3+
from .rustac import *
4+
from typing import TypedDict
5+
6+
7+
class Catalog(TypedDict):
8+
"""A STAC Catalog object represents a logical group of other Catalog, Collection, and Item objects."""
9+
10+
type: str
11+
"""Set to Catalog if this Catalog only implements the Catalog spec."""
12+
13+
stac_version: str
14+
"""The STAC version the Catalog implements."""
15+
16+
stac_extensions: list[str] | None
17+
"""A list of extension identifiers the Catalog implements."""
18+
19+
id: str
20+
"""Identifier for the Catalog."""
21+
22+
title: str | None
23+
"""A short descriptive one-line title for the Catalog."""
24+
25+
description: str
26+
"""Detailed multi-line description to fully explain the Catalog.
27+
28+
CommonMark 0.29 syntax MAY be used for rich text representation."""
29+
30+
links: list[Link]
31+
"""A list of references to other documents."""
32+
33+
class Collection(TypedDict):
34+
"""The STAC Collection Specification defines a set of common fields to describe a group of Items that share properties and metadata."""
35+
36+
type: str
37+
"""Must be set to Collection to be a valid Collection."""
38+
39+
stac_version: str
40+
"""The STAC version the Collection implements."""
41+
42+
stac_extensions: list[str] | None
43+
"""A list of extension identifiers the Collection implements."""
44+
45+
id: str
46+
"""Identifier for the Collection that is unique across all collections in the root catalog."""
47+
48+
title: str | None
49+
"""A short descriptive one-line title for the Collection."""
50+
51+
description: str
52+
"""Detailed multi-line description to fully explain the Collection.
53+
54+
CommonMark 0.29 syntax MAY be used for rich text representation."""
55+
56+
keywords: list[str] | None
57+
"""List of keywords describing the Collection."""
58+
59+
license: str
60+
"""License(s) of the data collection as SPDX License identifier, SPDX License expression, or `other`."""
61+
62+
providers: list[Provider] | None
63+
"""A list of providers, which may include all organizations capturing or processing the data or the hosting provider."""
64+
65+
extent: Extent
66+
"""Spatial and temporal extents."""
67+
68+
summaries: dict[str, Any]
69+
"""A map of property summaries, either a set of values, a range of values or a JSON Schema."""
70+
71+
links: list[Link]
72+
"""A list of references to other documents."""
73+
74+
assets: dict[str, Asset] | None
75+
"""Dictionary of asset objects that can be downloaded, each with a unique key."""
76+
77+
item_assets: dict[str, ItemAsset] | None
78+
"""A dictionary of assets that can be found in member Items."""
79+
80+
class Provider(TypedDict):
81+
"""A provider is any of the organizations that captures or processes the content of the Collection and therefore influences the data offered by this Collection."""
82+
83+
name: str
84+
"""The name of the organization or the individual."""
85+
86+
description: str | None
87+
"""Multi-line description to add further provider information such as processing details for processors and producers, hosting details for hosts or basic contact information.
88+
89+
CommonMark 0.29 syntax MAY be used for rich text representation."""
90+
91+
roles: list[
92+
Literal["licensor"]
93+
| Literal["producer"]
94+
| Literal["processor"]
95+
| Literal["host"]
96+
]
97+
"""Roles of the provider."""
98+
99+
url: str | None
100+
"""Homepage on which the provider describes the dataset and publishes contact information."""
101+
102+
class Extent(TypedDict):
103+
"""The object describes the spatio-temporal extents of the Collection."""
104+
105+
spatial: SpatialExtent
106+
"""Potential spatial extents covered by the Collection."""
107+
108+
temporal: TemporalExtent
109+
"""Potential temporal extents covered by the Collection."""
110+
111+
class SpatialExtent(TypedDict):
112+
"""The object describes the spatial extents of the Collection."""
113+
114+
bbox: list[list[int | float]]
115+
"""Potential spatial extents covered by the Collection."""
116+
117+
class TemporalExtent(TypedDict):
118+
"""The object describes the temporal extents of the Collection."""
119+
120+
bbox: list[list[str | None]]
121+
"""Potential temporal extents covered by the Collection."""
122+
123+
class ItemAsset(TypedDict):
124+
"""An Item Asset Object defined at the Collection level is nearly the same as the Asset Object in Items, except for two differences.
125+
126+
The href field is not required, because Item Asset Definitions don't point to any data by themselves, but at least two other fields must be present."""
127+
128+
title: str | None
129+
"""The displayed title for clients and users."""
130+
131+
description: str | None
132+
"""A description of the Asset providing additional details, such as how it was processed or created.
133+
134+
CommonMark 0.29 syntax MAY be used for rich text representation."""
135+
136+
type: str | None
137+
"""Media type of the asset."""
138+
139+
roles: list[str] | None
140+
"""The semantic roles of the asset, similar to the use of rel in links."""
141+
142+
class Item(TypedDict):
143+
"""An Item is a GeoJSON Feature augmented with foreign members relevant to a STAC object."""
144+
145+
type: str
146+
"""Type of the GeoJSON Object. MUST be set to Feature."""
147+
148+
stac_version: str
149+
"""The STAC version the Item implements."""
150+
151+
stac_extensions: list[str] | None
152+
"""A list of extensions the Item implements."""
153+
154+
id: str
155+
"""Provider identifier. The ID should be unique within the Collection that contains the Item."""
156+
157+
geometry: dict[str, Any] | None
158+
"""Defines the full footprint of the asset represented by this item, formatted according to RFC 7946, section 3.1 if a geometry is provided or section 3.2 if no geometry is provided."""
159+
160+
bbox: list[int | float] | None
161+
"""REQUIRED if geometry is not null, prohibited if geometry is null.
162+
163+
Bounding Box of the asset represented by this Item, formatted according to RFC 7946, section 5."""
164+
165+
properties: Properties
166+
"""A dictionary of additional metadata for the Item."""
167+
168+
links: list[Link]
169+
"""List of link objects to resources and related URLs.
170+
171+
See the best practices for details on when the use self links is strongly recommended."""
172+
173+
assets: dict[str, Asset]
174+
"""Dictionary of asset objects that can be downloaded, each with a unique key."""
175+
176+
collection: str | None
177+
"""The id of the STAC Collection this Item references to.
178+
179+
This field is required if a link with a collection relation type is present and is not allowed otherwise."""
180+
181+
class Properties(TypedDict):
182+
"""Additional metadata fields can be added to the GeoJSON Object Properties."""
183+
184+
datetime: str | None
185+
"""The searchable date and time of the assets, which must be in UTC.
186+
187+
It is formatted according to RFC 3339, section 5.6. null is allowed, but requires start_datetime and end_datetime from common metadata to be set."""
188+
189+
class Link(TypedDict):
190+
"""This object describes a relationship with another entity.
191+
192+
Data providers are advised to be liberal with the links section, to describe
193+
things like the Catalog an Item is in, related Items, parent or child Items
194+
(modeled in different ways, like an 'acquisition' or derived data)."""
195+
196+
href: str
197+
"""The actual link in the format of an URL.
198+
199+
Relative and absolute links are both allowed. Trailing slashes are significant."""
200+
201+
rel: str
202+
"""Relationship between the current document and the linked document."""
203+
204+
type: str | None
205+
"""Media type of the referenced entity."""
206+
207+
title: str | None
208+
"""A human readable title to be used in rendered displays of the link."""
209+
210+
method: str | None
211+
"""The HTTP method that shall be used for the request to the target resource, in uppercase.
212+
213+
GET by default"""
214+
215+
headers: dict[str, str | list[str]] | None
216+
"""The HTTP headers to be sent for the request to the target resource."""
217+
218+
body: Any | None
219+
"""The HTTP body to be sent to the target resource."""
220+
221+
class Asset(TypedDict):
222+
"""An Asset is an object that contains a URI to data associated with the Item that can be downloaded or streamed.
223+
224+
It is allowed to add additional fields."""
225+
226+
href: str
227+
"""URI to the asset object. Relative and absolute URI are both allowed. Trailing slashes are significant."""
228+
229+
title: str | None
230+
"""The displayed title for clients and users."""
231+
232+
description: str | None
233+
"""A description of the Asset providing additional details, such as how it was processed or created.
234+
235+
CommonMark 0.29 syntax MAY be used for rich text representation."""
236+
237+
type: str | None
238+
"""Media type of the asset.
239+
240+
See the common media types in the best practice doc for commonly used asset types."""
241+
242+
roles: list[str] | None
243+
"""The semantic roles of the asset, similar to the use of rel in links."""
244+
245+
class ItemCollection(TypedDict):
246+
"""A GeoJSON feature collection of STAC Items."""
247+
248+
features: list[Item]
249+
"""STAC items."""
250+
251+
__doc__ = rustac.__doc__
252+
if hasattr(rustac, "__all__"):
253+
__all__ = rustac.__all__

python/rustac/py.typed

Whitespace-only changes.

rustac.pyi renamed to python/rustac/rustac.pyi

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
"""The power of Rust for the Python STAC ecosystem."""
2+
13
from typing import Any, AsyncIterator, Literal, Optional, Tuple
24

35
import arro3.core
46

7+
from rustac import Catalog, Collection, Item, ItemCollection
8+
59
class RustacError(Exception):
610
"""A package-specific exception."""
711

@@ -144,7 +148,7 @@ class DuckdbClient:
144148
>>> data_frame = GeoDataFrame.from_arrow(table)
145149
"""
146150

147-
def get_collections(self, href: str) -> list[dict[str, Any]]:
151+
def get_collections(self, href: str) -> list[Collection]:
148152
"""Returns all collections in this stac-geoparquet file.
149153
150154
These collections will be auto-generated from the STAC items, one
@@ -215,7 +219,7 @@ async def read(
215219

216220
def from_arrow(
217221
table: arro3.core.Table,
218-
) -> dict[str, Any]:
222+
) -> ItemCollection:
219223
"""
220224
Converts an [arro3.core.Table][] to a STAC item collection.
221225
@@ -229,7 +233,7 @@ def from_arrow(
229233
"""
230234

231235
def to_arrow(
232-
items: list[dict[str, Any]] | dict[str, Any],
236+
items: list[Item] | ItemCollection,
233237
) -> arro3.core.Table:
234238
"""
235239
Converts items to an [arro3.core.Table][].
@@ -260,7 +264,7 @@ async def search(
260264
query: Optional[dict[str, Any]] = None,
261265
use_duckdb: Optional[bool] = None,
262266
**kwargs: str,
263-
) -> list[dict[str, Any]]:
267+
) -> dict[str, Any]:
264268
"""
265269
Searches a STAC API server.
266270
@@ -299,10 +303,10 @@ async def search(
299303
kwargs: Additional parameters to pass in to the search.
300304
301305
Returns:
302-
A list of the returned STAC items.
306+
A feature collection of the returned STAC items.
303307
304308
Examples:
305-
>>> items = await rustac.search(
309+
>>> item_collection = await rustac.search(
306310
... "https://landsatlook.usgs.gov/stac-server",
307311
... collections=["landsat-c2l2-sr"],
308312
... intersects={"type": "Point", "coordinates": [-105.119, 40.173]},
@@ -387,7 +391,7 @@ async def search_to(
387391

388392
def walk(
389393
container: dict[str, Any],
390-
) -> AsyncIterator[tuple[dict[str, Any], list[dict[str, Any]], list[dict[str, Any]]]]:
394+
) -> AsyncIterator[tuple[Catalog | Collection, list[Catalog | Collection], list[Item]]]:
391395
"""Recursively walks a STAC catalog or collection breadth-first.
392396
393397
Args:

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import json
22
from pathlib import Path
3-
from typing import Any
43

54
import pytest
5+
from rustac import Item
66

77

88
@pytest.fixture
@@ -26,6 +26,6 @@ def data(root: Path) -> Path:
2626

2727

2828
@pytest.fixture
29-
def item(examples: Path) -> dict[str, Any]:
29+
def item(examples: Path) -> Item:
3030
with open(examples / "simple-item.json") as f:
3131
return json.load(f)

tests/test_arrow.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
from typing import Any
2-
31
import pytest
42
import rustac
53
from geopandas import GeoDataFrame
4+
from rustac import Item
65

76
pytest.importorskip("arro3.core")
87

98

10-
def test_to_arrow(item: dict[str, Any]) -> None:
9+
def test_to_arrow(item: Item) -> None:
1110
table = rustac.to_arrow([item])
1211
data_frame = GeoDataFrame.from_arrow(table)
1312
assert len(data_frame) == 1

0 commit comments

Comments
 (0)