Skip to content

Commit 397de7e

Browse files
cardernegadomskijonhealy1vincentsarago
authored
Properly type bbox and datetime (#490)
* Set BBox and DateTimeType at API surface * replace conint usage * update CHANGES.md --------- Co-authored-by: Pete Gadomski <[email protected]> Co-authored-by: Jonathan Healy <[email protected]> Co-authored-by: Vincent Sarago <[email protected]>
1 parent 313486b commit 397de7e

File tree

6 files changed

+62
-62
lines changed

6 files changed

+62
-62
lines changed

CHANGES.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
* Add benchmark in CI ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
8+
59
### Changed
610

11+
* Improve bbox and datetime typing ([#490](https://github.com/stac-utils/stac-fastapi/pull/490)
712
* Add `items` link to inferred link relations ([#634](https://github.com/stac-utils/stac-fastapi/issues/634))
813
* Make sure FastAPI uses Pydantic validation and serialization by not wrapping endpoint output with a Response object ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
914

1015
### Removed
1116

1217
* Deprecate `response_class` option in `stac_fastapi.api.routes.create_async_endpoint` method ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
1318

14-
### Added
15-
16-
* Add benchmark in CI ([#650](https://github.com/stac-utils/stac-fastapi/pull/650))
17-
1819
## [2.4.9] - 2023-11-17
1920

2021
### Added

stac_fastapi/api/stac_fastapi/api/models.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
from fastapi import Body, Path
88
from pydantic import BaseModel, create_model
99
from pydantic.fields import UndefinedType
10+
from stac_pydantic.shared import BBox
1011

1112
from stac_fastapi.types.extension import ApiExtension
13+
from stac_fastapi.types.rfc3339 import DateTimeType
1214
from stac_fastapi.types.search import (
1315
APIRequest,
1416
BaseSearchGetRequest,
1517
BaseSearchPostRequest,
16-
str2list,
18+
str2bbox,
1719
)
1820

1921

@@ -124,8 +126,8 @@ class ItemCollectionUri(CollectionUri):
124126
"""Get item collection."""
125127

126128
limit: int = attr.ib(default=10)
127-
bbox: Optional[str] = attr.ib(default=None, converter=str2list)
128-
datetime: Optional[str] = attr.ib(default=None)
129+
bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
130+
datetime: Optional[DateTimeType] = attr.ib(default=None)
129131

130132

131133
class POSTTokenPagination(BaseModel):

stac_fastapi/types/stac_fastapi/types/core.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
"""Base clients."""
22
import abc
3-
from datetime import datetime
43
from typing import Any, Dict, List, Optional, Union
54
from urllib.parse import urljoin
65

76
import attr
87
from fastapi import Request
98
from stac_pydantic.links import Relations
10-
from stac_pydantic.shared import MimeTypes
9+
from stac_pydantic.shared import BBox, MimeTypes
1110
from stac_pydantic.version import STAC_VERSION
1211
from starlette.responses import Response
1312

1413
from stac_fastapi.types import stac as stac_types
1514
from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
1615
from stac_fastapi.types.extension import ApiExtension
1716
from stac_fastapi.types.requests import get_base_url
17+
from stac_fastapi.types.rfc3339 import DateTimeType
1818
from stac_fastapi.types.search import BaseSearchPostRequest
1919
from stac_fastapi.types.stac import Conformance
2020

@@ -436,8 +436,8 @@ def get_search(
436436
self,
437437
collections: Optional[List[str]] = None,
438438
ids: Optional[List[str]] = None,
439-
bbox: Optional[List[NumType]] = None,
440-
datetime: Optional[Union[str, datetime]] = None,
439+
bbox: Optional[BBox] = None,
440+
datetime: Optional[DateTimeType] = None,
441441
limit: Optional[int] = 10,
442442
query: Optional[str] = None,
443443
token: Optional[str] = None,
@@ -499,8 +499,8 @@ def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection:
499499
def item_collection(
500500
self,
501501
collection_id: str,
502-
bbox: Optional[List[NumType]] = None,
503-
datetime: Optional[Union[str, datetime]] = None,
502+
bbox: Optional[BBox] = None,
503+
datetime: Optional[DateTimeType] = None,
504504
limit: int = 10,
505505
token: str = None,
506506
**kwargs,
@@ -632,8 +632,8 @@ async def get_search(
632632
self,
633633
collections: Optional[List[str]] = None,
634634
ids: Optional[List[str]] = None,
635-
bbox: Optional[List[NumType]] = None,
636-
datetime: Optional[Union[str, datetime]] = None,
635+
bbox: Optional[BBox] = None,
636+
datetime: Optional[DateTimeType] = None,
637637
limit: Optional[int] = 10,
638638
query: Optional[str] = None,
639639
token: Optional[str] = None,
@@ -699,8 +699,8 @@ async def get_collection(
699699
async def item_collection(
700700
self,
701701
collection_id: str,
702-
bbox: Optional[List[NumType]] = None,
703-
datetime: Optional[Union[str, datetime]] = None,
702+
bbox: Optional[BBox] = None,
703+
datetime: Optional[DateTimeType] = None,
704704
limit: int = 10,
705705
token: str = None,
706706
**kwargs,

stac_fastapi/types/stac_fastapi/types/rfc3339.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""rfc3339."""
22
import re
33
from datetime import datetime, timezone
4-
from typing import Optional, Tuple
4+
from typing import Optional, Tuple, Union
55

66
import iso8601
77
from pystac.utils import datetime_to_str
@@ -11,6 +11,13 @@
1111
r"(Z|([-+])(\d\d):(\d\d))$"
1212
)
1313

14+
DateTimeType = Union[
15+
datetime,
16+
Tuple[datetime, datetime],
17+
Tuple[datetime, None],
18+
Tuple[None, datetime],
19+
]
20+
1421

1522
def rfc3339_str_to_datetime(s: str) -> datetime:
1623
"""Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
@@ -40,7 +47,7 @@ def rfc3339_str_to_datetime(s: str) -> datetime:
4047

4148
def str_to_interval(
4249
interval: str,
43-
) -> Optional[Tuple[Optional[datetime], Optional[datetime]]]:
50+
) -> Optional[DateTimeType]:
4451
"""Extract a tuple of datetimes from an interval string.
4552
4653
Interval strings are defined by
@@ -59,7 +66,10 @@ def str_to_interval(
5966
raise ValueError("Empty interval string is invalid.")
6067

6168
values = interval.split("/")
62-
if len(values) != 2:
69+
if len(values) == 1:
70+
# Single date for == date case
71+
return rfc3339_str_to_datetime(values[0])
72+
elif len(values) > 2:
6373
raise ValueError(
6474
f"Interval string '{interval}' contains more than one forward slash."
6575
)

stac_fastapi/types/stac_fastapi/types/search.py

Lines changed: 26 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
Polygon,
2121
_GeometryBase,
2222
)
23-
from pydantic import BaseModel, ConstrainedInt, validator
23+
from pydantic import BaseModel, ConstrainedInt, Field, validator
2424
from pydantic.errors import NumberNotGtError
2525
from pydantic.validators import int_validator
2626
from stac_pydantic.shared import BBox
2727
from stac_pydantic.utils import AutoValueEnum
2828

29-
from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime, str_to_interval
29+
from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval
3030

3131
# Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287
3232
NumType = Union[float, int]
@@ -82,6 +82,14 @@ def str2list(x: str) -> Optional[List]:
8282
return x.split(",")
8383

8484

85+
def str2bbox(x: str) -> Optional[BBox]:
86+
"""Convert string to BBox based on , delimiter."""
87+
if x:
88+
t = tuple(float(v) for v in str2list(x))
89+
assert len(t) == 4
90+
return t
91+
92+
8593
@attr.s # type:ignore
8694
class APIRequest(abc.ABC):
8795
"""Generic API Request base class."""
@@ -98,9 +106,9 @@ class BaseSearchGetRequest(APIRequest):
98106

99107
collections: Optional[str] = attr.ib(default=None, converter=str2list)
100108
ids: Optional[str] = attr.ib(default=None, converter=str2list)
101-
bbox: Optional[str] = attr.ib(default=None, converter=str2list)
102-
intersects: Optional[str] = attr.ib(default=None)
103-
datetime: Optional[str] = attr.ib(default=None)
109+
bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
110+
intersects: Optional[str] = attr.ib(default=None, converter=str2list)
111+
datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval)
104112
limit: Optional[int] = attr.ib(default=10)
105113

106114

@@ -121,20 +129,18 @@ class BaseSearchPostRequest(BaseModel):
121129
intersects: Optional[
122130
Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon]
123131
]
124-
datetime: Optional[str]
125-
limit: Optional[Limit] = 10
132+
datetime: Optional[DateTimeType]
133+
limit: Optional[Limit] = Field(default=10)
126134

127135
@property
128136
def start_date(self) -> Optional[datetime]:
129137
"""Extract the start date from the datetime string."""
130-
interval = str_to_interval(self.datetime)
131-
return interval[0] if interval else None
138+
return self.datetime[0] if self.datetime else None
132139

133140
@property
134141
def end_date(self) -> Optional[datetime]:
135142
"""Extract the end date from the datetime string."""
136-
interval = str_to_interval(self.datetime)
137-
return interval[1] if interval else None
143+
return self.datetime[1] if self.datetime else None
138144

139145
@validator("intersects")
140146
def validate_spatial(cls, v, values):
@@ -143,10 +149,12 @@ def validate_spatial(cls, v, values):
143149
raise ValueError("intersects and bbox parameters are mutually exclusive")
144150
return v
145151

146-
@validator("bbox")
147-
def validate_bbox(cls, v: BBox):
152+
@validator("bbox", pre=True)
153+
def validate_bbox(cls, v: Union[str, BBox]) -> BBox:
148154
"""Check order of supplied bbox coordinates."""
149155
if v:
156+
if type(v) == str:
157+
v = str2bbox(v)
150158
# Validate order
151159
if len(v) == 4:
152160
xmin, ymin, xmax, ymax = v
@@ -173,34 +181,11 @@ def validate_bbox(cls, v: BBox):
173181

174182
return v
175183

176-
@validator("datetime")
177-
def validate_datetime(cls, v):
178-
"""Validate datetime."""
179-
if "/" in v:
180-
values = v.split("/")
181-
else:
182-
# Single date is interpreted as end date
183-
values = ["..", v]
184-
185-
dates = []
186-
for value in values:
187-
if value == ".." or value == "":
188-
dates.append("..")
189-
continue
190-
191-
# throws ValueError if invalid RFC 3339 string
192-
dates.append(rfc3339_str_to_datetime(value))
193-
194-
if dates[0] == ".." and dates[1] == "..":
195-
raise ValueError(
196-
"Invalid datetime range, both ends of range may not be open"
197-
)
198-
199-
if ".." not in dates and dates[0] > dates[1]:
200-
raise ValueError(
201-
"Invalid datetime range, must match format (begin_date, end_date)"
202-
)
203-
184+
@validator("datetime", pre=True)
185+
def validate_datetime(cls, v: Union[str, DateTimeType]) -> DateTimeType:
186+
"""Parse datetime."""
187+
if type(v) == str:
188+
v = str_to_interval(v)
204189
return v
205190

206191
@property

stac_fastapi/types/stac_fastapi/types/stac.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import sys
33
from typing import Any, Dict, List, Literal, Optional, Union
44

5+
from stac_pydantic.shared import BBox
6+
57
# Avoids a Pydantic error:
68
# TypeError: You should use `typing_extensions.TypedDict` instead of
79
# `typing.TypedDict` with Python < 3.9.2. Without it, there is no way to
@@ -64,7 +66,7 @@ class Item(TypedDict, total=False):
6466
stac_extensions: Optional[List[str]]
6567
id: str
6668
geometry: Dict[str, Any]
67-
bbox: List[NumType]
69+
bbox: BBox
6870
properties: Dict[str, Any]
6971
links: List[Dict[str, Any]]
7072
assets: Dict[str, Any]

0 commit comments

Comments
 (0)