From e8ed32194e67efc0d81c91b3c85b787dd3c96800 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 20 Jan 2025 11:12:48 +0100 Subject: [PATCH 1/6] update for stac-fastapi 4.0 --- .github/workflows/cicd.yaml | 1 - CHANGES.md | 15 ++++++++++++-- setup.py | 13 +++++++----- stac_fastapi/pgstac/app.py | 4 ++-- stac_fastapi/pgstac/core.py | 40 +++++++++++++++++-------------------- tests/api/test_api.py | 4 ++-- tests/conftest.py | 2 +- 7 files changed, 44 insertions(+), 35 deletions(-) diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 5afebf1..c77ba14 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -17,7 +17,6 @@ jobs: - {python: '3.11', pypgstac: '0.8.*'} - {python: '3.10', pypgstac: '0.8.*'} - {python: '3.9', pypgstac: '0.8.*'} - - {python: '3.8', pypgstac: '0.8.*'} timeout-minutes: 20 diff --git a/CHANGES.md b/CHANGES.md index 99aa863..6412029 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,15 +2,26 @@ ## [Unreleased] -- Fix Docker compose file, so example data can be loaded into database (author @zstatmanweil, ) +### Changed + - Handle `next` and `dev` tokens now returned as links from pgstac>=0.9.0 (author @zstatmanweil, https://github.com/stac-utils/stac-fastapi-pgstac/pull/140) - Add collection search extension ([#139](https://github.com/stac-utils/stac-fastapi-pgstac/pull/139)) - keep `/search` and `/collections` extensions separate ([#158](https://github.com/stac-utils/stac-fastapi-pgstac/pull/158)) -- Fix `filter` extension implementation in `CoreCrudClient` - update `pypgstac` requirement to `>=0.8,<0.10` - set `pypgstac==0.9.*` for test requirements +- update `stac-fastapi-*` requirement to `~=4.0` +- remove `python 3.8` support +- renamed `post_request_model` attribute to `pgstac_search_model` in `CoreCrudClient` class +- changed `datetime` input type to `sting` in GET endpoint methods +- renamed `filter` to `filter_expr` input attributes in GET endpoint methods + +### Fixed + +- Fix Docker compose file, so example data can be loaded into database (author @zstatmanweil, ) +- Fix `filter` extension implementation in `CoreCrudClient` ## [3.0.1] - 2024-11-14 + - Enable runtime `CORS` configuration using environment variables (`CORS_ORIGIN="https://...,https://..."`, `CORS_METHODS="PUT,OPTIONS"`) (https://github.com/stac-utils/stac-fastapi-pgstac/pull/168) ## [3.0.0] - 2024-08-02 diff --git a/setup.py b/setup.py index 16edf78..09acc92 100644 --- a/setup.py +++ b/setup.py @@ -10,9 +10,9 @@ "orjson", "pydantic", "stac_pydantic==3.1.*", - "stac-fastapi.api~=3.0.3", - "stac-fastapi.extensions~=3.0.3", - "stac-fastapi.types~=3.0.3", + "stac-fastapi.api~=4.0", + "stac-fastapi.extensions~=4.0", + "stac-fastapi.types~=4.0", "asyncpg", "buildpg", "brotli_asgi", @@ -46,12 +46,15 @@ description="An implementation of STAC API based on the FastAPI framework and using the pgstac backend.", long_description=desc, long_description_content_type="text/markdown", - python_requires=">=3.8", + python_requires=">=3.9", classifiers=[ "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "License :: OSI Approved :: MIT License", ], keywords="STAC FastAPI COG", diff --git a/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/app.py index 13f2c05..069d972 100644 --- a/stac_fastapi/pgstac/app.py +++ b/stac_fastapi/pgstac/app.py @@ -117,7 +117,7 @@ async def lifespan(app: FastAPI): openapi_url=settings.openapi_url, docs_url=settings.docs_url, redoc_url=None, - root_path=getattr(settings, "root_path", None), + root_path=settings.root_path, lifespan=lifespan, ) @@ -128,7 +128,7 @@ async def lifespan(app: FastAPI): extensions=extensions + [collection_search_extension] if collection_search_extension else extensions, - client=CoreCrudClient(post_request_model=post_request_model), # type: ignore + client=CoreCrudClient(pgstac_search_model=post_request_model), response_class=ORJSONResponse, items_get_request_model=items_get_request_model, search_get_request_model=get_request_model, diff --git a/stac_fastapi/pgstac/core.py b/stac_fastapi/pgstac/core.py index da91138..12455e4 100644 --- a/stac_fastapi/pgstac/core.py +++ b/stac_fastapi/pgstac/core.py @@ -2,7 +2,7 @@ import json import re -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, List, Optional, Set, Type, Union from urllib.parse import unquote_plus, urljoin import attr @@ -18,7 +18,6 @@ from stac_fastapi.types.core import AsyncBaseCoreClient, Relations from stac_fastapi.types.errors import InvalidQueryParameter, NotFoundError from stac_fastapi.types.requests import get_base_url -from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.stac import Collection, Collections, Item, ItemCollection from stac_pydantic.shared import BBox, MimeTypes @@ -31,7 +30,7 @@ PagingLinks, ) from stac_fastapi.pgstac.types.search import PgstacSearch -from stac_fastapi.pgstac.utils import filter_fields, format_datetime_range +from stac_fastapi.pgstac.utils import filter_fields NumType = Union[float, int] @@ -40,18 +39,20 @@ class CoreCrudClient(AsyncBaseCoreClient): """Client for core endpoints defined by stac.""" + pgstac_search_model: Type[PgstacSearch] = attr.ib(default=PgstacSearch) + async def all_collections( # noqa: C901 self, request: Request, # Extensions bbox: Optional[BBox] = None, - datetime: Optional[DateTimeType] = None, + datetime: Optional[str] = None, limit: Optional[int] = None, offset: Optional[int] = None, query: Optional[str] = None, fields: Optional[List[str]] = None, sortby: Optional[str] = None, - filter: Optional[str] = None, + filter_expr: Optional[str] = None, filter_lang: Optional[str] = None, **kwargs, ) -> Collections: @@ -82,7 +83,7 @@ async def all_collections( # noqa: C901 datetime=datetime, fields=fields, sortby=sortby, - filter_query=filter, + filter_query=filter_expr, filter_lang=filter_lang, ) @@ -234,9 +235,6 @@ async def _search_base( # noqa: C901 settings: Settings = request.app.state.settings - if search_request.datetime: - search_request.datetime = format_datetime_range(search_request.datetime) - search_request.conf = search_request.conf or {} search_request.conf["nohydrate"] = settings.use_api_hydrate @@ -340,7 +338,7 @@ async def item_collection( collection_id: str, request: Request, bbox: Optional[BBox] = None, - datetime: Optional[DateTimeType] = None, + datetime: Optional[str] = None, limit: Optional[int] = None, token: Optional[str] = None, **kwargs, @@ -360,9 +358,6 @@ async def item_collection( # If collection does not exist, NotFoundError wil be raised await self.get_collection(collection_id, request=request) - if datetime: - datetime = format_datetime_range(datetime) - base_args = { "collections": [collection_id], "bbox": bbox, @@ -373,7 +368,7 @@ async def item_collection( if self.extension_is_enabled("FilterExtension"): filter_lang = kwargs.get("filter_lang", None) - filter_query = kwargs.get("filter", None) + filter_query = kwargs.get("filter_expr", None) if filter_query: if filter_lang == "cql2-text": filter_query = to_cql2(parse_cql2_text(filter_query)) @@ -387,7 +382,7 @@ async def item_collection( if v is not None and v != []: clean[k] = v - search_request = self.post_request_model(**clean) + search_request = self.pgstac_search_model(**clean) item_collection = await self._search_base(search_request, request=request) links = await ItemCollectionLinks( @@ -414,7 +409,7 @@ async def get_item( # If collection does not exist, NotFoundError wil be raised await self.get_collection(collection_id, request=request) - search_request = self.post_request_model( + search_request = self.pgstac_search_model( ids=[item_id], collections=[collection_id], limit=1 ) item_collection = await self._search_base(search_request, request=request) @@ -456,14 +451,14 @@ async def get_search( ids: Optional[List[str]] = None, bbox: Optional[BBox] = None, intersects: Optional[str] = None, - datetime: Optional[DateTimeType] = None, + datetime: Optional[str] = None, limit: Optional[int] = None, # Extensions query: Optional[str] = None, token: Optional[str] = None, fields: Optional[List[str]] = None, sortby: Optional[str] = None, - filter: Optional[str] = None, + filter_expr: Optional[str] = None, filter_lang: Optional[str] = None, **kwargs, ) -> ItemCollection: @@ -490,13 +485,13 @@ async def get_search( datetime=datetime, fields=fields, sortby=sortby, - filter_query=filter, + filter_query=filter_expr, filter_lang=filter_lang, ) # Do the request try: - search_request = self.post_request_model(**clean) + search_request = self.pgstac_search_model(**clean) except ValidationError as e: raise HTTPException( status_code=400, detail=f"Invalid parameters provided {e}" @@ -508,7 +503,7 @@ def _clean_search_args( # noqa: C901 self, base_args: Dict[str, Any], intersects: Optional[str] = None, - datetime: Optional[DateTimeType] = None, + datetime: Optional[str] = None, fields: Optional[List[str]] = None, sortby: Optional[str] = None, filter_query: Optional[str] = None, @@ -524,7 +519,7 @@ def _clean_search_args( # noqa: C901 base_args["filter_lang"] = filter_lang if datetime: - base_args["datetime"] = format_datetime_range(datetime) + base_args["datetime"] = datetime if intersects: base_args["intersects"] = orjson.loads(unquote_plus(intersects)) @@ -553,6 +548,7 @@ def _clean_search_args( # noqa: C901 includes.add(field[1:]) else: includes.add(field) + base_args["fields"] = {"include": includes, "exclude": excludes} # Remove None values from dict diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 34c75f0..6ce3d60 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -738,7 +738,7 @@ async def get_collection( ) api = StacApi( - client=Client(post_request_model=post_request_model), + client=Client(pgstac_search_model=post_request_model), settings=settings, extensions=extensions, search_post_request_model=post_request_model, @@ -794,7 +794,7 @@ async def test_no_extension( extensions = [] post_request_model = create_post_request_model(extensions, base_model=PgstacSearch) api = StacApi( - client=CoreCrudClient(post_request_model=post_request_model), + client=CoreCrudClient(pgstac_search_model=post_request_model), settings=settings, extensions=extensions, search_post_request_model=post_request_model, diff --git a/tests/conftest.py b/tests/conftest.py index 4b18014..f6b272f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -173,7 +173,7 @@ def api_client(request, database): api = StacApi( settings=api_settings, extensions=extensions + [collection_search_extension], - client=CoreCrudClient(post_request_model=search_post_request_model), + client=CoreCrudClient(pgstac_search_model=search_post_request_model), items_get_request_model=items_get_request_model, search_get_request_model=search_get_request_model, search_post_request_model=search_post_request_model, From ba1f65dff3398e1c1f790a57cc0e96415b0e582f Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 20 Jan 2025 12:00:08 +0100 Subject: [PATCH 2/6] update docker images --- Dockerfile | 2 +- Dockerfile.docs | 2 +- Dockerfile.tests | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5015019..f699d79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG PYTHON_VERSION=3.11 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION}-slim as base diff --git a/Dockerfile.docs b/Dockerfile.docs index db7d104..52affdb 100644 --- a/Dockerfile.docs +++ b/Dockerfile.docs @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.12-slim # build-essential is required to build a wheel for ciso8601 RUN apt update && apt install -y build-essential diff --git a/Dockerfile.tests b/Dockerfile.tests index 4af0dd7..c99b7c4 100644 --- a/Dockerfile.tests +++ b/Dockerfile.tests @@ -1,4 +1,4 @@ -ARG PYTHON_VERSION=3.11 +ARG PYTHON_VERSION=3.12 FROM python:${PYTHON_VERSION}-slim as base From 8ae834488f645426adc99dcbbdd3b6be18cc31bb Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 20 Jan 2025 13:21:27 +0100 Subject: [PATCH 3/6] use 3.10 for docs --- Dockerfile.docs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.docs b/Dockerfile.docs index 52affdb..5cb3eff 100644 --- a/Dockerfile.docs +++ b/Dockerfile.docs @@ -1,4 +1,4 @@ -FROM python:3.12-slim +FROM python:3.10-slim # build-essential is required to build a wheel for ciso8601 RUN apt update && apt install -y build-essential From 8a4f908f04d558ac0827800927c2630971d22e54 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Mon, 20 Jan 2025 13:23:25 +0100 Subject: [PATCH 4/6] update docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3aba52a..f7414ca 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ PgSTAC stores all collection and item records as jsonb fields exactly as they co | --| --| | 2.5 | >=0.7,<0.8 | | 3.0 | >=0.8,<0.9 | +| 4.0 | >=0.8,<0.10 | ## Usage From 2078ac71c975893407188a6a1435e34d61f6c3c3 Mon Sep 17 00:00:00 2001 From: Vincent Sarago Date: Tue, 21 Jan 2025 13:04:06 +0100 Subject: [PATCH 5/6] Update CHANGES.md Co-authored-by: Henry Rodman --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6412029..40249b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ - update `stac-fastapi-*` requirement to `~=4.0` - remove `python 3.8` support - renamed `post_request_model` attribute to `pgstac_search_model` in `CoreCrudClient` class -- changed `datetime` input type to `sting` in GET endpoint methods +- changed `datetime` input type to `string` in GET endpoint methods - renamed `filter` to `filter_expr` input attributes in GET endpoint methods ### Fixed From 428b0807e39fe8a624cdda1613e0b24b248d2a17 Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Tue, 21 Jan 2025 13:35:43 +0100 Subject: [PATCH 6/6] remove format_datetime_range function --- CHANGES.md | 1 + stac_fastapi/pgstac/utils.py | 33 --------------------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 40249b0..870f0a4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,7 @@ - renamed `post_request_model` attribute to `pgstac_search_model` in `CoreCrudClient` class - changed `datetime` input type to `string` in GET endpoint methods - renamed `filter` to `filter_expr` input attributes in GET endpoint methods +- delete `utils.format_datetime_range` function ### Fixed diff --git a/stac_fastapi/pgstac/utils.py b/stac_fastapi/pgstac/utils.py index bd0efff..e03e4a3 100644 --- a/stac_fastapi/pgstac/utils.py +++ b/stac_fastapi/pgstac/utils.py @@ -1,9 +1,7 @@ """stac-fastapi utility methods.""" -from datetime import datetime from typing import Any, Dict, Optional, Set, Union -from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.stac import Item @@ -114,34 +112,3 @@ def dict_deep_update(merge_to: Dict[str, Any], merge_from: Dict[str, Any]) -> No dict_deep_update(merge_to[k], merge_from[k]) else: merge_to[k] = v - - -def format_datetime_range(dt_range: Union[DateTimeType, str]) -> str: - """ - Convert a datetime object or a tuple of datetime objects to a formatted string for datetime ranges. - - Args: - dt_range (DateTimeType): The date interval, - which might be a single datetime or a tuple with one or two datetimes. - - Returns: - str: A formatted string like 'YYYY-MM-DDTHH:MM:SSZ/..', 'YYYY-MM-DDTHH:MM:SSZ', or the original string input. - """ - # Handle a single datetime object - if isinstance(dt_range, datetime): - return dt_range.isoformat().replace("+00:00", "Z") - - # Handle a tuple containing datetime objects or None - elif isinstance(dt_range, tuple): - start, end = dt_range - - # Convert start datetime to string if not None, otherwise use ".." - start_str = start.isoformat().replace("+00:00", "Z") if start else ".." - - # Convert end datetime to string if not None, otherwise use ".." - end_str = end.isoformat().replace("+00:00", "Z") if end else ".." - - return f"{start_str}/{end_str}" - - # Return input as-is if it's not any expected type (fallback) - return dt_range