Skip to content
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
1 change: 1 addition & 0 deletions .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
if: ${{ matrix.python-version == env.LATEST_PY_VERSION }}
run: |
uv run pre-commit run --all-files
uv run --with mypy --with types-attrs mypy -p stac_fastapi

- name: Run tests
run: uv run pytest -svvv
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repos:
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
rev: v1.19.0
hooks:
- id: mypy
language_version: python
Expand Down
7 changes: 5 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

- support for python 3.9 and 3.10

## [6.1.3] - 2025-12-09

### Fixed

- fixed output type for `api.models.create_request_model` and `api.models.create_get_request_model` methods
- fixed type hints

## [6.1.2] - 2025-12-09

Expand Down Expand Up @@ -686,7 +688,8 @@ Full changelog: https://stac-utils.github.io/stac-fastapi/migrations/v3.0.0/#cha

* First PyPi release!

[Unreleased]: <https://github.com/stac-utils/stac-fastapi/compare/6.1.2..main>
[Unreleased]: <https://github.com/stac-utils/stac-fastapi/compare/6.1.3..main>
[6.1.3]: <https://github.com/stac-utils/stac-fastapi/compare/6.1.2..6.1.3>
[6.1.2]: <https://github.com/stac-utils/stac-fastapi/compare/6.1.1..6.1.2>
[6.1.1]: <https://github.com/stac-utils/stac-fastapi/compare/6.1.0..6.1.1>
[6.1.0]: <https://github.com/stac-utils/stac-fastapi/compare/6.0.0..6.1.0>
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6.1.1
6.1.3
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "stac-fastapi"
version = "6.1.1"
version = "6.1.3"
description = "Python library for building a STAC-compliant FastAPI application."
requires-python = ">=3.11"
readme = "README.md"
Expand Down Expand Up @@ -81,7 +81,7 @@ explicit_package_bases = true
exclude = ["tests", ".venv"]

[tool.bumpversion]
current_version = "6.1.1"
current_version = "6.1.3"
parse = """(?x)
(?P<major>\\d+)\\.
(?P<minor>\\d+)\\.
Expand Down
4 changes: 3 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def request_validation_exception_handler(
status_code=status.HTTP_400_BAD_REQUEST,
)

# TODO: Argument 2 to "add_exception_handler" of "Starlette" has incompatible type
app.add_exception_handler(
RequestValidationError, request_validation_exception_handler
RequestValidationError,
request_validation_exception_handler, # type: ignore
)
2 changes: 1 addition & 1 deletion stac_fastapi/api/stac_fastapi/api/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str, str, str]:
proto = scope.get("scheme", "http")
header_host = self._get_header_value_by_name(scope, "host")
if header_host is None:
domain, port = scope.get("server")
domain, port = scope["server"]
else:
header_host_parts = header_host.split(":")
if len(header_host_parts) == 2:
Expand Down
17 changes: 8 additions & 9 deletions stac_fastapi/api/stac_fastapi/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@
import orjson # noqa
from fastapi.responses import ORJSONResponse as JSONResponse
except ImportError: # pragma: nocover
from starlette.responses import JSONResponse
from starlette.responses import JSONResponse # type: ignore


def create_request_model(
model_name="SearchGetRequest",
base_model: Union[Type[BaseModel], Type[APIRequest]] = BaseSearchGetRequest,
extensions: Optional[List[ApiExtension]] = None,
mixins: Optional[Union[List[Type[BaseModel]], List[Type[APIRequest]]]] = None,
request_type: Optional[str] = "GET",
request_type: str = "GET",
) -> Union[Type[BaseModel], Type[APIRequest]]:
"""Create a pydantic model for validating request bodies."""
fields = {}
extension_models = []
extension_models: List[Union[Type[BaseModel], Type[APIRequest]]] = []

# Check extensions for additional parameters to search
for extension in extensions or []:
Expand All @@ -54,7 +54,7 @@ def create_request_model(
# Handle POST requests
elif all([issubclass(m, BaseModel) for m in models]):
for model in models:
for k, field_info in model.model_fields.items():
for k, field_info in model.model_fields.items(): # type: ignore
fields[k] = (field_info.annotation, field_info)

return create_model(model_name, **fields, __base__=base_model) # type: ignore
Expand All @@ -64,11 +64,10 @@ def create_request_model(

def create_get_request_model(
extensions: Optional[List[ApiExtension]],
base_model: BaseSearchGetRequest = BaseSearchGetRequest,
base_model: Type[BaseSearchGetRequest] = BaseSearchGetRequest,
) -> Type[APIRequest]:
"""Wrap create_request_model to create the GET request model."""

return create_request_model(
return create_request_model( # type: ignore
"SearchGetRequest",
base_model=base_model,
extensions=extensions,
Expand All @@ -78,10 +77,10 @@ def create_get_request_model(

def create_post_request_model(
extensions: Optional[List[ApiExtension]],
base_model: BaseSearchPostRequest = BaseSearchPostRequest,
base_model: Type[BaseSearchPostRequest] = BaseSearchPostRequest,
) -> Type[BaseModel]:
"""Wrap create_request_model to create the POST request model."""
return create_request_model(
return create_request_model( # type: ignore
"SearchPostRequest",
base_model=base_model,
extensions=extensions,
Expand Down
5 changes: 4 additions & 1 deletion stac_fastapi/api/stac_fastapi/api/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ def update_openapi(app: FastAPI) -> FastAPI:
content-type response header.
"""
# Find the route for the openapi_url in the app
# TODO: Type info is Route, while it shoukd maybe be APIRoute? Check FastAPI source.
openapi_route: Route = next(
route for route in app.router.routes if route.path == app.openapi_url
route
for route in app.router.routes
if route.path == app.openapi_url # type: ignore
)
# Store the old endpoint function so we can call it from the patched function
old_endpoint = openapi_route.endpoint
Expand Down
21 changes: 15 additions & 6 deletions stac_fastapi/api/stac_fastapi/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from starlette.routing import BaseRoute, Match
from starlette.status import HTTP_204_NO_CONTENT

