From 2fe92fce9b791fc296a1972561752a988cbe5d95 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 7 Feb 2026 20:06:15 +0800 Subject: [PATCH 01/11] catalogs extension scratch --- .../stac_fastapi/extensions/core/__init__.py | 2 + .../core/multi_tenant_catalogs/__init__.py | 17 + .../core/multi_tenant_catalogs/catalogs.py | 248 ++++++++ .../core/multi_tenant_catalogs/client.py | 543 ++++++++++++++++++ .../core/multi_tenant_catalogs/types.py | 31 + 5 files changed, 841 insertions(+) create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py create mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index d6b5f7589..031408b37 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -1,6 +1,7 @@ """stac_api.extensions.core module.""" from .aggregation import AggregationExtension +from .multi_tenant_catalogs import CatalogsExtension from .collection_search import CollectionSearchExtension, CollectionSearchPostExtension from .fields import FieldsExtension from .filter import ( @@ -21,6 +22,7 @@ __all__ = ( "AggregationExtension", + "CatalogsExtension", "FieldsExtension", "FilterExtension", "FreeTextExtension", diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py new file mode 100644 index 000000000..8151e2e18 --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py @@ -0,0 +1,17 @@ +"""Catalogs extension module.""" + +from stac_pydantic.api.collections import Collections + +from .catalogs import CATALOGS_CONFORMANCE_CLASSES, CatalogsExtension +from .client import AsyncBaseCatalogsClient, BaseCatalogsClient +from .types import Catalogs, Children + +__all__ = [ + "CatalogsExtension", + "AsyncBaseCatalogsClient", + "BaseCatalogsClient", + "Catalogs", + "Collections", + "Children", + "CATALOGS_CONFORMANCE_CLASSES", +] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py new file mode 100644 index 000000000..f50d689ba --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py @@ -0,0 +1,248 @@ +"""Catalogs extension.""" + +from typing import List, Optional, Type + +import attr +from fastapi import APIRouter, FastAPI, Query +from fastapi.responses import JSONResponse +from starlette.responses import Response + +from stac_pydantic.catalog import Catalog +from stac_pydantic.collection import Collection +from stac_pydantic.item import Item +from stac_pydantic.item_collection import ItemCollection +from stac_fastapi.types.extension import ApiExtension + +from .client import AsyncBaseCatalogsClient +from .types import Catalogs, Children, Collections + +CATALOGS_CONFORMANCE_CLASSES = [ + "https://api.stacspec.org/v1.0.0/core", + "https://api.stacspec.org/v1.0.0-beta.1/multi-tenant-catalogs", + "https://api.stacspec.org/v1.0.0-rc.2/children", + "https://api.stacspec.org/v1.0.0-rc.2/children#type-filter", +] + + +@attr.s +class CatalogsExtension(ApiExtension): + """Catalogs Extension. + + The Catalogs extension adds a /catalogs endpoint that returns a list of all catalogs + in the database, similar to how /collections returns a list of collections. + + Attributes: + client: A client implementing the catalogs extension pattern. + settings: Extension settings. + conformance_classes: List of conformance classes for this extension. + router: FastAPI router for the extension endpoints. + response_class: Response class for the extension. + """ + + client: AsyncBaseCatalogsClient = attr.ib(default=None) + settings: dict = attr.ib(default=attr.Factory(dict)) + conformance_classes: List[str] = attr.ib( + default=attr.Factory(lambda: CATALOGS_CONFORMANCE_CLASSES) + ) + router: APIRouter = attr.ib(default=attr.Factory(APIRouter)) + response_class: Type[Response] = attr.ib(default=JSONResponse) + + def register(self, app: FastAPI, settings=None) -> None: + """Register the extension with a FastAPI application. + + Args: + app: target FastAPI application. + settings: extension settings. + """ + self.settings = settings or {} + self.router = APIRouter() + + self.router.add_api_route( + path="/catalogs", + endpoint=self.client.get_catalogs, + methods=["GET"], + response_model=Catalogs, + response_class=self.response_class, + summary="Get All Catalogs", + description="Returns a list of all catalogs in the database.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs", + endpoint=self.client.create_catalog, + methods=["POST"], + response_model=Catalog, + response_class=self.response_class, + status_code=201, + summary="Create Catalog", + description="Create a new STAC catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}", + endpoint=self.client.get_catalog, + methods=["GET"], + response_model=Catalog, + response_class=self.response_class, + summary="Get Catalog", + description="Get a specific STAC catalog by ID.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}", + endpoint=self.client.update_catalog, + methods=["PUT"], + response_model=Catalog, + response_class=self.response_class, + summary="Update Catalog", + description="Update an existing STAC catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}", + endpoint=self.client.delete_catalog, + methods=["DELETE"], + response_class=self.response_class, + status_code=204, + summary="Delete Catalog", + description="Delete a catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/collections", + endpoint=self.client.get_catalog_collections, + methods=["GET"], + response_model=Collections, + response_class=self.response_class, + summary="Get Catalog Collections", + description="Get collections linked from a specific catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/collections", + endpoint=self.client.create_catalog_collection, + methods=["POST"], + response_model=Collection, + response_class=self.response_class, + status_code=201, + summary="Create Catalog Collection", + description="Create a new collection and link it to a specific catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/collections/{collection_id}", + endpoint=self.client.get_catalog_collection, + methods=["GET"], + response_model=Collection, + response_class=self.response_class, + summary="Get Catalog Collection", + description="Get a specific collection from a catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/collections/{collection_id}", + endpoint=self.client.unlink_catalog_collection, + methods=["DELETE"], + response_class=self.response_class, + status_code=204, + summary="Unlink Collection from Catalog", + description="Removes the link between the catalog and collection. The Collection data is NOT deleted.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/collections/{collection_id}/items", + endpoint=self.client.get_catalog_collection_items, + methods=["GET"], + response_model=ItemCollection, + response_class=self.response_class, + summary="Get Catalog Collection Items", + description="Get items from a collection in a catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}", + endpoint=self.client.get_catalog_collection_item, + methods=["GET"], + response_model=Item, + response_class=self.response_class, + summary="Get Catalog Collection Item", + description="Get a specific item from a collection in a catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/catalogs", + endpoint=self.client.get_sub_catalogs, + methods=["GET"], + response_model=Catalogs, + response_class=self.response_class, + summary="Get Catalog Sub-Catalogs", + description="Get sub-catalogs linked from a specific catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/catalogs", + endpoint=self.client.create_sub_catalog, + methods=["POST"], + response_model=Catalog, + response_class=self.response_class, + status_code=201, + summary="Create Catalog Sub-Catalog", + description="Create a new catalog and link it as a sub-catalog of a specific catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/children", + endpoint=self.client.get_catalog_children, + methods=["GET"], + response_model=Children, + response_class=self.response_class, + summary="Get Catalog Children", + description="Retrieve all children (Catalogs and Collections) of this catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/conformance", + endpoint=self.client.get_catalog_conformance, + methods=["GET"], + response_class=self.response_class, + summary="Get Catalog Conformance", + description="Get conformance classes specific to this sub-catalog.", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/queryables", + endpoint=self.client.get_catalog_queryables, + methods=["GET"], + response_class=self.response_class, + summary="Get Catalog Queryables", + description="Get queryable fields available for filtering in this sub-catalog (Filter Extension).", + tags=["Catalogs"], + ) + + self.router.add_api_route( + path="/catalogs/{catalog_id}/catalogs/{sub_catalog_id}", + endpoint=self.client.unlink_sub_catalog, + methods=["DELETE"], + response_class=self.response_class, + status_code=204, + summary="Unlink Sub-Catalog", + description="Unlink a sub-catalog from its parent. Does not delete the sub-catalog.", + tags=["Catalogs"], + ) + + app.include_router(self.router, tags=["Catalogs"]) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py new file mode 100644 index 000000000..d3b3c351c --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py @@ -0,0 +1,543 @@ +"""Catalogs extension clients.""" + +import abc +from typing import Literal, Optional + +import attr +from stac_pydantic.api.collections import Collections +from stac_pydantic.catalog import Catalog +from stac_pydantic.collection import Collection +from stac_pydantic.item import Item +from stac_pydantic.item_collection import ItemCollection + +from .types import Catalogs, Children + + +@attr.s +class AsyncBaseCatalogsClient(abc.ABC): + """Defines an async pattern for implementing the STAC catalogs extension.""" + + @abc.abstractmethod + async def get_catalogs( + self, + limit: Optional[int] = None, + token: Optional[str] = None, + **kwargs, + ) -> Catalogs: + """Get all catalogs with pagination support. + + Args: + limit: The maximum number of catalogs to return. + token: Pagination token for the next page of results. + + Returns: + Catalogs object containing catalogs and pagination links. + """ + ... + + @abc.abstractmethod + async def create_catalog(self, catalog: Catalog, **kwargs) -> Catalog: + """Create a new catalog. + + Args: + catalog: The catalog to create. + + Returns: + The created catalog. + """ + ... + + @abc.abstractmethod + async def get_catalog(self, catalog_id: str, **kwargs) -> Catalog: + """Get a specific catalog by ID. + + Args: + catalog_id: The ID of the catalog to retrieve. + + Returns: + The requested catalog. + """ + ... + + @abc.abstractmethod + async def update_catalog( + self, catalog_id: str, catalog: Catalog, **kwargs + ) -> Catalog: + """Update an existing catalog. + + Args: + catalog_id: The ID of the catalog to update. + catalog: The updated catalog data. + + Returns: + The updated catalog. + """ + ... + + @abc.abstractmethod + async def delete_catalog(self, catalog_id: str, **kwargs) -> None: + """Delete a catalog. + + Args: + catalog_id: The ID of the catalog to delete. + """ + ... + + @abc.abstractmethod + async def get_catalog_collections( + self, catalog_id: str, **kwargs + ) -> Collections: + """Get collections linked from a specific catalog. + + Args: + catalog_id: The ID of the catalog. + + Returns: + Collections object containing collections linked from the catalog. + """ + ... + + @abc.abstractmethod + async def get_sub_catalogs( + self, + catalog_id: str, + limit: Optional[int] = None, + token: Optional[str] = None, + **kwargs, + ) -> Catalogs: + """Get all sub-catalogs of a specific catalog with pagination support. + + Args: + catalog_id: The ID of the parent catalog. + limit: Maximum number of results to return. + token: Pagination token for cursor-based pagination. + + Returns: + A Catalogs response containing sub-catalogs with pagination links. + """ + ... + + @abc.abstractmethod + async def create_sub_catalog( + self, catalog_id: str, catalog: Catalog, **kwargs + ) -> Catalog: + """Create a new catalog or link an existing catalog as a sub-catalog. + + Supports two modes: + - Mode A (Creation): Full Catalog JSON body with id that doesn't exist → creates new catalog + - Mode B (Linking): Minimal body with just id of existing catalog → links as sub-catalog + + Args: + catalog_id: The ID of the parent catalog. + catalog: The catalog to create or link. + + Returns: + The created or linked catalog. + """ + ... + + @abc.abstractmethod + async def create_catalog_collection( + self, catalog_id: str, collection: Collection, **kwargs + ) -> Collection: + """Create a new collection or link an existing collection to a specific catalog. + + Supports two modes: + - Mode A (Creation): Full Collection JSON body with id that doesn't exist → creates new collection + - Mode B (Linking): Minimal body with just id of existing collection → links to catalog + + Args: + catalog_id: The ID of the catalog to link the collection to. + collection: The collection to create or link. + + Returns: + The created or linked collection. + """ + ... + + @abc.abstractmethod + async def get_catalog_collection( + self, catalog_id: str, collection_id: str, **kwargs + ) -> Collection: + """Get a specific collection from a catalog. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + + Returns: + The requested collection. + """ + ... + + @abc.abstractmethod + async def unlink_catalog_collection( + self, catalog_id: str, collection_id: str, **kwargs + ) -> None: + """Unlink a collection from a catalog. + + Removes the link between the catalog and collection. + The Collection data is NOT deleted. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + """ + ... + + @abc.abstractmethod + async def get_catalog_collection_items( + self, + catalog_id: str, + collection_id: str, + **kwargs, + ) -> ItemCollection: + """Get items from a collection in a catalog. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + + Returns: + ItemCollection containing items from the collection. + """ + ... + + @abc.abstractmethod + async def get_catalog_collection_item( + self, catalog_id: str, collection_id: str, item_id: str, **kwargs + ) -> Item: + """Get a specific item from a collection in a catalog. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + item_id: The ID of the item. + + Returns: + The requested item. + """ + ... + + @abc.abstractmethod + async def get_catalog_children( + self, + catalog_id: str, + limit: Optional[int] = None, + token: Optional[str] = None, + type: Optional[Literal["Catalog", "Collection"]] = None, + **kwargs, + ) -> Children: + """Get all children (Catalogs and Collections) of a specific catalog. + + Args: + catalog_id: The ID of the catalog. + limit: Maximum number of results to return. + token: Pagination token. + type: Filter by resource type (Catalog or Collection). + + Returns: + Dictionary containing children and pagination links. + """ + ... + + @abc.abstractmethod + async def get_catalog_conformance(self, catalog_id: str, **kwargs) -> dict: + """Get conformance classes specific to this sub-catalog. + + Args: + catalog_id: The ID of the catalog. + + Returns: + Dictionary containing conformance classes. + """ + ... + + @abc.abstractmethod + async def get_catalog_queryables(self, catalog_id: str, **kwargs) -> dict: + """Get queryable fields available for filtering in this sub-catalog. + + Args: + catalog_id: The ID of the catalog. + + Returns: + Dictionary containing queryable fields (Filter Extension). + """ + ... + + @abc.abstractmethod + async def unlink_sub_catalog( + self, catalog_id: str, sub_catalog_id: str, **kwargs + ) -> None: + """Unlink a sub-catalog from its parent. + + Args: + catalog_id: The ID of the parent catalog. + sub_catalog_id: The ID of the sub-catalog to unlink. + """ + ... + + +@attr.s +class BaseCatalogsClient(abc.ABC): + """Defines a synchronous pattern for implementing the STAC catalogs extension.""" + + @abc.abstractmethod + def get_catalogs( + self, + limit: Optional[int] = None, + token: Optional[str] = None, + **kwargs, + ) -> Catalogs: + """Get all catalogs with pagination support. + + Args: + limit: The maximum number of catalogs to return. + token: Pagination token for the next page of results. + + Returns: + Catalogs object containing catalogs and pagination links. + """ + ... + + @abc.abstractmethod + def create_catalog(self, catalog: Catalog, **kwargs) -> Catalog: + """Create a new catalog. + + Args: + catalog: The catalog to create. + + Returns: + The created catalog. + """ + ... + + @abc.abstractmethod + def get_catalog(self, catalog_id: str, **kwargs) -> Catalog: + """Get a specific catalog by ID. + + Args: + catalog_id: The ID of the catalog to retrieve. + + Returns: + The requested catalog. + """ + ... + + @abc.abstractmethod + def update_catalog( + self, catalog_id: str, catalog: Catalog, **kwargs + ) -> Catalog: + """Update an existing catalog. + + Args: + catalog_id: The ID of the catalog to update. + catalog: The updated catalog data. + + Returns: + The updated catalog. + """ + ... + + @abc.abstractmethod + def delete_catalog(self, catalog_id: str, **kwargs) -> None: + """Delete a catalog. + + Args: + catalog_id: The ID of the catalog to delete. + """ + ... + + @abc.abstractmethod + def get_catalog_collections( + self, catalog_id: str, **kwargs + ) -> Collections: + """Get collections linked from a specific catalog. + + Args: + catalog_id: The ID of the catalog. + + Returns: + Collections object containing collections linked from the catalog. + """ + ... + + @abc.abstractmethod + def get_sub_catalogs( + self, + catalog_id: str, + limit: Optional[int] = None, + token: Optional[str] = None, + **kwargs, + ) -> Catalogs: + """Get all sub-catalogs of a specific catalog with pagination support. + + Args: + catalog_id: The ID of the parent catalog. + limit: Maximum number of results to return. + token: Pagination token for cursor-based pagination. + + Returns: + A Catalogs response containing sub-catalogs with pagination links. + """ + ... + + @abc.abstractmethod + def create_sub_catalog( + self, catalog_id: str, catalog: Catalog, **kwargs + ) -> Catalog: + """Create a new catalog or link an existing catalog as a sub-catalog. + + Supports two modes: + - Mode A (Creation): Full Catalog JSON body with id that doesn't exist → creates new catalog + - Mode B (Linking): Minimal body with just id of existing catalog → links as sub-catalog + + Args: + catalog_id: The ID of the parent catalog. + catalog: The catalog to create or link. + + Returns: + The created or linked catalog. + """ + ... + + @abc.abstractmethod + def create_catalog_collection( + self, catalog_id: str, collection: Collection, **kwargs + ) -> Collection: + """Create a new collection or link an existing collection to a specific catalog. + + Supports two modes: + - Mode A (Creation): Full Collection JSON body with id that doesn't exist → creates new collection + - Mode B (Linking): Minimal body with just id of existing collection → links to catalog + + Args: + catalog_id: The ID of the catalog to link the collection to. + collection: The collection to create or link. + + Returns: + The created or linked collection. + """ + ... + + @abc.abstractmethod + def get_catalog_collection( + self, catalog_id: str, collection_id: str, **kwargs + ) -> Collection: + """Get a specific collection from a catalog. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + + Returns: + The requested collection. + """ + ... + + @abc.abstractmethod + def unlink_catalog_collection( + self, catalog_id: str, collection_id: str, **kwargs + ) -> None: + """Unlink a collection from a catalog. + + Removes the link between the catalog and collection. + The Collection data is NOT deleted. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + """ + ... + + @abc.abstractmethod + def get_catalog_collection_items( + self, + catalog_id: str, + collection_id: str, + **kwargs, + ) -> ItemCollection: + """Get items from a collection in a catalog. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + + Returns: + ItemCollection containing items from the collection. + """ + ... + + @abc.abstractmethod + def get_catalog_collection_item( + self, catalog_id: str, collection_id: str, item_id: str, **kwargs + ) -> Item: + """Get a specific item from a collection in a catalog. + + Args: + catalog_id: The ID of the catalog. + collection_id: The ID of the collection. + item_id: The ID of the item. + + Returns: + The requested item. + """ + ... + + @abc.abstractmethod + def get_catalog_children( + self, + catalog_id: str, + limit: Optional[int] = None, + token: Optional[str] = None, + type: Optional[Literal["Catalog", "Collection"]] = None, + **kwargs, + ) -> Children: + """Get all children (Catalogs and Collections) of a specific catalog. + + Args: + catalog_id: The ID of the catalog. + limit: Maximum number of results to return. + token: Pagination token. + type: Filter by resource type (Catalog or Collection). + + Returns: + Dictionary containing children and pagination links. + """ + ... + + @abc.abstractmethod + def get_catalog_conformance(self, catalog_id: str, **kwargs) -> dict: + """Get conformance classes specific to this sub-catalog. + + Args: + catalog_id: The ID of the catalog. + + Returns: + Dictionary containing conformance classes. + """ + ... + + @abc.abstractmethod + def get_catalog_queryables(self, catalog_id: str, **kwargs) -> dict: + """Get queryable fields available for filtering in this sub-catalog. + + Args: + catalog_id: The ID of the catalog. + + Returns: + Dictionary containing queryable fields (Filter Extension). + """ + ... + + @abc.abstractmethod + def unlink_sub_catalog( + self, catalog_id: str, sub_catalog_id: str, **kwargs + ) -> None: + """Unlink a sub-catalog from its parent. + + Args: + catalog_id: The ID of the parent catalog. + sub_catalog_id: The ID of the sub-catalog to unlink. + """ + ... diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py new file mode 100644 index 000000000..70c80cc41 --- /dev/null +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py @@ -0,0 +1,31 @@ +"""Catalogs extension types.""" + +from typing import Any, Dict, List, Optional + +from stac_pydantic.catalog import Catalog +from stac_pydantic.links import Links +from stac_pydantic.shared import StacBaseModel + + +class Catalogs(StacBaseModel): + """Catalogs endpoint response. + + Similar to Collections but for catalogs. + """ + + catalogs: List[Catalog] + links: Links + numberMatched: Optional[int] = None + numberReturned: Optional[int] = None + + +class Children(StacBaseModel): + """Children endpoint response. + + Returns a mixed list of Catalogs and Collections as children. + """ + + children: List[Dict[str, Any]] + links: Links + numberMatched: Optional[int] = None + numberReturned: Optional[int] = None From d835241af9e26e116d2cc1ac3f73f6f5308a9b8b Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 7 Feb 2026 20:15:38 +0800 Subject: [PATCH 02/11] import fixes --- .../core/multi_tenant_catalogs/catalogs.py | 5 +++-- .../core/multi_tenant_catalogs/client.py | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py index f50d689ba..609609a2e 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py @@ -3,10 +3,11 @@ from typing import List, Optional, Type import attr -from fastapi import APIRouter, FastAPI, Query +from fastapi import APIRouter, FastAPI from fastapi.responses import JSONResponse from starlette.responses import Response +from stac_pydantic.api.collections import Collections from stac_pydantic.catalog import Catalog from stac_pydantic.collection import Collection from stac_pydantic.item import Item @@ -14,7 +15,7 @@ from stac_fastapi.types.extension import ApiExtension from .client import AsyncBaseCatalogsClient -from .types import Catalogs, Children, Collections +from .types import Catalogs, Children CATALOGS_CONFORMANCE_CLASSES = [ "https://api.stacspec.org/v1.0.0/core", diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py index d3b3c351c..93fbe3cb6 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py @@ -98,14 +98,14 @@ async def get_catalog_collections( ... @abc.abstractmethod - async def get_sub_catalogs( + async def get_catalog_catalogs( self, catalog_id: str, limit: Optional[int] = None, token: Optional[str] = None, **kwargs, ) -> Catalogs: - """Get all sub-catalogs of a specific catalog with pagination support. + """Get all sub-catalogs of a specific catalog with pagination. Args: catalog_id: The ID of the parent catalog. @@ -118,7 +118,7 @@ async def get_sub_catalogs( ... @abc.abstractmethod - async def create_sub_catalog( + async def create_catalog_catalog( self, catalog_id: str, catalog: Catalog, **kwargs ) -> Catalog: """Create a new catalog or link an existing catalog as a sub-catalog. @@ -127,6 +127,12 @@ async def create_sub_catalog( - Mode A (Creation): Full Catalog JSON body with id that doesn't exist → creates new catalog - Mode B (Linking): Minimal body with just id of existing catalog → links as sub-catalog + Logic: + 1. Verifies the parent catalog exists. + 2. If the sub-catalog already exists: Appends the parent ID to its parent_ids + (enabling poly-hierarchy - a catalog can have multiple parents). + 3. If the sub-catalog is new: Creates it with parent_ids initialized to [catalog_id]. + Args: catalog_id: The ID of the parent catalog. catalog: The catalog to create or link. @@ -363,14 +369,14 @@ def get_catalog_collections( ... @abc.abstractmethod - def get_sub_catalogs( + def get_catalog_catalogs( self, catalog_id: str, limit: Optional[int] = None, token: Optional[str] = None, **kwargs, ) -> Catalogs: - """Get all sub-catalogs of a specific catalog with pagination support. + """Get all sub-catalogs of a specific catalog with pagination. Args: catalog_id: The ID of the parent catalog. @@ -383,7 +389,7 @@ def get_sub_catalogs( ... @abc.abstractmethod - def create_sub_catalog( + def create_catalog_catalog( self, catalog_id: str, catalog: Catalog, **kwargs ) -> Catalog: """Create a new catalog or link an existing catalog as a sub-catalog. From 0dbb32ec72517f27e5d9a93287c861deedf844b3 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Sat, 7 Feb 2026 20:15:50 +0800 Subject: [PATCH 03/11] pre-commit --- .../stac_fastapi/extensions/core/__init__.py | 2 +- .../core/multi_tenant_catalogs/catalogs.py | 30 ++++++--- .../core/multi_tenant_catalogs/client.py | 62 +++++++++---------- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index 031408b37..ff5601aa3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -1,7 +1,6 @@ """stac_api.extensions.core module.""" from .aggregation import AggregationExtension -from .multi_tenant_catalogs import CatalogsExtension from .collection_search import CollectionSearchExtension, CollectionSearchPostExtension from .fields import FieldsExtension from .filter import ( @@ -11,6 +10,7 @@ SearchFilterExtension, ) from .free_text import FreeTextAdvancedExtension, FreeTextExtension +from .multi_tenant_catalogs import CatalogsExtension from .pagination import ( OffsetPaginationExtension, PaginationExtension, diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py index 609609a2e..f8619ad32 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py @@ -1,17 +1,17 @@ """Catalogs extension.""" -from typing import List, Optional, Type +from typing import List, Type import attr from fastapi import APIRouter, FastAPI from fastapi.responses import JSONResponse -from starlette.responses import Response - from stac_pydantic.api.collections import Collections from stac_pydantic.catalog import Catalog from stac_pydantic.collection import Collection from stac_pydantic.item import Item from stac_pydantic.item_collection import ItemCollection +from starlette.responses import Response + from stac_fastapi.types.extension import ApiExtension from .client import AsyncBaseCatalogsClient @@ -155,7 +155,10 @@ def register(self, app: FastAPI, settings=None) -> None: response_class=self.response_class, status_code=204, summary="Unlink Collection from Catalog", - description="Removes the link between the catalog and collection. The Collection data is NOT deleted.", + description=( + "Removes the link between the catalog and collection. " + "The Collection data is NOT deleted." + ), tags=["Catalogs"], ) @@ -200,7 +203,10 @@ def register(self, app: FastAPI, settings=None) -> None: response_class=self.response_class, status_code=201, summary="Create Catalog Sub-Catalog", - description="Create a new catalog and link it as a sub-catalog of a specific catalog.", + description=( + "Create a new catalog and link it as a sub-catalog " + "of a specific catalog." + ), tags=["Catalogs"], ) @@ -211,7 +217,9 @@ def register(self, app: FastAPI, settings=None) -> None: response_model=Children, response_class=self.response_class, summary="Get Catalog Children", - description="Retrieve all children (Catalogs and Collections) of this catalog.", + description=( + "Retrieve all children (Catalogs and Collections) " "of this catalog." + ), tags=["Catalogs"], ) @@ -231,7 +239,10 @@ def register(self, app: FastAPI, settings=None) -> None: methods=["GET"], response_class=self.response_class, summary="Get Catalog Queryables", - description="Get queryable fields available for filtering in this sub-catalog (Filter Extension).", + description=( + "Get queryable fields available for filtering in this " + "sub-catalog (Filter Extension)." + ), tags=["Catalogs"], ) @@ -242,7 +253,10 @@ def register(self, app: FastAPI, settings=None) -> None: response_class=self.response_class, status_code=204, summary="Unlink Sub-Catalog", - description="Unlink a sub-catalog from its parent. Does not delete the sub-catalog.", + description=( + "Unlink a sub-catalog from its parent. " + "Does not delete the sub-catalog." + ), tags=["Catalogs"], ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py index 93fbe3cb6..0eae1a226 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py @@ -84,9 +84,7 @@ async def delete_catalog(self, catalog_id: str, **kwargs) -> None: ... @abc.abstractmethod - async def get_catalog_collections( - self, catalog_id: str, **kwargs - ) -> Collections: + async def get_catalog_collections(self, catalog_id: str, **kwargs) -> Collections: """Get collections linked from a specific catalog. Args: @@ -98,7 +96,7 @@ async def get_catalog_collections( ... @abc.abstractmethod - async def get_catalog_catalogs( + async def get_sub_catalogs( self, catalog_id: str, limit: Optional[int] = None, @@ -118,20 +116,24 @@ async def get_catalog_catalogs( ... @abc.abstractmethod - async def create_catalog_catalog( + async def create_sub_catalog( self, catalog_id: str, catalog: Catalog, **kwargs ) -> Catalog: """Create a new catalog or link an existing catalog as a sub-catalog. Supports two modes: - - Mode A (Creation): Full Catalog JSON body with id that doesn't exist → creates new catalog - - Mode B (Linking): Minimal body with just id of existing catalog → links as sub-catalog + - Mode A (Creation): Full Catalog JSON body with id that doesn't exist + → creates new catalog + - Mode B (Linking): Minimal body with just id of existing catalog + → links as sub-catalog Logic: 1. Verifies the parent catalog exists. - 2. If the sub-catalog already exists: Appends the parent ID to its parent_ids - (enabling poly-hierarchy - a catalog can have multiple parents). - 3. If the sub-catalog is new: Creates it with parent_ids initialized to [catalog_id]. + 2. If the sub-catalog already exists: Appends the parent ID to its + parent_ids (enabling poly-hierarchy - a catalog can have multiple + parents). + 3. If the sub-catalog is new: Creates it with parent_ids initialized + to [catalog_id]. Args: catalog_id: The ID of the parent catalog. @@ -146,11 +148,13 @@ async def create_catalog_catalog( async def create_catalog_collection( self, catalog_id: str, collection: Collection, **kwargs ) -> Collection: - """Create a new collection or link an existing collection to a specific catalog. + """Create a new collection or link an existing collection to catalog. Supports two modes: - - Mode A (Creation): Full Collection JSON body with id that doesn't exist → creates new collection - - Mode B (Linking): Minimal body with just id of existing collection → links to catalog + - Mode A (Creation): Full Collection JSON body with id that doesn't + exist → creates new collection + - Mode B (Linking): Minimal body with just id of existing collection + → links to catalog Args: catalog_id: The ID of the catalog to link the collection to. @@ -331,9 +335,7 @@ def get_catalog(self, catalog_id: str, **kwargs) -> Catalog: ... @abc.abstractmethod - def update_catalog( - self, catalog_id: str, catalog: Catalog, **kwargs - ) -> Catalog: + def update_catalog(self, catalog_id: str, catalog: Catalog, **kwargs) -> Catalog: """Update an existing catalog. Args: @@ -355,9 +357,7 @@ def delete_catalog(self, catalog_id: str, **kwargs) -> None: ... @abc.abstractmethod - def get_catalog_collections( - self, catalog_id: str, **kwargs - ) -> Collections: + def get_catalog_collections(self, catalog_id: str, **kwargs) -> Collections: """Get collections linked from a specific catalog. Args: @@ -369,7 +369,7 @@ def get_catalog_collections( ... @abc.abstractmethod - def get_catalog_catalogs( + def get_sub_catalogs( self, catalog_id: str, limit: Optional[int] = None, @@ -389,14 +389,14 @@ def get_catalog_catalogs( ... @abc.abstractmethod - def create_catalog_catalog( - self, catalog_id: str, catalog: Catalog, **kwargs - ) -> Catalog: + def create_sub_catalog(self, catalog_id: str, catalog: Catalog, **kwargs) -> Catalog: """Create a new catalog or link an existing catalog as a sub-catalog. Supports two modes: - - Mode A (Creation): Full Catalog JSON body with id that doesn't exist → creates new catalog - - Mode B (Linking): Minimal body with just id of existing catalog → links as sub-catalog + - Mode A (Creation): Full Catalog JSON body with id that doesn't exist + → creates new catalog + - Mode B (Linking): Minimal body with just id of existing catalog + → links as sub-catalog Args: catalog_id: The ID of the parent catalog. @@ -411,11 +411,13 @@ def create_catalog_catalog( def create_catalog_collection( self, catalog_id: str, collection: Collection, **kwargs ) -> Collection: - """Create a new collection or link an existing collection to a specific catalog. + """Create a new collection or link an existing collection to catalog. Supports two modes: - - Mode A (Creation): Full Collection JSON body with id that doesn't exist → creates new collection - - Mode B (Linking): Minimal body with just id of existing collection → links to catalog + - Mode A (Creation): Full Collection JSON body with id that doesn't + exist → creates new collection + - Mode B (Linking): Minimal body with just id of existing collection + → links to catalog Args: catalog_id: The ID of the catalog to link the collection to. @@ -537,9 +539,7 @@ def get_catalog_queryables(self, catalog_id: str, **kwargs) -> dict: ... @abc.abstractmethod - def unlink_sub_catalog( - self, catalog_id: str, sub_catalog_id: str, **kwargs - ) -> None: + def unlink_sub_catalog(self, catalog_id: str, sub_catalog_id: str, **kwargs) -> None: """Unlink a sub-catalog from its parent. Args: From 29e9835136bebaefbe38f6b5e8178fbb57befb32 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 1 Apr 2026 23:52:26 +0800 Subject: [PATCH 04/11] update mypy --- .pre-commit-config.yaml | 4 ++-- .../extensions/core/transaction/transaction.py | 10 ++++++---- uv.lock | 4 ---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19bbaf97c..48ef4b4a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.2.2" + rev: "v0.15.8" hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.0 + rev: v1.20.0 hooks: - id: mypy language_version: python diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py index ca61c9e60..b2e87bb32 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py @@ -1,7 +1,7 @@ """Transaction extension.""" from enum import Enum -from typing import List, Optional, Type, Union +from typing import Any, List, Optional, Type, Union import attr from fastapi import APIRouter, Body, FastAPI @@ -108,7 +108,8 @@ class PatchCollection(CollectionUri): ] } # ref: https://github.com/pydantic/pydantic/issues/889 -_patch_item_schema["items"]["anyOf"] = list(_patch_item_schema["$defs"].values()) +_patch_item_schema_dict: Any = _patch_item_schema +_patch_item_schema_dict["items"]["anyOf"] = list(_patch_item_schema_dict["$defs"].values()) _patch_collection_schema = TypeAdapter(List[PatchOperation]).json_schema() | { "examples": [ @@ -146,8 +147,9 @@ class PatchCollection(CollectionUri): ] } # ref: https://github.com/pydantic/pydantic/issues/889 -_patch_collection_schema["items"]["anyOf"] = list( - _patch_collection_schema["$defs"].values() +_patch_collection_schema_dict: Any = _patch_collection_schema +_patch_collection_schema_dict["items"]["anyOf"] = list( + _patch_collection_schema_dict["$defs"].values() ) diff --git a/uv.lock b/uv.lock index b7c8f7c45..415f79f53 100644 --- a/uv.lock +++ b/uv.lock @@ -6,10 +6,6 @@ resolution-markers = [ "python_full_version < '3.12'", ] -[options] -exclude-newer = "2026-03-23T10:55:01.77892Z" -exclude-newer-span = "P3D" - [manifest] members = [ "stac-fastapi", From c18449ce923b3a8c0b2a72c3a0cf57952be4cf11 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 1 Apr 2026 23:55:41 +0800 Subject: [PATCH 05/11] update changelog --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 9bb2d8ad3..cc40dddec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- fix mypy type errors in transaction extension for Python 3.14 compatibility (mypy 1.20.0) ([#895](https://github.com/stac-utils/stac-fastapi/pull/895)) + ## [6.2.1] - 2026-02-10 ### Fixed From d4e18e382c5c9ebdfacb479ae8bc41412e4ccdc9 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Wed, 1 Apr 2026 23:58:51 +0800 Subject: [PATCH 06/11] remove old code --- .../core/multi_tenant_catalogs/__init__.py | 17 - .../core/multi_tenant_catalogs/catalogs.py | 263 --------- .../core/multi_tenant_catalogs/client.py | 549 ------------------ .../core/multi_tenant_catalogs/types.py | 31 - 4 files changed, 860 deletions(-) delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py delete mode 100644 stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py deleted file mode 100644 index 8151e2e18..000000000 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Catalogs extension module.""" - -from stac_pydantic.api.collections import Collections - -from .catalogs import CATALOGS_CONFORMANCE_CLASSES, CatalogsExtension -from .client import AsyncBaseCatalogsClient, BaseCatalogsClient -from .types import Catalogs, Children - -__all__ = [ - "CatalogsExtension", - "AsyncBaseCatalogsClient", - "BaseCatalogsClient", - "Catalogs", - "Collections", - "Children", - "CATALOGS_CONFORMANCE_CLASSES", -] diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py deleted file mode 100644 index f8619ad32..000000000 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/catalogs.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Catalogs extension.""" - -from typing import List, Type - -import attr -from fastapi import APIRouter, FastAPI -from fastapi.responses import JSONResponse -from stac_pydantic.api.collections import Collections -from stac_pydantic.catalog import Catalog -from stac_pydantic.collection import Collection -from stac_pydantic.item import Item -from stac_pydantic.item_collection import ItemCollection -from starlette.responses import Response - -from stac_fastapi.types.extension import ApiExtension - -from .client import AsyncBaseCatalogsClient -from .types import Catalogs, Children - -CATALOGS_CONFORMANCE_CLASSES = [ - "https://api.stacspec.org/v1.0.0/core", - "https://api.stacspec.org/v1.0.0-beta.1/multi-tenant-catalogs", - "https://api.stacspec.org/v1.0.0-rc.2/children", - "https://api.stacspec.org/v1.0.0-rc.2/children#type-filter", -] - - -@attr.s -class CatalogsExtension(ApiExtension): - """Catalogs Extension. - - The Catalogs extension adds a /catalogs endpoint that returns a list of all catalogs - in the database, similar to how /collections returns a list of collections. - - Attributes: - client: A client implementing the catalogs extension pattern. - settings: Extension settings. - conformance_classes: List of conformance classes for this extension. - router: FastAPI router for the extension endpoints. - response_class: Response class for the extension. - """ - - client: AsyncBaseCatalogsClient = attr.ib(default=None) - settings: dict = attr.ib(default=attr.Factory(dict)) - conformance_classes: List[str] = attr.ib( - default=attr.Factory(lambda: CATALOGS_CONFORMANCE_CLASSES) - ) - router: APIRouter = attr.ib(default=attr.Factory(APIRouter)) - response_class: Type[Response] = attr.ib(default=JSONResponse) - - def register(self, app: FastAPI, settings=None) -> None: - """Register the extension with a FastAPI application. - - Args: - app: target FastAPI application. - settings: extension settings. - """ - self.settings = settings or {} - self.router = APIRouter() - - self.router.add_api_route( - path="/catalogs", - endpoint=self.client.get_catalogs, - methods=["GET"], - response_model=Catalogs, - response_class=self.response_class, - summary="Get All Catalogs", - description="Returns a list of all catalogs in the database.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs", - endpoint=self.client.create_catalog, - methods=["POST"], - response_model=Catalog, - response_class=self.response_class, - status_code=201, - summary="Create Catalog", - description="Create a new STAC catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}", - endpoint=self.client.get_catalog, - methods=["GET"], - response_model=Catalog, - response_class=self.response_class, - summary="Get Catalog", - description="Get a specific STAC catalog by ID.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}", - endpoint=self.client.update_catalog, - methods=["PUT"], - response_model=Catalog, - response_class=self.response_class, - summary="Update Catalog", - description="Update an existing STAC catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}", - endpoint=self.client.delete_catalog, - methods=["DELETE"], - response_class=self.response_class, - status_code=204, - summary="Delete Catalog", - description="Delete a catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/collections", - endpoint=self.client.get_catalog_collections, - methods=["GET"], - response_model=Collections, - response_class=self.response_class, - summary="Get Catalog Collections", - description="Get collections linked from a specific catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/collections", - endpoint=self.client.create_catalog_collection, - methods=["POST"], - response_model=Collection, - response_class=self.response_class, - status_code=201, - summary="Create Catalog Collection", - description="Create a new collection and link it to a specific catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/collections/{collection_id}", - endpoint=self.client.get_catalog_collection, - methods=["GET"], - response_model=Collection, - response_class=self.response_class, - summary="Get Catalog Collection", - description="Get a specific collection from a catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/collections/{collection_id}", - endpoint=self.client.unlink_catalog_collection, - methods=["DELETE"], - response_class=self.response_class, - status_code=204, - summary="Unlink Collection from Catalog", - description=( - "Removes the link between the catalog and collection. " - "The Collection data is NOT deleted." - ), - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/collections/{collection_id}/items", - endpoint=self.client.get_catalog_collection_items, - methods=["GET"], - response_model=ItemCollection, - response_class=self.response_class, - summary="Get Catalog Collection Items", - description="Get items from a collection in a catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/collections/{collection_id}/items/{item_id}", - endpoint=self.client.get_catalog_collection_item, - methods=["GET"], - response_model=Item, - response_class=self.response_class, - summary="Get Catalog Collection Item", - description="Get a specific item from a collection in a catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/catalogs", - endpoint=self.client.get_sub_catalogs, - methods=["GET"], - response_model=Catalogs, - response_class=self.response_class, - summary="Get Catalog Sub-Catalogs", - description="Get sub-catalogs linked from a specific catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/catalogs", - endpoint=self.client.create_sub_catalog, - methods=["POST"], - response_model=Catalog, - response_class=self.response_class, - status_code=201, - summary="Create Catalog Sub-Catalog", - description=( - "Create a new catalog and link it as a sub-catalog " - "of a specific catalog." - ), - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/children", - endpoint=self.client.get_catalog_children, - methods=["GET"], - response_model=Children, - response_class=self.response_class, - summary="Get Catalog Children", - description=( - "Retrieve all children (Catalogs and Collections) " "of this catalog." - ), - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/conformance", - endpoint=self.client.get_catalog_conformance, - methods=["GET"], - response_class=self.response_class, - summary="Get Catalog Conformance", - description="Get conformance classes specific to this sub-catalog.", - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/queryables", - endpoint=self.client.get_catalog_queryables, - methods=["GET"], - response_class=self.response_class, - summary="Get Catalog Queryables", - description=( - "Get queryable fields available for filtering in this " - "sub-catalog (Filter Extension)." - ), - tags=["Catalogs"], - ) - - self.router.add_api_route( - path="/catalogs/{catalog_id}/catalogs/{sub_catalog_id}", - endpoint=self.client.unlink_sub_catalog, - methods=["DELETE"], - response_class=self.response_class, - status_code=204, - summary="Unlink Sub-Catalog", - description=( - "Unlink a sub-catalog from its parent. " - "Does not delete the sub-catalog." - ), - tags=["Catalogs"], - ) - - app.include_router(self.router, tags=["Catalogs"]) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py deleted file mode 100644 index 0eae1a226..000000000 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/client.py +++ /dev/null @@ -1,549 +0,0 @@ -"""Catalogs extension clients.""" - -import abc -from typing import Literal, Optional - -import attr -from stac_pydantic.api.collections import Collections -from stac_pydantic.catalog import Catalog -from stac_pydantic.collection import Collection -from stac_pydantic.item import Item -from stac_pydantic.item_collection import ItemCollection - -from .types import Catalogs, Children - - -@attr.s -class AsyncBaseCatalogsClient(abc.ABC): - """Defines an async pattern for implementing the STAC catalogs extension.""" - - @abc.abstractmethod - async def get_catalogs( - self, - limit: Optional[int] = None, - token: Optional[str] = None, - **kwargs, - ) -> Catalogs: - """Get all catalogs with pagination support. - - Args: - limit: The maximum number of catalogs to return. - token: Pagination token for the next page of results. - - Returns: - Catalogs object containing catalogs and pagination links. - """ - ... - - @abc.abstractmethod - async def create_catalog(self, catalog: Catalog, **kwargs) -> Catalog: - """Create a new catalog. - - Args: - catalog: The catalog to create. - - Returns: - The created catalog. - """ - ... - - @abc.abstractmethod - async def get_catalog(self, catalog_id: str, **kwargs) -> Catalog: - """Get a specific catalog by ID. - - Args: - catalog_id: The ID of the catalog to retrieve. - - Returns: - The requested catalog. - """ - ... - - @abc.abstractmethod - async def update_catalog( - self, catalog_id: str, catalog: Catalog, **kwargs - ) -> Catalog: - """Update an existing catalog. - - Args: - catalog_id: The ID of the catalog to update. - catalog: The updated catalog data. - - Returns: - The updated catalog. - """ - ... - - @abc.abstractmethod - async def delete_catalog(self, catalog_id: str, **kwargs) -> None: - """Delete a catalog. - - Args: - catalog_id: The ID of the catalog to delete. - """ - ... - - @abc.abstractmethod - async def get_catalog_collections(self, catalog_id: str, **kwargs) -> Collections: - """Get collections linked from a specific catalog. - - Args: - catalog_id: The ID of the catalog. - - Returns: - Collections object containing collections linked from the catalog. - """ - ... - - @abc.abstractmethod - async def get_sub_catalogs( - self, - catalog_id: str, - limit: Optional[int] = None, - token: Optional[str] = None, - **kwargs, - ) -> Catalogs: - """Get all sub-catalogs of a specific catalog with pagination. - - Args: - catalog_id: The ID of the parent catalog. - limit: Maximum number of results to return. - token: Pagination token for cursor-based pagination. - - Returns: - A Catalogs response containing sub-catalogs with pagination links. - """ - ... - - @abc.abstractmethod - async def create_sub_catalog( - self, catalog_id: str, catalog: Catalog, **kwargs - ) -> Catalog: - """Create a new catalog or link an existing catalog as a sub-catalog. - - Supports two modes: - - Mode A (Creation): Full Catalog JSON body with id that doesn't exist - → creates new catalog - - Mode B (Linking): Minimal body with just id of existing catalog - → links as sub-catalog - - Logic: - 1. Verifies the parent catalog exists. - 2. If the sub-catalog already exists: Appends the parent ID to its - parent_ids (enabling poly-hierarchy - a catalog can have multiple - parents). - 3. If the sub-catalog is new: Creates it with parent_ids initialized - to [catalog_id]. - - Args: - catalog_id: The ID of the parent catalog. - catalog: The catalog to create or link. - - Returns: - The created or linked catalog. - """ - ... - - @abc.abstractmethod - async def create_catalog_collection( - self, catalog_id: str, collection: Collection, **kwargs - ) -> Collection: - """Create a new collection or link an existing collection to catalog. - - Supports two modes: - - Mode A (Creation): Full Collection JSON body with id that doesn't - exist → creates new collection - - Mode B (Linking): Minimal body with just id of existing collection - → links to catalog - - Args: - catalog_id: The ID of the catalog to link the collection to. - collection: The collection to create or link. - - Returns: - The created or linked collection. - """ - ... - - @abc.abstractmethod - async def get_catalog_collection( - self, catalog_id: str, collection_id: str, **kwargs - ) -> Collection: - """Get a specific collection from a catalog. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - - Returns: - The requested collection. - """ - ... - - @abc.abstractmethod - async def unlink_catalog_collection( - self, catalog_id: str, collection_id: str, **kwargs - ) -> None: - """Unlink a collection from a catalog. - - Removes the link between the catalog and collection. - The Collection data is NOT deleted. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - """ - ... - - @abc.abstractmethod - async def get_catalog_collection_items( - self, - catalog_id: str, - collection_id: str, - **kwargs, - ) -> ItemCollection: - """Get items from a collection in a catalog. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - - Returns: - ItemCollection containing items from the collection. - """ - ... - - @abc.abstractmethod - async def get_catalog_collection_item( - self, catalog_id: str, collection_id: str, item_id: str, **kwargs - ) -> Item: - """Get a specific item from a collection in a catalog. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - item_id: The ID of the item. - - Returns: - The requested item. - """ - ... - - @abc.abstractmethod - async def get_catalog_children( - self, - catalog_id: str, - limit: Optional[int] = None, - token: Optional[str] = None, - type: Optional[Literal["Catalog", "Collection"]] = None, - **kwargs, - ) -> Children: - """Get all children (Catalogs and Collections) of a specific catalog. - - Args: - catalog_id: The ID of the catalog. - limit: Maximum number of results to return. - token: Pagination token. - type: Filter by resource type (Catalog or Collection). - - Returns: - Dictionary containing children and pagination links. - """ - ... - - @abc.abstractmethod - async def get_catalog_conformance(self, catalog_id: str, **kwargs) -> dict: - """Get conformance classes specific to this sub-catalog. - - Args: - catalog_id: The ID of the catalog. - - Returns: - Dictionary containing conformance classes. - """ - ... - - @abc.abstractmethod - async def get_catalog_queryables(self, catalog_id: str, **kwargs) -> dict: - """Get queryable fields available for filtering in this sub-catalog. - - Args: - catalog_id: The ID of the catalog. - - Returns: - Dictionary containing queryable fields (Filter Extension). - """ - ... - - @abc.abstractmethod - async def unlink_sub_catalog( - self, catalog_id: str, sub_catalog_id: str, **kwargs - ) -> None: - """Unlink a sub-catalog from its parent. - - Args: - catalog_id: The ID of the parent catalog. - sub_catalog_id: The ID of the sub-catalog to unlink. - """ - ... - - -@attr.s -class BaseCatalogsClient(abc.ABC): - """Defines a synchronous pattern for implementing the STAC catalogs extension.""" - - @abc.abstractmethod - def get_catalogs( - self, - limit: Optional[int] = None, - token: Optional[str] = None, - **kwargs, - ) -> Catalogs: - """Get all catalogs with pagination support. - - Args: - limit: The maximum number of catalogs to return. - token: Pagination token for the next page of results. - - Returns: - Catalogs object containing catalogs and pagination links. - """ - ... - - @abc.abstractmethod - def create_catalog(self, catalog: Catalog, **kwargs) -> Catalog: - """Create a new catalog. - - Args: - catalog: The catalog to create. - - Returns: - The created catalog. - """ - ... - - @abc.abstractmethod - def get_catalog(self, catalog_id: str, **kwargs) -> Catalog: - """Get a specific catalog by ID. - - Args: - catalog_id: The ID of the catalog to retrieve. - - Returns: - The requested catalog. - """ - ... - - @abc.abstractmethod - def update_catalog(self, catalog_id: str, catalog: Catalog, **kwargs) -> Catalog: - """Update an existing catalog. - - Args: - catalog_id: The ID of the catalog to update. - catalog: The updated catalog data. - - Returns: - The updated catalog. - """ - ... - - @abc.abstractmethod - def delete_catalog(self, catalog_id: str, **kwargs) -> None: - """Delete a catalog. - - Args: - catalog_id: The ID of the catalog to delete. - """ - ... - - @abc.abstractmethod - def get_catalog_collections(self, catalog_id: str, **kwargs) -> Collections: - """Get collections linked from a specific catalog. - - Args: - catalog_id: The ID of the catalog. - - Returns: - Collections object containing collections linked from the catalog. - """ - ... - - @abc.abstractmethod - def get_sub_catalogs( - self, - catalog_id: str, - limit: Optional[int] = None, - token: Optional[str] = None, - **kwargs, - ) -> Catalogs: - """Get all sub-catalogs of a specific catalog with pagination. - - Args: - catalog_id: The ID of the parent catalog. - limit: Maximum number of results to return. - token: Pagination token for cursor-based pagination. - - Returns: - A Catalogs response containing sub-catalogs with pagination links. - """ - ... - - @abc.abstractmethod - def create_sub_catalog(self, catalog_id: str, catalog: Catalog, **kwargs) -> Catalog: - """Create a new catalog or link an existing catalog as a sub-catalog. - - Supports two modes: - - Mode A (Creation): Full Catalog JSON body with id that doesn't exist - → creates new catalog - - Mode B (Linking): Minimal body with just id of existing catalog - → links as sub-catalog - - Args: - catalog_id: The ID of the parent catalog. - catalog: The catalog to create or link. - - Returns: - The created or linked catalog. - """ - ... - - @abc.abstractmethod - def create_catalog_collection( - self, catalog_id: str, collection: Collection, **kwargs - ) -> Collection: - """Create a new collection or link an existing collection to catalog. - - Supports two modes: - - Mode A (Creation): Full Collection JSON body with id that doesn't - exist → creates new collection - - Mode B (Linking): Minimal body with just id of existing collection - → links to catalog - - Args: - catalog_id: The ID of the catalog to link the collection to. - collection: The collection to create or link. - - Returns: - The created or linked collection. - """ - ... - - @abc.abstractmethod - def get_catalog_collection( - self, catalog_id: str, collection_id: str, **kwargs - ) -> Collection: - """Get a specific collection from a catalog. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - - Returns: - The requested collection. - """ - ... - - @abc.abstractmethod - def unlink_catalog_collection( - self, catalog_id: str, collection_id: str, **kwargs - ) -> None: - """Unlink a collection from a catalog. - - Removes the link between the catalog and collection. - The Collection data is NOT deleted. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - """ - ... - - @abc.abstractmethod - def get_catalog_collection_items( - self, - catalog_id: str, - collection_id: str, - **kwargs, - ) -> ItemCollection: - """Get items from a collection in a catalog. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - - Returns: - ItemCollection containing items from the collection. - """ - ... - - @abc.abstractmethod - def get_catalog_collection_item( - self, catalog_id: str, collection_id: str, item_id: str, **kwargs - ) -> Item: - """Get a specific item from a collection in a catalog. - - Args: - catalog_id: The ID of the catalog. - collection_id: The ID of the collection. - item_id: The ID of the item. - - Returns: - The requested item. - """ - ... - - @abc.abstractmethod - def get_catalog_children( - self, - catalog_id: str, - limit: Optional[int] = None, - token: Optional[str] = None, - type: Optional[Literal["Catalog", "Collection"]] = None, - **kwargs, - ) -> Children: - """Get all children (Catalogs and Collections) of a specific catalog. - - Args: - catalog_id: The ID of the catalog. - limit: Maximum number of results to return. - token: Pagination token. - type: Filter by resource type (Catalog or Collection). - - Returns: - Dictionary containing children and pagination links. - """ - ... - - @abc.abstractmethod - def get_catalog_conformance(self, catalog_id: str, **kwargs) -> dict: - """Get conformance classes specific to this sub-catalog. - - Args: - catalog_id: The ID of the catalog. - - Returns: - Dictionary containing conformance classes. - """ - ... - - @abc.abstractmethod - def get_catalog_queryables(self, catalog_id: str, **kwargs) -> dict: - """Get queryable fields available for filtering in this sub-catalog. - - Args: - catalog_id: The ID of the catalog. - - Returns: - Dictionary containing queryable fields (Filter Extension). - """ - ... - - @abc.abstractmethod - def unlink_sub_catalog(self, catalog_id: str, sub_catalog_id: str, **kwargs) -> None: - """Unlink a sub-catalog from its parent. - - Args: - catalog_id: The ID of the parent catalog. - sub_catalog_id: The ID of the sub-catalog to unlink. - """ - ... diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py deleted file mode 100644 index 70c80cc41..000000000 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/multi_tenant_catalogs/types.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Catalogs extension types.""" - -from typing import Any, Dict, List, Optional - -from stac_pydantic.catalog import Catalog -from stac_pydantic.links import Links -from stac_pydantic.shared import StacBaseModel - - -class Catalogs(StacBaseModel): - """Catalogs endpoint response. - - Similar to Collections but for catalogs. - """ - - catalogs: List[Catalog] - links: Links - numberMatched: Optional[int] = None - numberReturned: Optional[int] = None - - -class Children(StacBaseModel): - """Children endpoint response. - - Returns a mixed list of Catalogs and Collections as children. - """ - - children: List[Dict[str, Any]] - links: Links - numberMatched: Optional[int] = None - numberReturned: Optional[int] = None From 4f9d7069bd8fe62f5e7881be0d3167b029f23246 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Thu, 2 Apr 2026 00:00:33 +0800 Subject: [PATCH 07/11] removals --- .../extensions/stac_fastapi/extensions/core/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py index ff5601aa3..d6b5f7589 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/__init__.py @@ -10,7 +10,6 @@ SearchFilterExtension, ) from .free_text import FreeTextAdvancedExtension, FreeTextExtension -from .multi_tenant_catalogs import CatalogsExtension from .pagination import ( OffsetPaginationExtension, PaginationExtension, @@ -22,7 +21,6 @@ __all__ = ( "AggregationExtension", - "CatalogsExtension", "FieldsExtension", "FilterExtension", "FreeTextExtension", From 764f79e7b2cab038179848113c6e22d0bf80422a Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Thu, 2 Apr 2026 00:02:58 +0800 Subject: [PATCH 08/11] shorten line? --- .../stac_fastapi/extensions/core/transaction/transaction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py index b2e87bb32..d2e77eef3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py @@ -109,7 +109,9 @@ class PatchCollection(CollectionUri): } # ref: https://github.com/pydantic/pydantic/issues/889 _patch_item_schema_dict: Any = _patch_item_schema -_patch_item_schema_dict["items"]["anyOf"] = list(_patch_item_schema_dict["$defs"].values()) +_patch_item_schema_dict["items"]["anyOf"] = list( + _patch_item_schema_dict["$defs"].values() +) _patch_collection_schema = TypeAdapter(List[PatchOperation]).json_schema() | { "examples": [ From 770067252f3084d9b5f5a269d16123c043613823 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Thu, 2 Apr 2026 00:04:57 +0800 Subject: [PATCH 09/11] lint --- stac_fastapi/api/stac_fastapi/api/app.py | 1 - stac_fastapi/api/tests/test_api.py | 36 ++++++++----------- stac_fastapi/api/tests/test_app_prefix.py | 5 +-- .../core/aggregation/aggregation.py | 1 + .../extensions/core/filter/filter.py | 1 + .../extensions/tests/test_free_text.py | 1 - .../types/stac_fastapi/types/search.py | 3 +- stac_fastapi/types/tests/test_rfc3339.py | 36 +++++++++---------- 8 files changed, 37 insertions(+), 47 deletions(-) diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index 11aa8e414..f25291c25 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -1,6 +1,5 @@ """Fastapi app creation.""" - import inspect from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index 5f9aa73e3..03c595eba 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -39,9 +39,9 @@ def _assert_dependency_applied(api, routes): with TestClient(api.app) as client: for route in routes: response = getattr(client, route["method"].lower())(route["path"]) - assert ( - response.status_code == 401 - ), "Unauthenticated requests should be rejected" + assert response.status_code == 401, ( + "Unauthenticated requests should be rejected" + ) assert response.json() == {"detail": "Not authenticated"} path = route["path"].format( @@ -54,9 +54,9 @@ def _assert_dependency_applied(api, routes): content=route["payload"], headers={"content-type": "application/json"}, ) - assert ( - 200 <= response.status_code < 300 - ), "Authenticated requests should be accepted" + assert 200 <= response.status_code < 300, ( + "Authenticated requests should be accepted" + ) assert response.json() == "dummy response" @staticmethod @@ -72,9 +72,9 @@ def _assert_dependency_not_applied(api, routes): content=route["payload"], headers={"content-type": "application/json"}, ) - assert ( - 200 <= response.status_code < 300 - ), "Authenticated requests should be accepted" + assert 200 <= response.status_code < 300, ( + "Authenticated requests should be accepted" + ) assert response.json() == "dummy response" def test_openapi_content_type(self): @@ -400,23 +400,17 @@ def test_add_default_method_route_dependencies_after_building_api( class DummyCoreClient(core.BaseCoreClient): - def all_collections(self, *args, **kwargs): - ... + def all_collections(self, *args, **kwargs): ... - def get_collection(self, *args, **kwargs): - ... + def get_collection(self, *args, **kwargs): ... - def get_item(self, *args, **kwargs): - ... + def get_item(self, *args, **kwargs): ... - def get_search(self, *args, **kwargs): - ... + def get_search(self, *args, **kwargs): ... - def post_search(self, *args, **kwargs): - ... + def post_search(self, *args, **kwargs): ... - def item_collection(self, *args, **kwargs): - ... + def item_collection(self, *args, **kwargs): ... class DummyTransactionsClient(BaseTransactionsClient): diff --git a/stac_fastapi/api/tests/test_app_prefix.py b/stac_fastapi/api/tests/test_app_prefix.py index f2289ccfc..cf51d002e 100644 --- a/stac_fastapi/api/tests/test_app_prefix.py +++ b/stac_fastapi/api/tests/test_app_prefix.py @@ -12,8 +12,9 @@ def get_link(landing_page, rel_type, method: Optional[str] = None): return next( filter( - lambda link: link["rel"] == rel_type - and (not method or link.get("method") == method), + lambda link: ( + link["rel"] == rel_type and (not method or link.get("method") == method) + ), landing_page["links"], ), None, diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/aggregation.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/aggregation.py index d646df45f..e8def85d3 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/aggregation.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/aggregation.py @@ -1,4 +1,5 @@ """Aggregation Extension.""" + from enum import Enum from typing import List, Type, Union diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py index e6f1a0deb..c7a170b4d 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py @@ -1,5 +1,6 @@ # encoding: utf-8 """Filter Extension.""" + from enum import Enum from typing import List, Type, Union diff --git a/stac_fastapi/extensions/tests/test_free_text.py b/stac_fastapi/extensions/tests/test_free_text.py index 55f253a34..1e7955467 100644 --- a/stac_fastapi/extensions/tests/test_free_text.py +++ b/stac_fastapi/extensions/tests/test_free_text.py @@ -1,7 +1,6 @@ # noqa: E501 """test freetext extension.""" - from starlette.testclient import TestClient from stac_fastapi.api.app import StacApi diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index 83c4fd34a..d13fb0155 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -1,6 +1,5 @@ -"""stac_fastapi.types.search module. +"""stac_fastapi.types.search module.""" -""" from datetime import datetime as dt from typing import Dict, List, Optional, Union, cast diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py index dc4c897d5..08c327dc4 100644 --- a/stac_fastapi/types/tests/test_rfc3339.py +++ b/stac_fastapi/types/tests/test_rfc3339.py @@ -54,11 +54,7 @@ ] invalid_intervals = [ - "/" - "../" - "/.." - "../.." - "/1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # extra start / + "/..//..../../1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # extra start / "1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z/", # extra end / "1986-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # start > end ] @@ -89,39 +85,39 @@ def test_parse_valid_str_to_datetime(test_input): def test_str_to_interval_with_invalid_interval(test_input): with pytest.raises(HTTPException) as exc_info: str_to_interval(test_input) - assert ( - exc_info.value.status_code == 400 - ), "str_to_interval should return a 400 status code for invalid interval" + assert exc_info.value.status_code == 400, ( + "str_to_interval should return a 400 status code for invalid interval" + ) @pytest.mark.parametrize("test_input", invalid_datetimes) def test_str_to_interval_with_invalid_datetime(test_input): with pytest.raises(HTTPException) as exc_info: str_to_interval(test_input) - assert ( - exc_info.value.status_code == 400 - ), "str_to_interval should return a 400 status code for invalid datetime" + assert exc_info.value.status_code == 400, ( + "str_to_interval should return a 400 status code for invalid datetime" + ) @pytest.mark.parametrize("test_input", valid_intervals) def test_str_to_interval_with_valid_interval(test_input): - assert isinstance( - str_to_interval(test_input), tuple - ), "str_to_interval should return tuple for multi-value input" + assert isinstance(str_to_interval(test_input), tuple), ( + "str_to_interval should return tuple for multi-value input" + ) @pytest.mark.parametrize("test_input", valid_datetimes) def test_str_to_interval_with_valid_datetime(test_input): - assert isinstance( - str_to_interval(test_input), datetime - ), "str_to_interval should return single datetime for single-value input" + assert isinstance(str_to_interval(test_input), datetime), ( + "str_to_interval should return single datetime for single-value input" + ) def test_str_to_interval_with_none(): """Test that str_to_interval returns None when provided with None.""" - assert ( - str_to_interval(None) is None - ), "str_to_interval should return None when input is None" + assert str_to_interval(None) is None, ( + "str_to_interval should return None when input is None" + ) def test_now_functions() -> None: From a308021c788814fe8d37f0354346a2ea7c80a1d1 Mon Sep 17 00:00:00 2001 From: jonhealy1 Date: Thu, 2 Apr 2026 11:54:40 +0800 Subject: [PATCH 10/11] fix invalid_intervals --- stac_fastapi/types/tests/test_rfc3339.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/stac_fastapi/types/tests/test_rfc3339.py b/stac_fastapi/types/tests/test_rfc3339.py index 08c327dc4..7ce353112 100644 --- a/stac_fastapi/types/tests/test_rfc3339.py +++ b/stac_fastapi/types/tests/test_rfc3339.py @@ -54,7 +54,11 @@ ] invalid_intervals = [ - "/..//..../../1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # extra start / + "/", + "../", + "/..", + "../..", + "/1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # extra start / "1984-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z/", # extra end / "1986-04-12T23:20:50.52Z/1985-04-12T23:20:50.52Z", # start > end ] From 3db6fd6cf6f678e9903dcc595a252d0a364b61cc Mon Sep 17 00:00:00 2001 From: vincentsarago Date: Thu, 2 Apr 2026 10:23:17 +0200 Subject: [PATCH 11/11] fix type --- .../stac_fastapi/extensions/core/transaction/transaction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py index d2e77eef3..a30124d91 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction/transaction.py @@ -1,7 +1,7 @@ """Transaction extension.""" from enum import Enum -from typing import Any, List, Optional, Type, Union +from typing import Any, Dict, List, Optional, Type, Union import attr from fastapi import APIRouter, Body, FastAPI @@ -108,7 +108,7 @@ class PatchCollection(CollectionUri): ] } # ref: https://github.com/pydantic/pydantic/issues/889 -_patch_item_schema_dict: Any = _patch_item_schema +_patch_item_schema_dict: Dict[str, Any] = _patch_item_schema _patch_item_schema_dict["items"]["anyOf"] = list( _patch_item_schema_dict["$defs"].values() ) @@ -149,7 +149,7 @@ class PatchCollection(CollectionUri): ] } # ref: https://github.com/pydantic/pydantic/issues/889 -_patch_collection_schema_dict: Any = _patch_collection_schema +_patch_collection_schema_dict: Dict[str, Any] = _patch_collection_schema _patch_collection_schema_dict["items"]["anyOf"] = list( _patch_collection_schema_dict["$defs"].values() )