Skip to content

Commit 644d271

Browse files
committed
Set BBox and DateTimeType at API surface
1 parent 06218c5 commit 644d271

File tree

5 files changed

+55
-55
lines changed

5 files changed

+55
-55
lines changed

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: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
"""Base clients."""
22
import abc
33
from datetime import datetime
4-
from typing import Any, Dict, List, Optional, Union
4+
from typing import Any, Dict, List, Optional, Tuple, Union
55
from urllib.parse import urljoin
66

77
import attr
88
from fastapi import Request
99
from stac_pydantic.links import Relations
10-
from stac_pydantic.shared import MimeTypes
10+
from stac_pydantic.shared import BBox, MimeTypes
1111
from stac_pydantic.version import STAC_VERSION
1212
from starlette.responses import Response
1313

1414
from stac_fastapi.types import stac as stac_types
1515
from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES
1616
from stac_fastapi.types.extension import ApiExtension
1717
from stac_fastapi.types.requests import get_base_url
18+
from stac_fastapi.types.rfc3339 import DateTimeType
1819
from stac_fastapi.types.search import BaseSearchPostRequest
1920
from stac_fastapi.types.stac import Conformance
2021

@@ -429,8 +430,8 @@ def get_search(
429430
self,
430431
collections: Optional[List[str]] = None,
431432
ids: Optional[List[str]] = None,
432-
bbox: Optional[List[NumType]] = None,
433-
datetime: Optional[Union[str, datetime]] = None,
433+
bbox: Optional[BBox] = None,
434+
datetime: Optional[DateTimeType] = None,
434435
limit: Optional[int] = 10,
435436
query: Optional[str] = None,
436437
token: Optional[str] = None,
@@ -491,8 +492,8 @@ def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection:
491492
def item_collection(
492493
self,
493494
collection_id: str,
494-
bbox: Optional[List[NumType]] = None,
495-
datetime: Optional[Union[str, datetime]] = None,
495+
bbox: Optional[BBox] = None,
496+
datetime: Optional[DateTimeType] = None,
496497
limit: int = 10,
497498
token: str = None,
498499
**kwargs,
@@ -626,8 +627,8 @@ async def get_search(
626627
self,
627628
collections: Optional[List[str]] = None,
628629
ids: Optional[List[str]] = None,
629-
bbox: Optional[List[NumType]] = None,
630-
datetime: Optional[Union[str, datetime]] = None,
630+
bbox: Optional[BBox] = None,
631+
datetime: Optional[DateTimeType] = None,
631632
limit: Optional[int] = 10,
632633
query: Optional[str] = None,
633634
token: Optional[str] = None,
@@ -692,8 +693,8 @@ async def get_collection(
692693
async def item_collection(
693694
self,
694695
collection_id: str,
695-
bbox: Optional[List[NumType]] = None,
696-
datetime: Optional[Union[str, datetime]] = None,
696+
bbox: Optional[BBox] = None,
697+
datetime: Optional[DateTimeType] = None,
697698
limit: int = 10,
698699
token: str = None,
699700
**kwargs,

stac_fastapi/types/stac_fastapi/types/rfc3339.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
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
88

99
RFC33339_PATTERN = r"^(\d\d\d\d)\-(\d\d)\-(\d\d)(T|t)(\d\d):(\d\d):(\d\d)([.]\d+)?(Z|([-+])(\d\d):(\d\d))$"
1010

11+
DateTimeType = Union[
12+
datetime,
13+
Tuple[datetime, datetime],
14+
Tuple[datetime, None],
15+
Tuple[None, datetime],
16+
]
17+
1118

1219
def rfc3339_str_to_datetime(s: str) -> datetime:
1320
"""Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
@@ -37,7 +44,7 @@ def rfc3339_str_to_datetime(s: str) -> datetime:
3744

3845
def str_to_interval(
3946
interval: str,
40-
) -> Optional[Tuple[Optional[datetime], Optional[datetime]]]:
47+
) -> Optional[DateTimeType]:
4148
"""Extract a tuple of datetimes from an interval string.
4249
4350
Interval strings are defined by
@@ -56,7 +63,10 @@ def str_to_interval(
5663
raise ValueError("Empty interval string is invalid.")
5764

5865
values = interval.split("/")
59-
if len(values) != 2:
66+
if len(values) == 1:
67+
# Single date for == date case
68+
return rfc3339_str_to_datetime(values[0])
69+
elif len(values) > 2:
6070
raise ValueError(
6171
f"Interval string '{interval}' contains more than one forward slash."
6272
)

stac_fastapi/types/stac_fastapi/types/search.py

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from stac_pydantic.shared import BBox
2525
from stac_pydantic.utils import AutoValueEnum
2626

27-
from stac_fastapi.types.rfc3339 import rfc3339_str_to_datetime, str_to_interval
27+
from stac_fastapi.types.core import DateTimeType
28+
from stac_fastapi.types.rfc3339 import str_to_interval
2829

2930
# Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287
3031
NumType = Union[float, int]
@@ -58,6 +59,13 @@ def str2list(x: str) -> Optional[List]:
5859
return x.split(",")
5960

6061

62+
def str2bbox(x: str) -> Optional[BBox]:
63+
if x:
64+
t = tuple(float(v) for v in str2list(x))
65+
assert len(t) == 4
66+
return t
67+
68+
6169
@attr.s # type:ignore
6270
class APIRequest(abc.ABC):
6371
"""Generic API Request base class."""
@@ -73,9 +81,9 @@ class BaseSearchGetRequest(APIRequest):
7381

7482
collections: Optional[str] = attr.ib(default=None, converter=str2list)
7583
ids: Optional[str] = attr.ib(default=None, converter=str2list)
76-
bbox: Optional[str] = attr.ib(default=None, converter=str2list)
84+
bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
7785
intersects: Optional[str] = attr.ib(default=None, converter=str2list)
78-
datetime: Optional[str] = attr.ib(default=None)
86+
datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval)
7987
limit: Optional[int] = attr.ib(default=10)
8088

8189

@@ -96,20 +104,18 @@ class BaseSearchPostRequest(BaseModel):
96104
intersects: Optional[
97105
Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon]
98106
]
99-
datetime: Optional[str]
107+
datetime: Optional[DateTimeType]
100108
limit: Optional[conint(gt=0, le=10000)] = 10
101109

102110
@property
103111
def start_date(self) -> Optional[datetime]:
104112
"""Extract the start date from the datetime string."""
105-
interval = str_to_interval(self.datetime)
106-
return interval[0] if interval else None
113+
return self.datetime[0] if self.datetime else None
107114

108115
@property
109116
def end_date(self) -> Optional[datetime]:
110117
"""Extract the end date from the datetime string."""
111-
interval = str_to_interval(self.datetime)
112-
return interval[1] if interval else None
118+
return self.datetime[1] if self.datetime else None
113119

114120
@validator("intersects")
115121
def validate_spatial(cls, v, values):
@@ -118,10 +124,12 @@ def validate_spatial(cls, v, values):
118124
raise ValueError("intersects and bbox parameters are mutually exclusive")
119125
return v
120126

121-
@validator("bbox")
122-
def validate_bbox(cls, v: BBox):
127+
@validator("bbox", pre=True)
128+
def validate_bbox(cls, v: Union[str, BBox]) -> BBox:
123129
"""Check order of supplied bbox coordinates."""
124130
if v:
131+
if type(v) == str:
132+
v = str2bbox(v)
125133
# Validate order
126134
if len(v) == 4:
127135
xmin, ymin, xmax, ymax = v
@@ -148,34 +156,11 @@ def validate_bbox(cls, v: BBox):
148156

149157
return v
150158

151-
@validator("datetime")
152-
def validate_datetime(cls, v):
153-
"""Validate datetime."""
154-
if "/" in v:
155-
values = v.split("/")
156-
else:
157-
# Single date is interpreted as end date
158-
values = ["..", v]
159-
160-
dates = []
161-
for value in values:
162-
if value == ".." or value == "":
163-
dates.append("..")
164-
continue
165-
166-
# throws ValueError if invalid RFC 3339 string
167-
dates.append(rfc3339_str_to_datetime(value))
168-
169-
if dates[0] == ".." and dates[1] == "..":
170-
raise ValueError(
171-
"Invalid datetime range, both ends of range may not be open"
172-
)
173-
174-
if ".." not in dates and dates[0] > dates[1]:
175-
raise ValueError(
176-
"Invalid datetime range, must match format (begin_date, end_date)"
177-
)
178-
159+
@validator("datetime", pre=True)
160+
def validate_datetime(cls, v: Union[str, DateTimeType]) -> DateTimeType:
161+
"""Parse datetime."""
162+
if type(v) == str:
163+
v = str_to_interval(v)
179164
return v
180165

181166
@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, 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 `typing.TypedDict` with Python < 3.9.2.
79
# Without it, there is no way to differentiate required and optional fields when subclassed.
@@ -63,7 +65,7 @@ class Item(TypedDict, total=False):
6365
stac_extensions: Optional[List[str]]
6466
id: str
6567
geometry: Dict[str, Any]
66-
bbox: List[NumType]
68+
bbox: BBox
6769
properties: Dict[str, Any]
6870
links: List[Dict[str, Any]]
6971
assets: Dict[str, Any]

0 commit comments

Comments
 (0)