Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 87 additions & 23 deletions stac_fastapi/api/stac_fastapi/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi import APIRouter, FastAPI
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from stac_pydantic import Collection, Item, ItemCollection
from stac_pydantic import Catalog, Collection, Item, ItemCollection
from stac_pydantic.api import ConformanceClasses, LandingPage
from stac_pydantic.api.collections import Collections
from stac_pydantic.version import STAC_VERSION
Expand All @@ -16,6 +16,7 @@
from stac_fastapi.api.errors import DEFAULT_STATUS_CODES, add_exception_handlers
from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware
from stac_fastapi.api.models import (
CatalogUri,
CollectionUri,
EmptyRequest,
GeoJSONResponse,
Expand All @@ -32,6 +33,7 @@
from stac_fastapi.types.core import AsyncBaseCoreClient, BaseCoreClient
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest
from stac_fastapi.types.stac import Catalogs


@attr.s
Expand Down Expand Up @@ -138,9 +140,9 @@ def register_landing_page(self):
self.router.add_api_route(
name="Landing Page",
path="/",
response_model=LandingPage
if self.settings.enable_response_models
else None,
response_model=(
LandingPage if self.settings.enable_response_models else None
),
response_class=self.response_class,
response_model_exclude_unset=False,
response_model_exclude_none=True,
Expand All @@ -157,9 +159,9 @@ def register_conformance_classes(self):
self.router.add_api_route(
name="Conformance Classes",
path="/conformance",
response_model=ConformanceClasses
if self.settings.enable_response_models
else None,
response_model=(
ConformanceClasses if self.settings.enable_response_models else None
),
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -175,7 +177,7 @@ def register_get_item(self):
"""
self.router.add_api_route(
name="Get Item",
path="/collections/{collection_id}/items/{item_id}",
path="/catalogs/{catalog_id}collections/{collection_id}/items/{item_id}",
response_model=Item if self.settings.enable_response_models else None,
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
Expand All @@ -194,9 +196,11 @@ def register_post_search(self):
self.router.add_api_route(
name="Search",
path="/search",
response_model=(ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None,
response_model=(
(ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None
),
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -216,9 +220,11 @@ def register_get_search(self):
self.router.add_api_route(
name="Search",
path="/search",
response_model=(ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None,
response_model=(
(ItemCollection if not fields_ext else None)
if self.settings.enable_response_models
else None
),
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand All @@ -237,25 +243,63 @@ def register_get_collections(self):
self.router.add_api_route(
name="Get Collections",
path="/collections",
response_model=Collections
if self.settings.enable_response_models
else None,
response_model=(
Collections if self.settings.enable_response_models else None
),
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(self.client.all_collections, EmptyRequest),
)

def register_get_catalogs(self):
"""Register get catalogs endpoint (GET /catalogs).

Returns:
None
"""
self.router.add_api_route(
name="Get Catalogs",
path="/catalogs",
response_model=Catalogs if self.settings.enable_response_models else None,
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(self.client.all_catalogs, EmptyRequest),
)

def register_get_catalog_collections(self):
"""Register get catalogs collections endpoint (GET /catalogs/{catalog_id}/collections).

Returns:
None
"""
self.router.add_api_route(
name="Get CatalogCollections",
path="/catalogs/{catalog_id}/collections",
response_model=(
Collections if self.settings.enable_response_models else None
),
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(
self.client.get_catalog_collections, CatalogUri
),
)

def register_get_collection(self):
"""Register get collection endpoint (GET /collection/{collection_id}).
"""Register get collection endpoint (GET /catalogs/{catalog_id}/collection/{collection_id}).

Returns:
None
"""
self.router.add_api_route(
name="Get Collection",
path="/collections/{collection_id}",
path="/catalogs/{catalog_id}/collections/{collection_id}",
response_model=Collection if self.settings.enable_response_models else None,
response_class=self.response_class,
response_model_exclude_unset=True,
Expand All @@ -264,6 +308,23 @@ def register_get_collection(self):
endpoint=create_async_endpoint(self.client.get_collection, CollectionUri),
)

def register_get_catalog(self):
"""Register get catalog endpoint (GET /catalogs/{catalog_id}).

Returns:
None
"""
self.router.add_api_route(
name="Get Catalog",
path="/catalogs/{catalog_id}",
response_model=Catalog if self.settings.enable_response_models else None,
response_class=self.response_class,
response_model_exclude_unset=True,
response_model_exclude_none=True,
methods=["GET"],
endpoint=create_async_endpoint(self.client.get_catalog, CatalogUri),
)

def register_get_item_collection(self):
"""Register get item collection endpoint (GET /collection/{collection_id}/items).

Expand All @@ -282,10 +343,10 @@ def register_get_item_collection(self):
)
self.router.add_api_route(
name="Get ItemCollection",
path="/collections/{collection_id}/items",
response_model=ItemCollection
if self.settings.enable_response_models
else None,
path="/catalogs/{catalog_id}/collections/{collection_id}/items",
response_model=(
ItemCollection if self.settings.enable_response_models else None
),
response_class=GeoJSONResponse,
response_model_exclude_unset=True,
response_model_exclude_none=True,
Expand Down Expand Up @@ -316,8 +377,11 @@ def register_core(self):
self.register_post_search()
self.register_get_search()
self.register_get_collections()
self.register_get_catalogs()
self.register_get_collection()
self.register_get_catalog()
self.register_get_item_collection()
self.register_get_catalog_collections()

def customize_openapi(self) -> Optional[Dict[str, Any]]:
"""Customize openapi schema."""
Expand Down
3 changes: 2 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Application settings."""

import enum


Expand All @@ -17,10 +18,10 @@ class ApiExtensions(enum.Enum):
query = "query"
sort = "sort"
transaction = "transaction"
collection_search = "collection-search"


class AddOns(enum.Enum):
"""Enumeration of available third party add ons."""

bulk_transaction = "bulk-transaction"
collection_search = "collection-search"
1 change: 1 addition & 0 deletions stac_fastapi/api/stac_fastapi/api/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Api middleware."""

import re
import typing
from http.client import HTTP_PORT, HTTPS_PORT
Expand Down
21 changes: 15 additions & 6 deletions stac_fastapi/api/stac_fastapi/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from stac_pydantic.shared import BBox

from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.rfc3339 import DateTimeType
from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval
from stac_fastapi.types.search import (
APIRequest,
BaseSearchGetRequest,
Expand Down Expand Up @@ -49,9 +49,11 @@ def create_request_model(
for k, v in model.__fields__.items():
field_info = v.field_info
body = Body(
None
if isinstance(field_info.default, UndefinedType)
else field_info.default,
(
None
if isinstance(field_info.default, UndefinedType)
else field_info.default
),
default_factory=field_info.default_factory,
alias=field_info.alias,
alias_priority=field_info.alias_priority,
Expand Down Expand Up @@ -101,7 +103,14 @@ def create_post_request_model(


@attr.s # type:ignore
class CollectionUri(APIRequest):
class CatalogUri(APIRequest):
"""Delete catalog."""

catalog_id: str = attr.ib(default=Path(..., description="Catalog ID"))


@attr.s # type:ignore
class CollectionUri(CatalogUri):
"""Delete collection."""

collection_id: str = attr.ib(default=Path(..., description="Collection ID"))
Expand All @@ -127,7 +136,7 @@ class ItemCollectionUri(CollectionUri):

limit: int = attr.ib(default=10)
bbox: Optional[BBox] = attr.ib(default=None, converter=str2bbox)
datetime: Optional[DateTimeType] = attr.ib(default=None)
datetime: Optional[DateTimeType] = attr.ib(default=None, converter=str_to_interval)


class POSTTokenPagination(BaseModel):
Expand Down
7 changes: 4 additions & 3 deletions stac_fastapi/api/stac_fastapi/api/openapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""openapi."""

import warnings

from fastapi import FastAPI
Expand Down Expand Up @@ -43,9 +44,9 @@ async def patched_openapi_endpoint(req: Request) -> Response:
# Get the response from the old endpoint function
response: JSONResponse = await old_endpoint(req)
# Update the content type header in place
response.headers[
"content-type"
] = "application/vnd.oai.openapi+json;version=3.0"
response.headers["content-type"] = (
"application/vnd.oai.openapi+json;version=3.0"
)
# Return the updated response
return response

Expand Down
1 change: 1 addition & 0 deletions stac_fastapi/api/stac_fastapi/api/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
"""Library version."""

__version__ = "2.5.2"
25 changes: 23 additions & 2 deletions stac_fastapi/api/tests/benchmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

collection_links = link_factory.CollectionLinks("/", "test").create_links()
item_links = link_factory.ItemLinks("/", "test", "test").create_links()
catalog_links = link_factory.CatalogLinks("/", "test").create_links()


collections = [
Expand Down Expand Up @@ -62,7 +63,9 @@ def get_search(
) -> stac_types.ItemCollection:
raise NotImplementedError

def get_item(self, item_id: str, collection_id: str, **kwargs) -> stac_types.Item:
def get_item(
self, item_id: str, collection_id: str, catalog_id: str, **kwargs
) -> stac_types.Item:
raise NotImplementedError

def all_collections(self, **kwargs) -> stac_types.Collections:
Expand All @@ -75,9 +78,27 @@ def all_collections(self, **kwargs) -> stac_types.Collections:
],
)

def get_collection(self, collection_id: str, **kwargs) -> stac_types.Collection:
def all_catalogs(self, **kwargs) -> stac_types.Catalogs:
return stac_types.Catalogs(
catalogs=catalogs,
links=[
{"href": "test", "rel": "root"},
{"href": "test", "rel": "self"},
{"href": "test", "rel": "parent"},
],
)

def get_collection(
self, catalog_id: str, collection_id: str, **kwargs
) -> stac_types.Collection:
return collections[0]

def get_catalog_collection(self, catalog_id: str, **kwargs) -> stac_types.Catalogs:
return collections[0]

def get_catalog(self, catalog_id: str, **kwargs) -> stac_types.Catalog:
return catalogs[0]

def item_collection(
self,
collection_id: str,
Expand Down
27 changes: 15 additions & 12 deletions stac_fastapi/api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,17 @@ def test_add_route_dependencies_after_building_api(self):


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(core.BaseTransactionsClient):
Expand All @@ -126,6 +120,15 @@ def update_collection(self, *args, **kwargs):
def delete_collection(self, *args, **kwargs):
return "dummy response"

def create_catalog(self, *args, **kwargs):
return "dummy response"

def update_catalog(self, *args, **kwargs):
return "dummy response"

def delete_catalog(self, *args, **kwargs):
return "dummy response"


def must_be_bob(
credentials: security.HTTPBasicCredentials = Depends(security.HTTPBasic()),
Expand Down
Loading