from stac_fastapi.api.models import APIRequest
from stac_fastapi.types.search import APIRequest


def _wrap_response(resp: Any) -> Any:
Expand Down Expand Up @@ -58,7 +58,7 @@ async def _endpoint(request: Request, request_data: Dict[str, Any]):

elif issubclass(request_model, APIRequest):

async def _endpoint(request: Request, request_data=Depends(request_model)):
async def _endpoint(request: Request, request_data=Depends(request_model)): # type: ignore
"""Endpoint."""
return _wrap_response(await func(request=request, **request_data.kwargs()))

Expand Down Expand Up @@ -100,10 +100,14 @@ def add_route_dependencies(
_scope = copy.deepcopy(scope)
for route in routes:
if scope["path"] == "*":
_scope["path"] = route.path
# NOTE: ignore type, because BaseRoute has no "path" attribute
# but APIRoute does.
_scope["path"] = route.path # type: ignore

# NOTE: ignore type, because BaseRoute has no "method" attribute
# but APIRoute does.
if scope["method"] == "*":
_scope["method"] = list(route.methods)[0]
_scope["method"] = list(route.methods)[0] # type: ignore

match, _ = route.matches({"type": "http", **_scope})
if match != Match.FULL:
Expand All @@ -119,7 +123,10 @@ def add_route_dependencies(
route.dependant.dependencies.insert(
0,
get_parameterless_sub_dependant(
depends=depends, path=route.path_format
# NOTE: ignore type, because BaseRoute has no "path_format"
# attribute but APIRoute does.
depends=depends,
path=route.path_format, # type: ignore
),
)

Expand All @@ -128,7 +135,9 @@ def add_route_dependencies(
# app.include_router(router))
# https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360
# https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678
route.dependencies.extend(dependencies)
# NOTE: ignore type, because BaseRoute has no "dependencies" attribute
# but APIRoute does.
route.dependencies.extend(dependencies) # type: ignore


def add_direct_response(app: FastAPI) -> None:
Expand Down
2 changes: 1 addition & 1 deletion stac_fastapi/api/stac_fastapi/api/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Library version."""

__version__ = "6.1.1"
__version__ = "6.1.3"
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Aggregation Extension."""
from enum import Enum
from typing import List, Union
from typing import List, Type, Union

import attr
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel

from stac_fastapi.api.models import CollectionUri, EmptyRequest
from stac_fastapi.api.routes import create_async_endpoint
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import APIRequest

from .client import AsyncBaseAggregationClient, BaseAggregationClient
from .request import AggregationExtensionGetRequest, AggregationExtensionPostRequest
Expand Down Expand Up @@ -50,8 +52,8 @@ class AggregationExtension(ApiExtension):
conformance_classes: Conformance classes provided by the extension
"""

GET = AggregationExtensionGetRequest
POST = AggregationExtensionPostRequest
GET: Type[APIRequest] = AggregationExtensionGetRequest
POST: Type[BaseModel] = AggregationExtensionPostRequest

client: Union[AsyncBaseAggregationClient, BaseAggregationClient] = attr.ib(
factory=BaseAggregationClient
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""Collection-Search extension."""

from enum import Enum
from typing import List, Optional, Union
from typing import List, Optional, Type, Union

import attr
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from stac_pydantic.api.collections import Collections
from stac_pydantic.shared import MimeTypes

from stac_fastapi.api.models import GeoJSONResponse, create_request_model
from stac_fastapi.api.routes import create_async_endpoint
from stac_fastapi.types.config import ApiSettings
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import APIRequest

from .client import AsyncBaseCollectionSearchClient, BaseCollectionSearchClient
from .request import BaseCollectionSearchGetRequest, BaseCollectionSearchPostRequest
Expand Down Expand Up @@ -47,8 +49,8 @@ class CollectionSearchExtension(ApiExtension):
the extension
"""

GET: BaseCollectionSearchGetRequest = attr.ib(default=BaseCollectionSearchGetRequest) # type: ignore
POST = attr.ib(init=False)
GET: Type[APIRequest] = attr.ib(default=BaseCollectionSearchGetRequest)
POST: Optional[Type[BaseModel]] = attr.ib(init=False)

conformance_classes: List[str] = attr.ib(
default=[
Expand Down Expand Up @@ -93,7 +95,7 @@ def from_extensions(
)

return cls(
GET=get_request_model,
GET=get_request_model, # type: ignore
conformance_classes=conformance_classes,
schema_href=schema_href,
)
Expand Down Expand Up @@ -127,10 +129,8 @@ class CollectionSearchPostExtension(CollectionSearchExtension):
schema_href: Optional[str] = attr.ib(default=None)
router: APIRouter = attr.ib(factory=APIRouter)

GET: BaseCollectionSearchGetRequest = attr.ib(default=BaseCollectionSearchGetRequest) # type: ignore
POST: BaseCollectionSearchPostRequest = attr.ib( # type: ignore
default=BaseCollectionSearchPostRequest
)
GET: Type[APIRequest] = attr.ib(default=BaseCollectionSearchGetRequest)
POST: Type[BaseModel] = attr.ib(default=BaseCollectionSearchPostRequest)

def register(self, app: FastAPI) -> None:
"""Register the extension with a FastAPI application.
Expand Down Expand Up @@ -198,8 +198,8 @@ def from_extensions( # type: ignore
return cls(
client=client,
settings=settings,
GET=get_request_model,
POST=post_request_model,
GET=get_request_model, # type: ignore
POST=post_request_model, # type: ignore
conformance_classes=conformance_classes,
router=router or APIRouter(),
schema_href=schema_href,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Fields extension."""

from enum import Enum
from typing import List, Optional
from typing import List, Optional, Type

import attr
from fastapi import FastAPI
from pydantic import BaseModel

from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import APIRequest

from .request import FieldsExtensionGetRequest, FieldsExtensionPostRequest

Expand Down Expand Up @@ -42,8 +44,8 @@ class FieldsExtension(ApiExtension):
the extension
"""

GET = FieldsExtensionGetRequest
POST = FieldsExtensionPostRequest
GET: Type[APIRequest] = FieldsExtensionGetRequest
POST: Type[BaseModel] = FieldsExtensionPostRequest

conformance_classes: List[str] = attr.ib(
factory=lambda: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

import attr
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel
from starlette.responses import Response

from stac_fastapi.api.models import CollectionUri, EmptyRequest, JSONSchemaResponse
from stac_fastapi.api.routes import create_async_endpoint
from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import APIRequest

from .client import AsyncBaseFiltersClient, BaseFiltersClient
from .request import FilterExtensionGetRequest, FilterExtensionPostRequest
Expand Down Expand Up @@ -71,8 +73,8 @@ class FilterExtension(ApiExtension):
conformance_classes: Conformance classes provided by the extension
"""

GET = FilterExtensionGetRequest
POST = FilterExtensionPostRequest
GET: Type[APIRequest] = FilterExtensionGetRequest
POST: Type[BaseModel] = FilterExtensionPostRequest

client: Union[AsyncBaseFiltersClient, BaseFiltersClient] = attr.ib(
factory=BaseFiltersClient
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Free-text extension."""

from enum import Enum
from typing import List, Optional
from typing import List, Optional, Type

import attr
from fastapi import FastAPI
from pydantic import BaseModel

from stac_fastapi.types.extension import ApiExtension
from stac_fastapi.types.search import APIRequest

from .request import (
FreeTextAdvancedExtensionGetRequest,
Expand Down Expand Up @@ -51,8 +53,8 @@ class FreeTextExtension(ApiExtension):

"""

GET = FreeTextExtensionGetRequest
POST = FreeTextExtensionPostRequest
GET: Type[APIRequest] = FreeTextExtensionGetRequest
POST: Type[BaseModel] = FreeTextExtensionPostRequest

conformance_classes: List[str] = attr.ib(
default=[
Expand Down Expand Up @@ -84,8 +86,8 @@ class FreeTextAdvancedExtension(ApiExtension):

"""

GET = FreeTextAdvancedExtensionGetRequest
POST = FreeTextAdvancedExtensionPostRequest
GET: Type[APIRequest] = FreeTextAdvancedExtensionGetRequest
POST: Type[BaseModel] = FreeTextAdvancedExtensionPostRequest

conformance_classes: List[str] = attr.ib(
default=[
Expand Down
Loading
Loading