From c55c2537728f4df49f5235cc49574f90f8c709b1 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Wed, 17 Jul 2024 11:09:34 +0200 Subject: [PATCH] add description and examples (#734) * add description and examples * update docs * update changelog * Update stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py --- CHANGES.md | 2 + docs/src/migrations/v3.0.0.md | 32 +++- stac_fastapi/api/stac_fastapi/api/models.py | 10 +- .../extensions/core/aggregation/request.py | 18 ++- .../extensions/core/fields/request.py | 23 ++- .../extensions/core/filter/request.py | 64 ++++++-- .../extensions/core/query/query.py | 2 +- .../extensions/core/query/request.py | 20 ++- .../extensions/core/sort/request.py | 31 +++- .../types/stac_fastapi/types/search.py | 138 ++++++++++++++++-- 10 files changed, 293 insertions(+), 47 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 649bd2edf..1cc1773c4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## [Unreleased] - TBD +* add more openapi metadata in input models [#734](https://github.com/stac-utils/stac-fastapi/pull/734) + ## [3.0.0b2] - 2024-07-09 ### Changed diff --git a/docs/src/migrations/v3.0.0.md b/docs/src/migrations/v3.0.0.md index e9b2ee649..f781687c3 100644 --- a/docs/src/migrations/v3.0.0.md +++ b/docs/src/migrations/v3.0.0.md @@ -95,14 +95,12 @@ e.g the BaseSearchGetRequest, default for the `GET - /search` endpoint: class BaseSearchGetRequest(APIRequest): """Base arguments for GET Request.""" - collections: Annotated[Optional[str], Query()] = attr.ib( - default=None, converter=str2list - ) - ids: Annotated[Optional[str], Query()] = attr.ib(default=None, converter=str2list) - bbox: Annotated[Optional[BBox], Query()] = attr.ib(default=None, converter=str2bbox) + collections: Optional[List[str]] = attr.ib(default=None, converter=_collection_converter) + ids: Optional[List[str]] = attr.ib(default=None, converter=_ids_converter) + bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) intersects: Annotated[Optional[str], Query()] = attr.ib(default=None) - datetime: Annotated[Optional[DateTimeType], Query()] = attr.ib( - default=None, converter=str_to_interval + datetime: Optional[DateTimeType] = attr.ib( + default=None, converter=_datetime_converter ) limit: Annotated[Optional[int], Query()] = attr.ib(default=10) ``` @@ -115,6 +113,26 @@ class SomeRequest(APIRequest): user_number: Annotated[Optional[int], Query(alias="user-number")] = attr.ib(default=None) ``` +Note: when an attribute has a `converter` (e.g `_ids_converter`), the **Type Hint** should be defined directly in the converter: + +```python +def _ids_converter( + val: Annotated[ + Optional[str], + Query( + description="Array of Item ids to return.", + ), + ] = None, +) -> Optional[List[str]]: + return str2list(val) + +@attr.s +class BaseSearchGetRequest(APIRequest): + """Base arguments for GET Request.""" + + ids: Optional[List[str]] = attr.ib(default=None, converter=_ids_converter) +``` + ## Filter extension `default_includes` attribute has been removed from the `ApiSettings` object. If you need `defaults` includes you can overwrite the `FieldExtension` models (see https://github.com/stac-utils/stac-fastapi/pull/706). diff --git a/stac_fastapi/api/stac_fastapi/api/models.py b/stac_fastapi/api/stac_fastapi/api/models.py index 737089c2d..5a239b9f0 100644 --- a/stac_fastapi/api/stac_fastapi/api/models.py +++ b/stac_fastapi/api/stac_fastapi/api/models.py @@ -14,8 +14,8 @@ APIRequest, BaseSearchGetRequest, BaseSearchPostRequest, - str2bbox, - str_to_interval, + _bbox_converter, + _datetime_converter, ) try: @@ -114,9 +114,9 @@ class ItemCollectionUri(APIRequest): collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() limit: Annotated[int, Query()] = attr.ib(default=10) - bbox: Annotated[Optional[BBox], Query()] = attr.ib(default=None, converter=str2bbox) - datetime: Annotated[Optional[DateTimeType], Query()] = attr.ib( - default=None, converter=str_to_interval + bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) + datetime: Optional[DateTimeType] = attr.ib( + default=None, converter=_datetime_converter ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py index 1f4b6a93b..4e72e0005 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py @@ -14,16 +14,26 @@ ) +def _agg_converter( + val: Annotated[ + Optional[str], + Query(description="A list of aggregations to compute and return."), + ] = None, +) -> Optional[List[str]]: + return str2list(val) + + @attr.s class AggregationExtensionGetRequest(BaseSearchGetRequest): """Aggregation Extension GET request model.""" - aggregations: Annotated[Optional[str], Query()] = attr.ib( - default=None, converter=str2list - ) + aggregations: Optional[List[str]] = attr.ib(default=None, converter=_agg_converter) class AggregationExtensionPostRequest(BaseSearchPostRequest): """Aggregation Extension POST request model.""" - aggregations: Optional[List[str]] = Field(default=None) + aggregations: Optional[List[str]] = Field( + default=None, + description="A list of aggregations to compute and return.", + ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py index e0c42a574..d3737ea49 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py @@ -1,7 +1,7 @@ """Request models for the fields extension.""" import warnings -from typing import Dict, Optional, Set +from typing import Dict, List, Optional, Set import attr from fastapi import Query @@ -70,14 +70,31 @@ def filter_fields(self) -> Dict: } +def _fields_converter( + val: Annotated[ + Optional[str], + Query( + description="Include or exclude fields from items body.", + json_schema_extra={ + "example": "properties.datetime", + }, + ), + ] = None, +) -> Optional[List[str]]: + return str2list(val) + + @attr.s class FieldsExtensionGetRequest(APIRequest): """Additional fields for the GET request.""" - fields: Annotated[Optional[str], Query()] = attr.ib(default=None, converter=str2list) + fields: Optional[List[str]] = attr.ib(default=None, converter=_fields_converter) class FieldsExtensionPostRequest(BaseModel): """Additional fields and schema for the POST request.""" - fields: Optional[PostFieldsExtension] = Field(PostFieldsExtension()) + fields: Optional[PostFieldsExtension] = Field( + PostFieldsExtension(), + description="Include or exclude fields from items body.", + ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py index 917f5f086..30ac011b0 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py @@ -16,18 +16,62 @@ class FilterExtensionGetRequest(APIRequest): """Filter extension GET request model.""" - filter: Annotated[Optional[str], Query()] = attr.ib(default=None) - filter_crs: Annotated[Optional[str], Query(alias="filter-crs")] = attr.ib( - default=None - ) - filter_lang: Annotated[Optional[FilterLang], Query(alias="filter-lang")] = attr.ib( - default="cql2-text" - ) + filter: Annotated[ + Optional[str], + Query( + description="""A CQL filter expression for filtering items.\n +Supports `CQL-JSON` as defined in https://portal.ogc.org/files/96288\n +Remember to URL encode the CQL-JSON if using GET""", + json_schema_extra={ + "example": "id='LC08_L1TP_060247_20180905_20180912_01_T1_L1TP' AND collection='landsat8_l1tp'", # noqa: E501 + }, + ), + ] = attr.ib(default=None) + filter_crs: Annotated[ + Optional[str], + Query( + alias="filter-crs", + description="The coordinate reference system (CRS) used by spatial literals in the 'filter' value. Default is `http://www.opengis.net/def/crs/OGC/1.3/CRS84`", # noqa: E501 + ), + ] = attr.ib(default=None) + filter_lang: Annotated[ + Optional[FilterLang], + Query( + alias="filter-lang", + description="The CQL filter encoding that the 'filter' value uses.", + ), + ] = attr.ib(default="cql2-text") class FilterExtensionPostRequest(BaseModel): """Filter extension POST request model.""" - filter: Optional[Dict[str, Any]] = None - filter_crs: Optional[str] = Field(alias="filter-crs", default=None) - filter_lang: Optional[FilterLang] = Field(alias="filter-lang", default="cql2-json") + filter: Optional[Dict[str, Any]] = Field( + default=None, + description="A CQL filter expression for filtering items.", + json_schema_extra={ + "example": { + "op": "and", + "args": [ + { + "op": "=", + "args": [ + {"property": "id"}, + "LC08_L1TP_060247_20180905_20180912_01_T1_L1TP", + ], + }, + {"op": "=", "args": [{"property": "collection"}, "landsat8_l1tp"]}, + ], + }, + }, + ) + filter_crs: Optional[str] = Field( + alias="filter-crs", + default=None, + description="The coordinate reference system (CRS) used by spatial literals in the 'filter' value. Default is `http://www.opengis.net/def/crs/OGC/1.3/CRS84`", # noqa: E501 + ) + filter_lang: Optional[FilterLang] = Field( + alias="filter-lang", + default="cql2-json", + description="The CQL filter encoding that the 'filter' value uses.", + ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py index dcb162060..472c385b4 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py @@ -17,7 +17,7 @@ class QueryExtension(ApiExtension): The Query extension adds an additional `query` parameter to `/search` requests which allows the caller to perform queries against item metadata (ex. find all images with cloud cover less than 15%). - https://github.com/radiantearth/stac-api-spec/blob/master/item-search/README.md#query + https://github.com/stac-api-extensions/query """ GET = QueryExtensionGetRequest diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py index 5d403a677..ad7f461c3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py @@ -4,7 +4,7 @@ import attr from fastapi import Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from typing_extensions import Annotated from stac_fastapi.types.search import APIRequest @@ -14,10 +14,24 @@ class QueryExtensionGetRequest(APIRequest): """Query Extension GET request model.""" - query: Annotated[Optional[str], Query()] = attr.ib(default=None) + query: Annotated[ + Optional[str], + Query( + description="Allows additional filtering based on the properties of Item objects", # noqa: E501 + json_schema_extra={ + "example": '{"eo:cloud_cover": {"gte": 95}}', + }, + ), + ] = attr.ib(default=None) class QueryExtensionPostRequest(BaseModel): """Query Extension POST request model.""" - query: Optional[Dict[str, Dict[str, Any]]] = None + query: Optional[Dict[str, Dict[str, Any]]] = Field( + None, + description="Allows additional filtering based on the properties of Item objects", # noqa: E501 + json_schema_extra={ + "example": {"eo:cloud_cover": {"gte": 95}}, + }, + ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py index 8eeccba0c..e1c22eea3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py @@ -4,21 +4,46 @@ import attr from fastapi import Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from stac_pydantic.api.extensions.sort import SortExtension as PostSortModel from typing_extensions import Annotated from stac_fastapi.types.search import APIRequest, str2list +def _sort_converter( + val: Annotated[ + Optional[str], + Query( + description="An array of property names, prefixed by either '+' for ascending or '-' for descending. If no prefix is provided, '+' is assumed.", # noqa: E501 + json_schema_extra={ + "example": "-gsd,-datetime", + }, + ), + ], +) -> Optional[List[str]]: + return str2list(val) + + @attr.s class SortExtensionGetRequest(APIRequest): """Sortby Parameter for GET requests.""" - sortby: Annotated[Optional[str], Query()] = attr.ib(default=None, converter=str2list) + sortby: Optional[List[str]] = attr.ib(default=None, converter=_sort_converter) class SortExtensionPostRequest(BaseModel): """Sortby parameter for POST requests.""" - sortby: Optional[List[PostSortModel]] = None + sortby: Optional[List[PostSortModel]] = Field( + None, + description="An array of property (field) names, and direction in form of '{'field': '', 'direction':''}'", # noqa: E501 + json_schema_extra={ + "example": [ + { + "field": "properties.created", + "direction": "asc", + } + ], + }, + ) diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index b8ae23c86..064ae10cb 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -6,7 +6,7 @@ import attr from fastapi import Query -from pydantic import PositiveInt +from pydantic import Field, PositiveInt from pydantic.functional_validators import AfterValidator from stac_pydantic.api import Search from stac_pydantic.shared import BBox @@ -23,11 +23,13 @@ def crop(v: PositiveInt) -> PositiveInt: return v -def str2list(x: str) -> Optional[List]: +def str2list(x: str) -> Optional[List[str]]: """Convert string to list base on , delimiter.""" if x: return x.split(",") + return None + def str2bbox(x: str) -> Optional[BBox]: """Convert string to BBox based on , delimiter.""" @@ -36,6 +38,68 @@ def str2bbox(x: str) -> Optional[BBox]: assert len(t) == 4 return t + return None + + +def _collection_converter( + val: Annotated[ + Optional[str], + Query( + description="Array of collection Ids to search for items.", + json_schema_extra={ + "example": "collection1,collection2", + }, + ), + ] = None, +) -> Optional[List[str]]: + return str2list(val) + + +def _ids_converter( + val: Annotated[ + Optional[str], + Query( + description="Array of Item ids to return.", + json_schema_extra={ + "example": "item1,item2", + }, + ), + ] = None, +) -> Optional[List[str]]: + return str2list(val) + + +def _bbox_converter( + val: Annotated[ + Optional[str], + Query( + description="Only return items intersecting this bounding box. Mutually exclusive with **intersects**.", # noqa: E501 + json_schema_extra={ + "example": "-175.05,-85.05,175.05,85.05", + }, + ), + ] = None, +) -> Optional[BBox]: + return str2bbox(val) + + +def _datetime_converter( + val: Annotated[ + Optional[str], + Query( + description="""Only return items that have a temporal property that intersects this value.\n +Either a date-time or an interval, open or closed. Date and time expressions adhere to RFC 3339. Open intervals are expressed using double-dots.""", # noqa: E501 + openapi_examples={ + "datetime": {"value": "2018-02-12T23:20:50Z"}, + "closed-interval": {"value": "2018-02-12T00:00:00Z/2018-03-18T12:31:12Z"}, + "open-interval-from": {"value": "2018-02-12T00:00:00Z/.."}, + "open-interval-to": {"value": "../2018-03-18T12:31:12Z"}, + }, + ), + ] = None, +): + return str_to_interval(val) + # Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287 NumType = Union[float, int] @@ -56,19 +120,71 @@ def kwargs(self) -> Dict: class BaseSearchGetRequest(APIRequest): """Base arguments for GET Request.""" - collections: Annotated[Optional[str], Query()] = attr.ib( - default=None, converter=str2list + collections: Optional[List[str]] = attr.ib( + default=None, converter=_collection_converter ) - ids: Annotated[Optional[str], Query()] = attr.ib(default=None, converter=str2list) - bbox: Annotated[Optional[BBox], Query()] = attr.ib(default=None, converter=str2bbox) - intersects: Annotated[Optional[str], Query()] = attr.ib(default=None) - datetime: Annotated[Optional[DateTimeType], Query()] = attr.ib( - default=None, converter=str_to_interval + ids: Optional[List[str]] = attr.ib(default=None, converter=_ids_converter) + bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) + intersects: Annotated[ + Optional[str], + Query( + description="""Only return items intersecting this GeoJSON Geometry. Mutually exclusive with **bbox**. \n +*Remember to URL encode the GeoJSON geometry when using GET request*.""", # noqa: E501 + openapi_examples={ + "madrid": { + "value": { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [-3.8549260500072933, 40.54923557897152], + [-3.8549260500072933, 40.29428000041938], + [-3.516597069715033, 40.29428000041938], + [-3.516597069715033, 40.54923557897152], + [-3.8549260500072933, 40.54923557897152], + ] + ], + "type": "Polygon", + }, + }, + }, + "new-york": { + "value": { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [-74.50117532354284, 41.128266394414055], + [-74.50117532354284, 40.35633909727355], + [-73.46713183168603, 40.35633909727355], + [-73.46713183168603, 41.128266394414055], + [-74.50117532354284, 41.128266394414055], + ] + ], + "type": "Polygon", + }, + }, + }, + }, + ), + ] = attr.ib(default=None) + datetime: Optional[DateTimeType] = attr.ib( + default=None, converter=_datetime_converter ) - limit: Annotated[Optional[int], Query()] = attr.ib(default=10) + limit: Annotated[ + Optional[int], + Query( + description="Limits the number of results that are included in each page of the response." # noqa: E501 + ), + ] = attr.ib(default=10) class BaseSearchPostRequest(Search): """Base arguments for POST Request.""" - limit: Optional[Limit] = 10 + limit: Optional[Limit] = Field( + 10, + description="Limits the number of results that are included in each page of the response.", # noqa: E501 + )