diff --git a/CHANGES.md b/CHANGES.md index 25a8eaea5..0a904d9a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### Fixed - Remove defaults in OpenAPI schemas +- Type Hints for TypedDict ### Added diff --git a/stac_fastapi/api/stac_fastapi/api/middleware.py b/stac_fastapi/api/stac_fastapi/api/middleware.py index b0965bd5e..0e7ece7e9 100644 --- a/stac_fastapi/api/stac_fastapi/api/middleware.py +++ b/stac_fastapi/api/stac_fastapi/api/middleware.py @@ -82,7 +82,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await self.app(scope, receive, send) - def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str]: + def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str, str, str]: proto = scope.get("scheme", "http") header_host = self._get_header_value_by_name(scope, "host") if header_host is None: @@ -127,7 +127,7 @@ def _get_header_value_by_name( @staticmethod def _replace_header_value_by_name( scope: Scope, header_name: str, new_value: str - ) -> List[Tuple[str]]: + ) -> List[Tuple[str, str]]: return [ (name, value) for name, value in scope["headers"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/types.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/types.py index 428b65225..fa98eb6fb 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/types.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/types.py @@ -2,33 +2,34 @@ from typing import Any, Dict, List, Literal, Optional, Union -from pydantic import Field -from typing_extensions import TypedDict +from typing_extensions import NotRequired, TypedDict from stac_fastapi.types.rfc3339 import DateTimeType +Bucket = TypedDict( + "Bucket", + { + "key": str, + "data_type": str, + "frequency": NotRequired[Dict], + # we can't use the `class Bucket` notation because `from` is a reserved key + "from": NotRequired[Union[int, float]], + "to": NotRequired[Optional[Union[int, float]]], + }, +) -class Bucket(TypedDict, total=False): - """A STAC aggregation bucket.""" - key: str - data_type: str - frequency: Optional[Dict] = None - _from: Optional[Union[int, float]] = Field(alias="from", default=None) - to: Optional[Optional[Union[int, float]]] = None - - -class Aggregation(TypedDict, total=False): +class Aggregation(TypedDict): """A STAC aggregation.""" name: str data_type: str - buckets: Optional[List[Bucket]] = None - overflow: Optional[int] = None - value: Optional[Union[str, int, DateTimeType]] = None + buckets: NotRequired[List[Bucket]] + overflow: NotRequired[int] + value: NotRequired[Union[str, int, DateTimeType]] -class AggregationCollection(TypedDict, total=False): +class AggregationCollection(TypedDict): """STAC Item Aggregation Collection.""" type: Literal["AggregationCollection"] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py index 9dae2aa8e..67de422d3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py @@ -7,7 +7,7 @@ from pydantic import BaseModel, Field from typing_extensions import Annotated -from stac_fastapi.types.search import APIRequest, str2list +from stac_fastapi.types.search import APIRequest def _ft_converter( @@ -22,7 +22,9 @@ def _ft_converter( ), ] = None, ) -> Optional[List[str]]: - return str2list(val) + if val: + return val.split(",") + return None @attr.s diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 99ef87f9c..f90b72823 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -533,7 +533,7 @@ def item_collection( bbox: Optional[BBox] = None, datetime: Optional[str] = None, limit: int = 10, - token: str = None, + token: Optional[str] = None, **kwargs, ) -> stac.ItemCollection: """Get all items from a specific collection. @@ -744,7 +744,7 @@ async def item_collection( bbox: Optional[BBox] = None, datetime: Optional[str] = None, limit: int = 10, - token: str = None, + token: Optional[str] = None, **kwargs, ) -> stac.ItemCollection: """Get all items from a specific collection. diff --git a/stac_fastapi/types/stac_fastapi/types/extension.py b/stac_fastapi/types/stac_fastapi/types/extension.py index 55a4a123c..22fed6068 100644 --- a/stac_fastapi/types/stac_fastapi/types/extension.py +++ b/stac_fastapi/types/stac_fastapi/types/extension.py @@ -15,7 +15,7 @@ class ApiExtension(abc.ABC): GET = None POST = None - def get_request_model(self, verb: Optional[str] = "GET") -> Optional[BaseModel]: + def get_request_model(self, verb: str = "GET") -> Optional[BaseModel]: """Return the request model for the extension.method. The model can differ based on HTTP verb diff --git a/stac_fastapi/types/stac_fastapi/types/rfc3339.py b/stac_fastapi/types/stac_fastapi/types/rfc3339.py index 77ec993dd..681a491aa 100644 --- a/stac_fastapi/types/stac_fastapi/types/rfc3339.py +++ b/stac_fastapi/types/stac_fastapi/types/rfc3339.py @@ -145,7 +145,7 @@ def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]: status_code=400, detail="Start datetime cannot be before end datetime." ) - return start, end + return start, end # type: ignore def now_in_utc() -> datetime: diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index 7c13c1b19..f52f4530e 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -34,7 +34,7 @@ def str2list(x: str) -> Optional[List[str]]: def str2bbox(x: str) -> Optional[BBox]: """Convert string to BBox based on , delimiter.""" if x: - t = tuple(float(v) for v in str2list(x)) + t = tuple(float(v) for v in x.split(",")) assert len(t) in [4, 6], f"BBox '{x}' must have 4 or 6 values." return t @@ -54,7 +54,9 @@ def _collection_converter( ), ] = None, ) -> Optional[List[str]]: - return str2list(val) + if val: + return val.split(",") + return None def _ids_converter( @@ -70,7 +72,9 @@ def _ids_converter( ), ] = None, ) -> Optional[List[str]]: - return str2list(val) + if val: + return val.split(",") + return None def _bbox_converter( @@ -85,7 +89,9 @@ def _bbox_converter( ), ] = None, ) -> Optional[BBox]: - return str2bbox(val) + if val: + return str2bbox(val) + return None def _validate_datetime(instance, attribute, value): diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index 38fdf737e..7cfdb3d65 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -1,35 +1,26 @@ """STAC types.""" -import sys -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Union from stac_pydantic.shared import BBox - -# Avoids a Pydantic error: -# TypeError: You should use `typing_extensions.TypedDict` instead of -# `typing.TypedDict` with Python < 3.12.0. Without it, there is no way to -# differentiate required and optional fields when subclassed. -if sys.version_info < (3, 12, 0): - from typing_extensions import TypedDict -else: - from typing import TypedDict +from typing_extensions import NotRequired, TypedDict NumType = Union[float, int] -class Catalog(TypedDict, total=False): +class Catalog(TypedDict): """STAC Catalog.""" type: str stac_version: str - stac_extensions: Optional[List[str]] + stac_extensions: NotRequired[List[str]] id: str - title: Optional[str] + title: NotRequired[str] description: str links: List[Dict[str, Any]] -class LandingPage(Catalog, total=False): +class LandingPage(Catalog): """STAC Landing Page.""" conformsTo: List[str] @@ -41,7 +32,7 @@ class Conformance(TypedDict): conformsTo: List[str] -class Collection(Catalog, total=False): +class Collection(Catalog): """STAC Collection.""" keywords: List[str] @@ -52,12 +43,12 @@ class Collection(Catalog, total=False): assets: Dict[str, Any] -class Item(TypedDict, total=False): +class Item(TypedDict): """STAC Item.""" type: Literal["Feature"] stac_version: str - stac_extensions: Optional[List[str]] + stac_extensions: NotRequired[List[str]] id: str geometry: Dict[str, Any] bbox: BBox @@ -67,22 +58,22 @@ class Item(TypedDict, total=False): collection: str -class ItemCollection(TypedDict, total=False): +class ItemCollection(TypedDict): """STAC Item Collection.""" type: Literal["FeatureCollection"] features: List[Item] links: List[Dict[str, Any]] - numberMatched: Optional[int] - numberReturned: Optional[int] + numberMatched: NotRequired[int] + numberReturned: NotRequired[int] -class Collections(TypedDict, total=False): +class Collections(TypedDict): """All collections endpoint. https://github.com/radiantearth/stac-api-spec/tree/master/collections """ collections: List[Collection] links: List[Dict[str, Any]] - numberMatched: Optional[int] = None - numberReturned: Optional[int] = None + numberMatched: NotRequired[int] + numberReturned: NotRequired[int]