Skip to content

Commit f2158df

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

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: 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

@@ -429,8 +429,8 @@ def get_search(
429429
self,
430430
collections: Optional[List[str]] = None,
431431
ids: Optional[List[str]] = None,
432-
bbox: Optional[List[NumType]] = None,
433-
datetime: Optional[Union[str, datetime]] = None,
432+
bbox: Optional[BBox] = None,
433+
datetime: Optional[DateTimeType] = None,
434434
limit: Optional[int] = 10,
435435
query: Optional[str] = None,
436436
token: Optional[str] = None,
@@ -491,8 +491,8 @@ def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection:
491491
def item_collection(
492492
self,
493493
collection_id: str,
494-
bbox: Optional[List[NumType]] = None,
495-
datetime: Optional[Union[str, datetime]] = None,
494+
bbox: Optional[BBox] = None,
495+
datetime: Optional[DateTimeType] = None,
496496
limit: int = 10,
497497
token: str = None,
498498
**kwargs,
@@ -626,8 +626,8 @@ async def get_search(
626626
self,
627627
collections: Optional[List[str]] = None,
628628
ids: Optional[List[str]] = None,
629-
bbox: Optional[List[NumType]] = None,
630-
datetime: Optional[Union[str, datetime]] = None,
629+
bbox: Optional[BBox] = None,
630+
datetime: Optional[DateTimeType] = None,
631631
limit: Optional[int] = 10,
632632
query: Optional[str] = None,
633633
token: Optional[str] = None,
@@ -692,8 +692,8 @@ async def get_collection(
692692
async def item_collection(
693693
self,
694694
collection_id: str,
695-
bbox: Optional[List[NumType]] = None,
696-
datetime: Optional[Union[str, datetime]] = None,
695+
bbox: Optional[BBox] = None,
696+
datetime: Optional[DateTimeType] = None,
697697
limit: int = 10,
698698
token: str = None,
699699
**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: 24 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,14 @@ def str2list(x: str) -> Optional[List]:
5859
return x.split(",")
5960

6061

62+
def str2bbox(x: str) -> Optional[BBox]:
63+
"""Convert string to BBox based on , delimiter."""
64+
if x:
65+
t = tuple(float(v) for v in str2list(x))
66+
assert len(t) == 4
67+
return t
68+
69+
6170
@attr.s # type:ignore
6271
class APIRequest(abc.ABC):
6372
"""Generic API Request base class."""
@@ -73,9 +82,9 @@ class BaseSearchGetRequest(APIRequest):
7382

7483
collections: Optional[str] = attr.ib(default=None, converter=str2list)
7584
ids: Optional[str] = attr.ib(default=None, converter=str2list)
76-
bbox: Optional[str] = attr.ib(default=None, converter=str2list)
85+
bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
7786
intersects: Optional[str] = attr.ib(default=None, converter=str2list)
78-
datetime: Optional[str] = attr.ib(default=None)
87+
datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval)
7988
limit: Optional[int] = attr.ib(default=10)
8089

8190

@@ -96,20 +105,18 @@ class BaseSearchPostRequest(BaseModel):
96105
intersects: Optional[
97106
Union[Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon]
98107
]
99-
datetime: Optional[str]
108+
datetime: Optional[DateTimeType]
100109
limit: Optional[conint(gt=0, le=10000)] = 10
101110

102111
@property
103112
def start_date(self) -> Optional[datetime]:
104113
"""Extract the start date from the datetime string."""
105-
interval = str_to_interval(self.datetime)
106-
return interval[0] if interval else None
114+
return self.datetime[0] if self.datetime else None
107115

108116
@property
109117
def end_date(self) -> Optional[datetime]:
110118
"""Extract the end date from the datetime string."""
111-
interval = str_to_interval(self.datetime)
112-
return interval[1] if interval else None
119+
return self.datetime[1] if self.datetime else None
113120

114121
@validator("intersects")
115122
def validate_spatial(cls, v, values):
@@ -118,10 +125,12 @@ def validate_spatial(cls, v, values):
118125
raise ValueError("intersects and bbox parameters are mutually exclusive")
119126
return v
120127

121-
@validator("bbox")
122-
def validate_bbox(cls, v: BBox):
128+
@validator("bbox", pre=True)
129+
def validate_bbox(cls, v: Union[str, BBox]) -> BBox:
123130
"""Check order of supplied bbox coordinates."""
124131
if v:
132+
if type(v) == str:
133+
v = str2bbox(v)
125134
# Validate order
126135
if len(v) == 4:
127136
xmin, ymin, xmax, ymax = v
@@ -148,34 +157,11 @@ def validate_bbox(cls, v: BBox):
148157

149158
return v
150159

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-
160+
@validator("datetime", pre=True)
161+
def validate_datetime(cls, v: Union[str, DateTimeType]) -> DateTimeType:
162+
"""Parse datetime."""
163+
if type(v) == str:
164+
v = str_to_interval(v)
179165
return v
180166

181167
@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)