diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68c3b8567..4c77f0967 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,3 +5,13 @@ repos: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.15.0 + hooks: + - id: mypy + language_version: python + exclude: tests/.* + additional_dependencies: + - types-attrs + - pydantic diff --git a/CHANGES.md b/CHANGES.md index c1020119d..172882cb1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,18 @@ ## [Unreleased] +### Changed + +- switched from `attr.s` to `attrs.define` for dataclasses definition. +- Extension's classes now HAVE TO be defined with `@define(slots=False)` +- switched from `attr.id` to `attrs.field` for dataclasses's attributes definition - remove support of `cql-json` in Filter extension +### Added + +- `py.typed` files for each sub-modules +- type checking in `pre-commit` + ## [5.2.1] - 2025-04-18 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 12b4afbd5..d6d50b810 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,12 @@ section-order = ["future", "standard-library", "third-party", "first-party", "lo [tool.ruff.format] quote-style = "double" +[tool.mypy] +ignore_missing_imports = true +namespace_packages = true +explicit_package_bases = true +exclude = ["tests", ".venv"] + [tool.bumpversion] current_version = "5.2.1" parse = """(?x) diff --git a/stac_fastapi/api/stac_fastapi/api/app.py b/stac_fastapi/api/stac_fastapi/api/app.py index ed39c31a7..206b1aa96 100644 --- a/stac_fastapi/api/stac_fastapi/api/app.py +++ b/stac_fastapi/api/stac_fastapi/api/app.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union -import attr +import attrs from brotli_asgi import BrotliMiddleware from fastapi import APIRouter, FastAPI from fastapi.params import Depends @@ -39,7 +39,7 @@ from stac_fastapi.types.search import BaseSearchGetRequest, BaseSearchPostRequest -@attr.s +@attrs.define class StacApi: """StacApi factory. @@ -75,30 +75,30 @@ class StacApi: """ - settings: ApiSettings = attr.ib() - client: Union[AsyncBaseCoreClient, BaseCoreClient] = attr.ib() - extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list)) - exceptions: Dict[Type[Exception], int] = attr.ib( - default=attr.Factory(lambda: DEFAULT_STATUS_CODES) + settings: ApiSettings = attrs.field() + client: Union[AsyncBaseCoreClient, BaseCoreClient] = attrs.field() + extensions: List[ApiExtension] = attrs.field(factory=list) + exceptions: Dict[Type[Exception], int] = attrs.field( + factory=lambda: DEFAULT_STATUS_CODES ) - title: str = attr.ib( - default=attr.Factory( + title: str = attrs.field( + default=attrs.Factory( lambda self: self.settings.stac_fastapi_title, takes_self=True ) ) - api_version: str = attr.ib( - default=attr.Factory( + api_version: str = attrs.field( + default=attrs.Factory( lambda self: self.settings.stac_fastapi_version, takes_self=True ) ) - stac_version: str = attr.ib(default=STAC_VERSION) - description: str = attr.ib( - default=attr.Factory( + stac_version: str = attrs.field(default=STAC_VERSION) + description: str = attrs.field( + default=attrs.Factory( lambda self: self.settings.stac_fastapi_description, takes_self=True ) ) - app: FastAPI = attr.ib( - default=attr.Factory( + app: FastAPI = attrs.field( + default=attrs.Factory( lambda self: FastAPI( openapi_url=self.settings.openapi_url, docs_url=self.settings.docs_url, @@ -112,20 +112,20 @@ class StacApi: ), converter=update_openapi, ) - router: APIRouter = attr.ib(default=attr.Factory(APIRouter)) - search_get_request_model: Type[BaseSearchGetRequest] = attr.ib( + router: APIRouter = attrs.field(default=attrs.Factory(APIRouter)) + search_get_request_model: Type[BaseSearchGetRequest] = attrs.field( default=BaseSearchGetRequest ) - search_post_request_model: Type[BaseSearchPostRequest] = attr.ib( + search_post_request_model: Type[BaseSearchPostRequest] = attrs.field( default=BaseSearchPostRequest ) - collections_get_request_model: Type[APIRequest] = attr.ib(default=EmptyRequest) - collection_get_request_model: Type[APIRequest] = attr.ib(default=CollectionUri) - items_get_request_model: Type[APIRequest] = attr.ib(default=ItemCollectionUri) - item_get_request_model: Type[APIRequest] = attr.ib(default=ItemUri) - response_class: Type[Response] = attr.ib(default=JSONResponse) - middlewares: List[Middleware] = attr.ib( - default=attr.Factory( + collections_get_request_model: Type[APIRequest] = attrs.field(default=EmptyRequest) + collection_get_request_model: Type[APIRequest] = attrs.field(default=CollectionUri) + items_get_request_model: Type[APIRequest] = attrs.field(default=ItemCollectionUri) + item_get_request_model: Type[APIRequest] = attrs.field(default=ItemUri) + response_class: Type[Response] = attrs.field(default=JSONResponse) + middlewares: List[Middleware] = attrs.field( + default=attrs.Factory( lambda: [ Middleware(BrotliMiddleware), Middleware(CORSMiddleware), @@ -133,8 +133,8 @@ class StacApi: ] ) ) - route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[]) - health_check: Union[Callable[[], Dict], Callable[[], Awaitable[Dict]]] = attr.ib( + route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attrs.field(default=[]) + health_check: Union[Callable[[], Dict], Callable[[], Awaitable[Dict]]] = attrs.field( default=lambda: {"status": "UP"} ) diff --git a/stac_fastapi/api/stac_fastapi/api/models.py b/stac_fastapi/api/stac_fastapi/api/models.py index fc764efe1..ba1405dd0 100644 --- a/stac_fastapi/api/stac_fastapi/api/models.py +++ b/stac_fastapi/api/stac_fastapi/api/models.py @@ -2,7 +2,7 @@ from typing import List, Literal, Optional, Type, Union -import attr +import attrs from fastapi import Path, Query from pydantic import BaseModel, create_model from stac_pydantic.shared import BBox @@ -49,14 +49,19 @@ def create_request_model( # Handle GET requests if all([issubclass(m, APIRequest) for m in models]): - return attr.make_class(model_name, attrs={}, bases=tuple(models)) + return attrs.make_class( + model_name, + attrs={**{}}, + bases=tuple(models), + ) # 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(): fields[k] = (field_info.annotation, field_info) - return create_model(model_name, **fields, __base__=base_model) + + return create_model(model_name, **fields, __base__=base_model) # type: ignore raise TypeError("Mixed Request Model types. Check extension request types.") @@ -88,41 +93,41 @@ def create_post_request_model( ) -@attr.s +@attrs.define class CollectionUri(APIRequest): """Get or delete collection.""" - collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() + collection_id: Annotated[str, Path(description="Collection ID")] = attrs.field() -@attr.s +@attrs.define class ItemUri(APIRequest): """Get or delete item.""" - collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() - item_id: Annotated[str, Path(description="Item ID")] = attr.ib() + collection_id: Annotated[str, Path(description="Collection ID")] = attrs.field() + item_id: Annotated[str, Path(description="Item ID")] = attrs.field() -@attr.s +@attrs.define class EmptyRequest(APIRequest): """Empty request.""" ... -@attr.s +@attrs.define class ItemCollectionUri(APIRequest, DatetimeMixin): """Get item collection.""" - collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() + collection_id: Annotated[str, Path(description="Collection ID")] = attrs.field() limit: Annotated[ Optional[Limit], Query( description="Limits the number of results that are included in each page of the response (capped to 10_000)." # noqa: E501 ), - ] = attr.ib(default=10) - bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) - datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime) + ] = attrs.field(default=10) + bbox: Optional[BBox] = attrs.field(default=None, converter=_bbox_converter) + datetime: DateTimeQueryType = attrs.field(default=None, validator=_validate_datetime) class GeoJSONResponse(JSONResponse): diff --git a/stac_fastapi/api/stac_fastapi/api/py.typed b/stac_fastapi/api/stac_fastapi/api/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/stac_fastapi/api/stac_fastapi/api/routes.py b/stac_fastapi/api/stac_fastapi/api/routes.py index 79150c473..9235ef54a 100644 --- a/stac_fastapi/api/stac_fastapi/api/routes.py +++ b/stac_fastapi/api/stac_fastapi/api/routes.py @@ -3,7 +3,7 @@ import copy import functools import inspect -from typing import Any, Callable, Dict, List, Optional, Type, TypedDict, Union +from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, TypedDict, Union from fastapi import Depends, FastAPI, params from fastapi.datastructures import DefaultPlaceholder @@ -48,30 +48,26 @@ def create_async_endpoint( if not inspect.iscoroutinefunction(func): func = sync_to_async(func) - if issubclass(request_model, APIRequest): + _endpoint: Callable[[Any, Any], Awaitable[Any]] + + if isinstance(request_model, dict): async def _endpoint( request: Request, - request_data: request_model = Depends(), # type:ignore + request_data: Dict[str, Any], ): """Endpoint.""" - return _wrap_response(await func(request=request, **request_data.kwargs())) + return _wrap_response(await func(request_data, request=request)) - elif issubclass(request_model, BaseModel): + elif issubclass(request_model, APIRequest): - async def _endpoint( - request: Request, - request_data: request_model, # type:ignore - ): + async def _endpoint(request: Request, request_data=Depends(request_model)): """Endpoint.""" - return _wrap_response(await func(request_data, request=request)) + return _wrap_response(await func(request=request, **request_data.kwargs())) - else: + elif issubclass(request_model, BaseModel): - async def _endpoint( - request: Request, - request_data: Dict[str, Any], # type:ignore - ): + async def _endpoint(request: Request, request_data: request_model): # type: ignore """Endpoint.""" return _wrap_response(await func(request_data, request=request)) diff --git a/stac_fastapi/api/tests/test_app.py b/stac_fastapi/api/tests/test_app.py index 5500303ef..41cb1b9fb 100644 --- a/stac_fastapi/api/tests/test_app.py +++ b/stac_fastapi/api/tests/test_app.py @@ -1,6 +1,6 @@ from typing import List, Optional, Union -import attr +import attrs import pytest from fastapi import Path, Query from fastapi.testclient import TestClient @@ -385,25 +385,25 @@ def item_collection( def test_request_model(AsyncTestCoreClient): """Test if request models are passed correctly.""" - @attr.s + @attrs.define class CollectionsRequest(APIRequest): - user: Annotated[str, Query(...)] = attr.ib() + user: Annotated[str, Query(...)] = attrs.field() - @attr.s + @attrs.define class CollectionRequest(APIRequest): - collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() - user: Annotated[str, Query(...)] = attr.ib() + collection_id: Annotated[str, Path(description="Collection ID")] = attrs.field() + user: Annotated[str, Query(...)] = attrs.field() - @attr.s + @attrs.define class ItemsRequest(APIRequest): - collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() - user: Annotated[str, Query(...)] = attr.ib() + collection_id: Annotated[str, Path(description="Collection ID")] = attrs.field() + user: Annotated[str, Query(...)] = attrs.field() - @attr.s + @attrs.define class ItemRequest(APIRequest): - collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib() - item_id: Annotated[str, Path(description="Item ID")] = attr.ib() - user: Annotated[str, Query(...)] = attr.ib() + collection_id: Annotated[str, Path(description="Collection ID")] = attrs.field() + item_id: Annotated[str, Path(description="Item ID")] = attrs.field() + user: Annotated[str, Query(...)] = attrs.field() test_app = app.StacApi( settings=ApiSettings(), 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 c6e892914..35b97f266 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/aggregation.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/aggregation.py @@ -2,7 +2,7 @@ from enum import Enum from typing import List, Union -import attr +import attrs from fastapi import APIRouter, FastAPI from stac_fastapi.api.models import CollectionUri, EmptyRequest @@ -23,7 +23,7 @@ class AggregationConformanceClasses(str, Enum): AGGREGATION = "https://api.stacspec.org/v0.3.0/aggregation" -@attr.s +@attrs.define class AggregationExtension(ApiExtension): """Aggregation Extension. @@ -53,14 +53,14 @@ class AggregationExtension(ApiExtension): GET = AggregationExtensionGetRequest POST = AggregationExtensionPostRequest - client: Union[AsyncBaseAggregationClient, BaseAggregationClient] = attr.ib( + client: Union[AsyncBaseAggregationClient, BaseAggregationClient] = attrs.field( factory=BaseAggregationClient ) - conformance_classes: List[str] = attr.ib( - default=[AggregationConformanceClasses.AGGREGATION] + conformance_classes: List[str] = attrs.field( + default=[AggregationConformanceClasses.AGGREGATION.value] ) - router: APIRouter = attr.ib(factory=APIRouter) + router: APIRouter = attrs.field(factory=APIRouter) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/client.py index 23d90fb28..9d8c8f65a 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/client.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/client.py @@ -3,7 +3,7 @@ import abc from typing import List, Optional, Union -import attr +import attrs from geojson_pydantic.geometries import Geometry from stac_pydantic.shared import BBox @@ -12,7 +12,7 @@ from .types import Aggregation, AggregationCollection -@attr.s +@attrs.define class BaseAggregationClient(abc.ABC): """Defines a pattern for implementing the STAC aggregation extension.""" @@ -67,7 +67,7 @@ def aggregate( ) -@attr.s +@attrs.define class AsyncBaseAggregationClient(abc.ABC): """Defines an async pattern for implementing the STAC aggregation extension.""" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py index 4e72e0005..f73df9f8f 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/aggregation/request.py @@ -2,7 +2,7 @@ from typing import List, Optional -import attr +import attrs from fastapi import Query from pydantic import Field from typing_extensions import Annotated @@ -23,11 +23,13 @@ def _agg_converter( return str2list(val) -@attr.s +@attrs.define(slots=False) class AggregationExtensionGetRequest(BaseSearchGetRequest): """Aggregation Extension GET request model.""" - aggregations: Optional[List[str]] = attr.ib(default=None, converter=_agg_converter) + aggregations: Optional[List[str]] = attrs.field( + default=None, converter=_agg_converter + ) class AggregationExtensionPostRequest(BaseSearchPostRequest): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/client.py index ac148dfb4..b8b0d3a23 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/client.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/client.py @@ -2,14 +2,14 @@ import abc -import attr +import attrs -from stac_fastapi.types import stac +from stac_fastapi.types.stac import ItemCollection from .request import BaseCollectionSearchPostRequest -@attr.s +@attrs.define class AsyncBaseCollectionSearchClient(abc.ABC): """Defines a pattern for implementing the STAC collection-search POST extension.""" @@ -18,7 +18,7 @@ async def post_all_collections( self, search_request: BaseCollectionSearchPostRequest, **kwargs, - ) -> stac.ItemCollection: + ) -> ItemCollection: """Get all available collections. Called with `POST /collections`. @@ -30,14 +30,14 @@ async def post_all_collections( ... -@attr.s +@attrs.define class BaseCollectionSearchClient(abc.ABC): """Defines a pattern for implementing the STAC collection-search POST extension.""" @abc.abstractmethod def post_all_collections( self, search_request: BaseCollectionSearchPostRequest, **kwargs - ) -> stac.ItemCollection: + ) -> ItemCollection: """Get all available collections. Called with `POST /collections`. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py index e5ab77b50..ba7b563cd 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/collection_search.py @@ -1,9 +1,9 @@ """Collection-Search extension.""" from enum import Enum -from typing import List, Optional, Union +from typing import Any, List, Optional, Sequence, Union -import attr +import attrs from fastapi import APIRouter, FastAPI from stac_pydantic.api.collections import Collections from stac_pydantic.shared import MimeTypes @@ -28,7 +28,7 @@ class CollectionSearchConformanceClasses(str, Enum): BASIS = "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/simple-query" -@attr.s +@attrs.define class CollectionSearchExtension(ApiExtension): """Collection-Search Extension. @@ -47,16 +47,18 @@ class CollectionSearchExtension(ApiExtension): the extension """ - GET: BaseCollectionSearchGetRequest = attr.ib(default=BaseCollectionSearchGetRequest) - POST = None + GET: BaseCollectionSearchGetRequest = attrs.field( + default=BaseCollectionSearchGetRequest + ) + POST = attrs.field(default=None, init=False) - conformance_classes: List[str] = attr.ib( + conformance_classes: Sequence[str] = attrs.field( default=[ - CollectionSearchConformanceClasses.COLLECTIONSEARCH, - CollectionSearchConformanceClasses.BASIS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH.value, + CollectionSearchConformanceClasses.BASIS.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -73,13 +75,15 @@ def register(self, app: FastAPI) -> None: def from_extensions( cls, extensions: List[ApiExtension], + *, schema_href: Optional[str] = None, + **kwargs: Any, ) -> "CollectionSearchExtension": """Create CollectionSearchExtension object from extensions.""" conformance_classes = [ - CollectionSearchConformanceClasses.COLLECTIONSEARCH, - CollectionSearchConformanceClasses.BASIS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH.value, + CollectionSearchConformanceClasses.BASIS.value, ] for ext in extensions: conformance_classes.extend(ext.conformance_classes) @@ -98,7 +102,7 @@ def from_extensions( ) -@attr.s +@attrs.define class CollectionSearchPostExtension(CollectionSearchExtension): """Collection-Search Extension. @@ -115,19 +119,23 @@ class CollectionSearchPostExtension(CollectionSearchExtension): the extension """ - client: Union[AsyncBaseCollectionSearchClient, BaseCollectionSearchClient] = attr.ib() - settings: ApiSettings = attr.ib() - conformance_classes: List[str] = attr.ib( + client: Union[ + AsyncBaseCollectionSearchClient, BaseCollectionSearchClient + ] = attrs.field() + settings: ApiSettings = attrs.field() + conformance_classes: Sequence[str] = attrs.field( default=[ - CollectionSearchConformanceClasses.COLLECTIONSEARCH, - CollectionSearchConformanceClasses.BASIS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH.value, + CollectionSearchConformanceClasses.BASIS.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) - router: APIRouter = attr.ib(factory=APIRouter) + schema_href: Optional[str] = attrs.field(default=None) + router: APIRouter = attrs.field(factory=APIRouter) - GET: BaseCollectionSearchGetRequest = attr.ib(default=BaseCollectionSearchGetRequest) - POST: BaseCollectionSearchPostRequest = attr.ib( + GET: BaseCollectionSearchGetRequest = attrs.field( + default=BaseCollectionSearchGetRequest + ) + POST: BaseCollectionSearchPostRequest = attrs.field( default=BaseCollectionSearchPostRequest ) @@ -163,19 +171,19 @@ def register(self, app: FastAPI) -> None: app.include_router(self.router) @classmethod - def from_extensions( + def from_extensions( # type: ignore cls, extensions: List[ApiExtension], *, + schema_href: Optional[str] = None, client: Union[AsyncBaseCollectionSearchClient, BaseCollectionSearchClient], settings: ApiSettings, - schema_href: Optional[str] = None, router: Optional[APIRouter] = None, ) -> "CollectionSearchPostExtension": """Create CollectionSearchPostExtension object from extensions.""" conformance_classes = [ - CollectionSearchConformanceClasses.COLLECTIONSEARCH, - CollectionSearchConformanceClasses.BASIS, + CollectionSearchConformanceClasses.COLLECTIONSEARCH.value, + CollectionSearchConformanceClasses.BASIS.value, ] for ext in extensions: conformance_classes.extend(ext.conformance_classes) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py index 2ac4b608d..716b5a827 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/collection_search/request.py @@ -3,7 +3,7 @@ from datetime import datetime as dt from typing import List, Optional, Tuple, cast -import attr +import attrs from fastapi import Query from pydantic import BaseModel, Field, PrivateAttr, ValidationInfo, field_validator from stac_pydantic.api.search import SearchDatetime @@ -20,18 +20,18 @@ ) -@attr.s +@attrs.define(slots=False) class BaseCollectionSearchGetRequest(APIRequest, DatetimeMixin): """Basics additional Collection-Search parameters for the GET request.""" - bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) - datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime) + bbox: Optional[BBox] = attrs.field(default=None, converter=_bbox_converter) + datetime: DateTimeQueryType = attrs.field(default=None, validator=_validate_datetime) limit: Annotated[ Optional[Limit], Query( description="Limits the number of results that are included in each page of the response." # noqa: E501 ), - ] = attr.ib(default=10) + ] = attrs.field(default=10) class BaseCollectionSearchPostRequest(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py index 0b6e4177d..f4939d43b 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/fields.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -23,7 +23,7 @@ class FieldsConformanceClasses(str, Enum): COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#fields" -@attr.s +@attrs.define class FieldsExtension(ApiExtension): """Fields Extension. @@ -45,12 +45,12 @@ class FieldsExtension(ApiExtension): GET = FieldsExtensionGetRequest POST = FieldsExtensionPostRequest - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( factory=lambda: [ - FieldsConformanceClasses.SEARCH, + FieldsConformanceClasses.SEARCH.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py index 28b0003d6..2ca2f9f4d 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/fields/request.py @@ -2,7 +2,7 @@ from typing import Dict, List, Optional, Set -import attr +import attrs from fastapi import Query from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -59,11 +59,11 @@ def _fields_converter( return str2list(val) -@attr.s +@attrs.define(slots=False) class FieldsExtensionGetRequest(APIRequest): """Additional fields for the GET request.""" - fields: Optional[List[str]] = attr.ib(default=None, converter=_fields_converter) + fields: Optional[List[str]] = attrs.field(default=None, converter=_fields_converter) class FieldsExtensionPostRequest(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/client.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/client.py index 2ee59b770..d8d0662f9 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/client.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/client.py @@ -3,10 +3,10 @@ import abc from typing import Any, Dict, Optional -import attr +import attrs -@attr.s +@attrs.define class AsyncBaseFiltersClient(abc.ABC): """Defines a pattern for implementing the STAC filter extension.""" @@ -32,7 +32,7 @@ async def get_queryables( } -@attr.s +@attrs.define class BaseFiltersClient(abc.ABC): """Defines a pattern for implementing the STAC filter extension.""" 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 7e8d7f57c..7fa97b23a 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/filter.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Type, Union -import attr +import attrs from fastapi import APIRouter, FastAPI from starlette.responses import Response @@ -48,7 +48,7 @@ class FilterConformanceClasses(str, Enum): ) -@attr.s +@attrs.define class FilterExtension(ApiExtension): """Filter Extension. @@ -68,21 +68,21 @@ class FilterExtension(ApiExtension): GET = FilterExtensionGetRequest POST = FilterExtensionPostRequest - client: Union[AsyncBaseFiltersClient, BaseFiltersClient] = attr.ib( + client: Union[AsyncBaseFiltersClient, BaseFiltersClient] = attrs.field( factory=BaseFiltersClient ) - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( default=[ - FilterConformanceClasses.FILTER, - FilterConformanceClasses.SEARCH, - FilterConformanceClasses.ITEMS, - FilterConformanceClasses.BASIC_CQL2, - FilterConformanceClasses.CQL2_JSON, - FilterConformanceClasses.CQL2_TEXT, + FilterConformanceClasses.FILTER.value, + FilterConformanceClasses.SEARCH.value, + FilterConformanceClasses.ITEMS.value, + FilterConformanceClasses.BASIC_CQL2.value, + FilterConformanceClasses.CQL2_JSON.value, + FilterConformanceClasses.CQL2_TEXT.value, ] ) - router: APIRouter = attr.ib(factory=APIRouter) - response_class: Type[Response] = attr.ib(default=JSONSchemaResponse) + router: APIRouter = attrs.field(factory=APIRouter) + response_class: Type[Response] = attrs.field(default=JSONSchemaResponse) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -127,17 +127,17 @@ def register(self, app: FastAPI) -> None: app.include_router(self.router, tags=["Filter Extension"]) -@attr.s +@attrs.define class SearchFilterExtension(FilterExtension): """Item Search Filter Extension.""" - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( default=[ - FilterConformanceClasses.FILTER, - FilterConformanceClasses.SEARCH, - FilterConformanceClasses.BASIC_CQL2, - FilterConformanceClasses.CQL2_JSON, - FilterConformanceClasses.CQL2_TEXT, + FilterConformanceClasses.FILTER.value, + FilterConformanceClasses.SEARCH.value, + FilterConformanceClasses.BASIC_CQL2.value, + FilterConformanceClasses.CQL2_JSON.value, + FilterConformanceClasses.CQL2_TEXT.value, ] ) @@ -169,17 +169,17 @@ def register(self, app: FastAPI) -> None: app.include_router(self.router, tags=["Filter Extension"]) -@attr.s +@attrs.define class ItemCollectionFilterExtension(FilterExtension): """Item Collection Filter Extension.""" - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( default=[ - FilterConformanceClasses.FILTER, - FilterConformanceClasses.ITEMS, - FilterConformanceClasses.BASIC_CQL2, - FilterConformanceClasses.CQL2_JSON, - FilterConformanceClasses.CQL2_TEXT, + FilterConformanceClasses.FILTER.value, + FilterConformanceClasses.ITEMS.value, + FilterConformanceClasses.BASIC_CQL2.value, + FilterConformanceClasses.CQL2_JSON.value, + FilterConformanceClasses.CQL2_TEXT.value, ] ) @@ -211,17 +211,17 @@ def register(self, app: FastAPI) -> None: app.include_router(self.router, tags=["Filter Extension"]) -@attr.s +@attrs.define class CollectionSearchFilterExtension(FilterExtension): """Collection Search Filter Extension.""" - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( default=[ - FilterConformanceClasses.FILTER, - FilterConformanceClasses.COLLECTIONS, - FilterConformanceClasses.BASIC_CQL2, - FilterConformanceClasses.CQL2_JSON, - FilterConformanceClasses.CQL2_TEXT, + FilterConformanceClasses.FILTER.value, + FilterConformanceClasses.COLLECTIONS.value, + FilterConformanceClasses.BASIC_CQL2.value, + FilterConformanceClasses.CQL2_JSON.value, + FilterConformanceClasses.CQL2_TEXT.value, ] ) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py index 08514e4fc..051e51e50 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/filter/request.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Literal, Optional -import attr +import attrs from fastapi import Query from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -12,7 +12,7 @@ FilterLang = Literal["cql2-json", "cql2-text"] -@attr.s +@attrs.define(slots=False) class FilterExtensionGetRequest(APIRequest): """Filter extension GET request model.""" @@ -30,21 +30,21 @@ class FilterExtensionGetRequest(APIRequest): }, }, ), - ] = attr.ib(default=None) + ] = attrs.field(default=None) filter_crs: Annotated[ Optional[str], Query( alias="filter-crs", description="The coordinate reference system (CRS) used by spatial literals in the 'filter' value. Default is `http://www.opengis.net/def/crs/OGC/1.3/CRS84`", # noqa: E501 ), - ] = attr.ib(default=None) + ] = attrs.field(default=None) filter_lang: Annotated[ Optional[FilterLang], Query( alias="filter-lang", description="The CQL filter encoding that the 'filter' value uses.", ), - ] = attr.ib(default="cql2-text") + ] = attrs.field(default="cql2-text") class FilterExtensionPostRequest(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py index 67aaa2b27..0ec13dfb5 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/free_text.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -40,7 +40,7 @@ class FreeTextConformanceClasses(str, Enum): ) -@attr.s +@attrs.define class FreeTextExtension(ApiExtension): """Free-text Extension. @@ -54,12 +54,12 @@ class FreeTextExtension(ApiExtension): GET = FreeTextExtensionGetRequest POST = FreeTextExtensionPostRequest - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( default=[ - FreeTextConformanceClasses.SEARCH, + FreeTextConformanceClasses.SEARCH.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -73,7 +73,7 @@ def register(self, app: FastAPI) -> None: pass -@attr.s +@attrs.define class FreeTextAdvancedExtension(ApiExtension): """Free-text Extension. @@ -87,12 +87,12 @@ class FreeTextAdvancedExtension(ApiExtension): GET = FreeTextAdvancedExtensionGetRequest POST = FreeTextAdvancedExtensionPostRequest - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( default=[ - FreeTextConformanceClasses.SEARCH_ADVANCED, + FreeTextConformanceClasses.SEARCH_ADVANCED.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py index 67de422d3..54706f061 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/free_text/request.py @@ -2,7 +2,7 @@ from typing import List, Optional -import attr +import attrs from fastapi import Query from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -27,11 +27,11 @@ def _ft_converter( return None -@attr.s +@attrs.define(slots=False) class FreeTextExtensionGetRequest(APIRequest): """Free-text Extension GET request model.""" - q: Optional[List[str]] = attr.ib(default=None, converter=_ft_converter) + q: Optional[List[str]] = attrs.field(default=None, converter=_ft_converter) class FreeTextExtensionPostRequest(BaseModel): @@ -43,7 +43,7 @@ class FreeTextExtensionPostRequest(BaseModel): ) -@attr.s +@attrs.define(slots=False) class FreeTextAdvancedExtensionGetRequest(APIRequest): """Free-text Extension GET request model.""" @@ -56,7 +56,7 @@ class FreeTextAdvancedExtensionGetRequest(APIRequest): "Coastal": {"value": "ocean,coast"}, }, ), - ] = attr.ib(default=None) + ] = attrs.field(default=None) class FreeTextAdvancedExtensionPostRequest(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/offset_pagination.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/offset_pagination.py index 81c1429dc..ee54062c8 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/offset_pagination.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/offset_pagination.py @@ -2,7 +2,7 @@ from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -10,7 +10,7 @@ from .request import GETOffsetPagination, POSTOffsetPagination -@attr.s +@attrs.define class OffsetPaginationExtension(ApiExtension): """Offset Pagination. @@ -23,8 +23,8 @@ class OffsetPaginationExtension(ApiExtension): GET = GETOffsetPagination POST = POSTOffsetPagination - conformance_classes: List[str] = attr.ib(factory=list) - schema_href: Optional[str] = attr.ib(default=None) + conformance_classes: List[str] = attrs.field(factory=list) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py index 7959b0357..005ecc8aa 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/pagination.py @@ -2,7 +2,7 @@ from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -10,7 +10,7 @@ from .request import GETPagination, POSTPagination -@attr.s +@attrs.define class PaginationExtension(ApiExtension): """Token Pagination. @@ -23,8 +23,8 @@ class PaginationExtension(ApiExtension): GET = GETPagination POST = POSTPagination - conformance_classes: List[str] = attr.ib(factory=list) - schema_href: Optional[str] = attr.ib(default=None) + conformance_classes: List[str] = attrs.field(factory=list) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/request.py index ffa2e3225..8ab2c0b67 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/request.py @@ -2,7 +2,7 @@ from typing import Optional -import attr +import attrs from fastapi import Query from pydantic import BaseModel from typing_extensions import Annotated @@ -10,11 +10,11 @@ from stac_fastapi.types.search import APIRequest -@attr.s +@attrs.define(slots=False) class GETTokenPagination(APIRequest): """Token pagination for GET requests.""" - token: Annotated[Optional[str], Query()] = attr.ib(default=None) + token: Annotated[Optional[str], Query()] = attrs.field(default=None) class POSTTokenPagination(BaseModel): @@ -23,11 +23,11 @@ class POSTTokenPagination(BaseModel): token: Optional[str] = None -@attr.s +@attrs.define(slots=False) class GETPagination(APIRequest): """Page based pagination for GET requests.""" - page: Annotated[Optional[str], Query()] = attr.ib(default=None) + page: Annotated[Optional[str], Query()] = attrs.field(default=None) class POSTPagination(BaseModel): @@ -36,11 +36,11 @@ class POSTPagination(BaseModel): page: Optional[str] = None -@attr.s +@attrs.define(slots=False) class GETOffsetPagination(APIRequest): """Offset pagination for GET requests.""" - offset: Annotated[Optional[int], Query()] = attr.ib(default=None) + offset: Annotated[Optional[int], Query()] = attrs.field(default=None) class POSTOffsetPagination(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py index 11ccfb35b..945f12b99 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/pagination/token_pagination.py @@ -2,7 +2,7 @@ from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -10,7 +10,7 @@ from .request import GETTokenPagination, POSTTokenPagination -@attr.s +@attrs.define class TokenPaginationExtension(ApiExtension): """Token Pagination. @@ -23,8 +23,8 @@ class TokenPaginationExtension(ApiExtension): GET = GETTokenPagination POST = POSTTokenPagination - conformance_classes: List[str] = attr.ib(factory=list) - schema_href: Optional[str] = attr.ib(default=None) + conformance_classes: List[str] = attrs.field(factory=list) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py index 9f4c8cb0c..56b39c837 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/query.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -22,7 +22,7 @@ class QueryConformanceClasses(str, Enum): COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#query" -@attr.s +@attrs.define(slots=False) class QueryExtension(ApiExtension): """Query Extension. @@ -35,12 +35,12 @@ class QueryExtension(ApiExtension): GET = QueryExtensionGetRequest POST = QueryExtensionPostRequest - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( factory=lambda: [ - QueryConformanceClasses.SEARCH, + QueryConformanceClasses.SEARCH.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py index c18e2e37d..b37dc051a 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/query/request.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Optional -import attr +import attrs from fastapi import Query from pydantic import BaseModel, Field from typing_extensions import Annotated @@ -10,7 +10,7 @@ from stac_fastapi.types.search import APIRequest -@attr.s +@attrs.define class QueryExtensionGetRequest(APIRequest): """Query Extension GET request model.""" @@ -23,7 +23,7 @@ class QueryExtensionGetRequest(APIRequest): "cloudy": {"value": '{"eo:cloud_cover": {"gte": 95}}'}, }, ), - ] = attr.ib(default=None) + ] = attrs.field(default=None) class QueryExtensionPostRequest(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py index fc38ab772..ecbda9142 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/request.py @@ -2,7 +2,7 @@ from typing import List, Optional -import attr +import attrs from fastapi import Query from pydantic import BaseModel, Field from stac_pydantic.api.extensions.sort import SortExtension as PostSortModel @@ -27,11 +27,11 @@ def _sort_converter( return str2list(val) -@attr.s +@attrs.define(slots=False) class SortExtensionGetRequest(APIRequest): """Sortby Parameter for GET requests.""" - sortby: Optional[List[str]] = attr.ib(default=None, converter=_sort_converter) + sortby: Optional[List[str]] = attrs.field(default=None, converter=_sort_converter) class SortExtensionPostRequest(BaseModel): diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py index 77984719f..fae872754 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/sort/sort.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from stac_fastapi.types.extension import ApiExtension @@ -23,7 +23,7 @@ class SortConformanceClasses(str, Enum): COLLECTIONS = "https://api.stacspec.org/v1.0.0-rc.1/collection-search#sort" -@attr.s +@attrs.define class SortExtension(ApiExtension): """Sort Extension. @@ -35,12 +35,12 @@ class SortExtension(ApiExtension): GET = SortExtensionGetRequest POST = SortExtensionPostRequest - conformance_classes: List[str] = attr.ib( + conformance_classes: List[str] = attrs.field( factory=lambda: [ - SortConformanceClasses.SEARCH, + SortConformanceClasses.SEARCH.value, ] ) - schema_href: Optional[str] = attr.ib(default=None) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 2287bc8b0..258c8424b 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -3,7 +3,7 @@ from enum import Enum from typing import List, Optional, Type, Union -import attr +import attrs from fastapi import APIRouter, Body, FastAPI from stac_pydantic import Collection, Item, ItemCollection from stac_pydantic.shared import MimeTypes @@ -28,28 +28,28 @@ class TransactionConformanceClasses(str, Enum): COLLECTIONS = "https://api.stacspec.org/v1.0.0/collections/extensions/transaction" -@attr.s +@attrs.define class PostItem(CollectionUri): """Create Item.""" - item: Annotated[Union[Item, ItemCollection], Body()] = attr.ib(default=None) + item: Annotated[Union[Item, ItemCollection], Body()] = attrs.field(default=None) -@attr.s +@attrs.define class PutItem(ItemUri): """Update Item.""" - item: Annotated[Item, Body()] = attr.ib(default=None) + item: Annotated[Item, Body()] = attrs.field(default=None) -@attr.s +@attrs.define class PutCollection(CollectionUri): """Update Collection.""" - collection: Annotated[Collection, Body()] = attr.ib(default=None) + collection: Annotated[Collection, Body()] = attrs.field(default=None) -@attr.s +@attrs.define class TransactionExtension(ApiExtension): """Transaction Extension. @@ -70,17 +70,17 @@ class TransactionExtension(ApiExtension): """ - client: Union[AsyncBaseTransactionsClient, BaseTransactionsClient] = attr.ib() - settings: ApiSettings = attr.ib() - conformance_classes: List[str] = attr.ib( + client: Union[AsyncBaseTransactionsClient, BaseTransactionsClient] = attrs.field() + settings: ApiSettings = attrs.field() + conformance_classes: List[str] = attrs.field( factory=lambda: [ TransactionConformanceClasses.ITEMS, TransactionConformanceClasses.COLLECTIONS, ] ) - schema_href: Optional[str] = attr.ib(default=None) - router: APIRouter = attr.ib(factory=APIRouter) - response_class: Type[Response] = attr.ib(default=JSONResponse) + schema_href: Optional[str] = attrs.field(default=None) + router: APIRouter = attrs.field(factory=APIRouter) + response_class: Type[Response] = attrs.field(default=JSONResponse) def register_create_item(self): """Register create item endpoint (POST /collections/{collection_id}/items).""" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/py.typed b/stac_fastapi/extensions/stac_fastapi/extensions/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py index d1faa5c0f..16ec56121 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/third_party/bulk_transactions.py @@ -4,7 +4,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Union -import attr +import attrs from fastapi import APIRouter, FastAPI from pydantic import BaseModel @@ -31,7 +31,7 @@ def __iter__(self): return iter(self.items.values()) -@attr.s # type: ignore +@attrs.define class BaseBulkTransactionsClient(abc.ABC): """BulkTransactionsClient.""" @@ -63,7 +63,7 @@ def bulk_item_insert( raise NotImplementedError -@attr.s # type: ignore +@attrs.define class AsyncBaseBulkTransactionsClient(abc.ABC): """BulkTransactionsClient.""" @@ -84,7 +84,7 @@ async def bulk_item_insert( raise NotImplementedError -@attr.s +@attrs.define class BulkTransactionExtension(ApiExtension): """Bulk Transaction Extension. @@ -110,9 +110,11 @@ class BulkTransactionExtension(ApiExtension): } """ - client: Union[AsyncBaseBulkTransactionsClient, BaseBulkTransactionsClient] = attr.ib() - conformance_classes: List[str] = attr.ib(default=list()) - schema_href: Optional[str] = attr.ib(default=None) + client: Union[ + AsyncBaseBulkTransactionsClient, BaseBulkTransactionsClient + ] = attrs.field() + conformance_classes: List[str] = attrs.field(default=list()) + schema_href: Optional[str] = attrs.field(default=None) def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. diff --git a/stac_fastapi/extensions/tests/test_collection_search.py b/stac_fastapi/extensions/tests/test_collection_search.py index 50fa79be7..cb378467d 100644 --- a/stac_fastapi/extensions/tests/test_collection_search.py +++ b/stac_fastapi/extensions/tests/test_collection_search.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from urllib.parse import quote_plus -import attr +import attrs import pytest from starlette.testclient import TestClient @@ -80,7 +80,7 @@ def item_collection(self, *args, **kwargs): raise NotImplementedError -@attr.s +@attrs.define class DummyPostClient(BaseCollectionSearchClient): def post_all_collections( self, search_request: BaseCollectionSearchPostRequest, **kwargs diff --git a/stac_fastapi/extensions/tests/test_pagination.py b/stac_fastapi/extensions/tests/test_pagination.py index ba1d5b31c..9c549a7d9 100644 --- a/stac_fastapi/extensions/tests/test_pagination.py +++ b/stac_fastapi/extensions/tests/test_pagination.py @@ -45,38 +45,37 @@ def item_collection(self, *args, **kwargs): return args, kwargs -collections_get_request_model = create_request_model( - model_name="CollectionsGetRequest", - base_model=EmptyRequest, - mixins=[ - OffsetPaginationExtension().GET, - ], - request_type="GET", -) - -items_get_request_model = create_request_model( - model_name="ItemsGetRequest", - base_model=EmptyRequest, - mixins=[ - PaginationExtension().GET, - ], - request_type="GET", -) - -search_get_request_model = create_request_model( - model_name="SearchGetRequest", - base_model=BaseSearchGetRequest, - mixins=[ - TokenPaginationExtension().GET, - ], - request_type="GET", -) - - @pytest.fixture def client() -> Iterator[TestClient]: settings = ApiSettings() + collections_get_request_model = create_request_model( + model_name="CollectionsGetRequest", + base_model=EmptyRequest, + mixins=[ + OffsetPaginationExtension().GET, + ], + request_type="GET", + ) + + items_get_request_model = create_request_model( + model_name="ItemsGetRequest", + base_model=EmptyRequest, + mixins=[ + PaginationExtension().GET, + ], + request_type="GET", + ) + + search_get_request_model = create_request_model( + model_name="SearchGetRequest", + base_model=BaseSearchGetRequest, + mixins=[ + TokenPaginationExtension().GET, + ], + request_type="GET", + ) + api = StacApi( settings=settings, client=DummyCoreClient(), diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index f90b72823..731a6adf0 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Union from urllib.parse import urljoin -import attr +import attrs from fastapi import Request from geojson_pydantic.geometries import Geometry from stac_pydantic import Collection, Item, ItemCollection @@ -13,12 +13,12 @@ from stac_pydantic.version import STAC_VERSION from starlette.responses import Response -from stac_fastapi.types import stac -from stac_fastapi.types.config import ApiSettings -from stac_fastapi.types.conformance import BASE_CONFORMANCE_CLASSES -from stac_fastapi.types.extension import ApiExtension -from stac_fastapi.types.requests import get_base_url -from stac_fastapi.types.search import BaseSearchPostRequest +from . import stac +from .config import ApiSettings +from .conformance import BASE_CONFORMANCE_CLASSES +from .extension import ApiExtension +from .requests import get_base_url +from .search import BaseSearchPostRequest __all__ = [ "NumType", @@ -36,7 +36,7 @@ api_settings = ApiSettings() -@attr.s # type:ignore +@attrs.define # type:ignore class BaseTransactionsClient(abc.ABC): """Defines a pattern for implementing the STAC API Transaction Extension.""" @@ -150,7 +150,7 @@ def delete_collection( ... -@attr.s # type:ignore +@attrs.define # type:ignore class AsyncBaseTransactionsClient(abc.ABC): """Defines a pattern for implementing the STAC transaction extension.""" @@ -263,14 +263,14 @@ async def delete_collection( ... -@attr.s +@attrs.define class LandingPageMixin(abc.ABC): """Create a STAC landing page (GET /).""" - stac_version: str = attr.ib(default=STAC_VERSION) - landing_page_id: str = attr.ib(default=api_settings.stac_fastapi_landing_id) - title: str = attr.ib(default=api_settings.stac_fastapi_title) - description: str = attr.ib(default=api_settings.stac_fastapi_description) + stac_version: str = attrs.field(default=STAC_VERSION) + landing_page_id: str = attrs.field(default=api_settings.stac_fastapi_landing_id) + title: str = attrs.field(default=api_settings.stac_fastapi_title) + description: str = attrs.field(default=api_settings.stac_fastapi_description) def _landing_page( self, @@ -331,7 +331,7 @@ def _landing_page( return landing_page -@attr.s # type:ignore +@attrs.define # type:ignore class BaseCoreClient(LandingPageMixin, abc.ABC): """Defines a pattern for implementing STAC api core endpoints. @@ -339,10 +339,10 @@ class BaseCoreClient(LandingPageMixin, abc.ABC): extensions: list of registered api extensions. """ - base_conformance_classes: List[str] = attr.ib( + base_conformance_classes: List[str] = attrs.field( factory=lambda: BASE_CONFORMANCE_CLASSES ) - extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list)) + extensions: List[ApiExtension] = attrs.field(default=attrs.Factory(list)) def conformance_classes(self) -> List[str]: """Generate conformance classes by adding extension conformance to base @@ -551,7 +551,7 @@ def item_collection( ... -@attr.s # type:ignore +@attrs.define # type:ignore class AsyncBaseCoreClient(LandingPageMixin, abc.ABC): """Defines a pattern for implementing STAC api core endpoints. @@ -559,10 +559,10 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC): extensions: list of registered api extensions. """ - base_conformance_classes: List[str] = attr.ib( + base_conformance_classes: List[str] = attrs.field( factory=lambda: BASE_CONFORMANCE_CLASSES ) - extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list)) + extensions: List[ApiExtension] = attrs.field(default=attrs.Factory(list)) def conformance_classes(self) -> List[str]: """Generate conformance classes by adding extension conformance to base diff --git a/stac_fastapi/types/stac_fastapi/types/extension.py b/stac_fastapi/types/stac_fastapi/types/extension.py index 22fed6068..0f4bcd863 100644 --- a/stac_fastapi/types/stac_fastapi/types/extension.py +++ b/stac_fastapi/types/stac_fastapi/types/extension.py @@ -3,12 +3,12 @@ import abc from typing import List, Optional -import attr +import attrs from fastapi import FastAPI from pydantic import BaseModel -@attr.s +@attrs.define class ApiExtension(abc.ABC): """Abstract base class for defining API extensions.""" @@ -22,8 +22,8 @@ def get_request_model(self, verb: str = "GET") -> Optional[BaseModel]: """ return getattr(self, verb) - conformance_classes: List[str] = attr.ib(factory=list) - schema_href: Optional[str] = attr.ib(default=None) + conformance_classes: List[str] = attrs.field(factory=list) + schema_href: Optional[str] = attrs.field(default=None) @abc.abstractmethod def register(self, app: FastAPI) -> None: diff --git a/stac_fastapi/types/stac_fastapi/types/links.py b/stac_fastapi/types/stac_fastapi/types/links.py index 28f05d6c0..7af4563a5 100644 --- a/stac_fastapi/types/stac_fastapi/types/links.py +++ b/stac_fastapi/types/stac_fastapi/types/links.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List from urllib.parse import urljoin -import attr +import attrs from stac_pydantic.links import Relations from stac_pydantic.shared import MimeTypes @@ -26,19 +26,19 @@ def resolve_links(links: list, base_url: str) -> List[Dict]: return filtered_links -@attr.s +@attrs.define class BaseLinks: """Create inferred links common to collections and items.""" - collection_id: str = attr.ib() - base_url: str = attr.ib() + collection_id: str = attrs.field() + base_url: str = attrs.field() def root(self) -> Dict[str, Any]: """Return the catalog root.""" return dict(rel=Relations.root, type=MimeTypes.json, href=self.base_url) -@attr.s +@attrs.define class CollectionLinks(BaseLinks): """Create inferred links specific to collections.""" @@ -67,11 +67,11 @@ def create_links(self) -> List[Dict[str, Any]]: return [self.self(), self.parent(), self.items(), self.root()] -@attr.s +@attrs.define class ItemLinks(BaseLinks): """Create inferred links specific to items.""" - item_id: str = attr.ib() + item_id: str = attrs.field() def self(self) -> Dict[str, Any]: """Create the `self` link.""" diff --git a/stac_fastapi/types/stac_fastapi/types/py.typed b/stac_fastapi/types/stac_fastapi/types/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/stac_fastapi/types/stac_fastapi/types/search.py b/stac_fastapi/types/stac_fastapi/types/search.py index 01e622738..4bf9f1850 100644 --- a/stac_fastapi/types/stac_fastapi/types/search.py +++ b/stac_fastapi/types/stac_fastapi/types/search.py @@ -4,7 +4,7 @@ from datetime import datetime as dt from typing import Dict, List, Optional, Union -import attr +import attrs from fastapi import HTTPException, Query from pydantic import Field, PositiveInt from pydantic.functional_validators import AfterValidator @@ -12,7 +12,7 @@ from stac_pydantic.shared import BBox from typing_extensions import Annotated -from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval +from .rfc3339 import DateTimeType, str_to_interval def crop(v: PositiveInt) -> PositiveInt: @@ -128,21 +128,21 @@ def _validate_datetime(instance, attribute, value): ] -@attr.s +@attrs.define(slots=False) class APIRequest: """Generic API Request base class.""" def kwargs(self) -> Dict: """Transform api request params into format which matches the signature of the endpoint.""" - return self.__dict__ + return attrs.asdict(self) -@attr.s +@attrs.define(slots=False) class DatetimeMixin: """Datetime Mixin.""" - datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime) + datetime: DateTimeQueryType = attrs.field(default=None, validator=_validate_datetime) def parse_datetime(self) -> Optional[DateTimeType]: """Return Datetime objects.""" @@ -167,15 +167,15 @@ def end_date(self) -> Optional[dt]: return parsed[1] if isinstance(parsed, tuple) else None -@attr.s +@attrs.define(slots=False) class BaseSearchGetRequest(APIRequest, DatetimeMixin): """Base arguments for GET Request.""" - collections: Optional[List[str]] = attr.ib( + collections: Optional[List[str]] = attrs.field( default=None, converter=_collection_converter ) - ids: Optional[List[str]] = attr.ib(default=None, converter=_ids_converter) - bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) + ids: Optional[List[str]] = attrs.field(default=None, converter=_ids_converter) + bbox: Optional[BBox] = attrs.field(default=None, converter=_bbox_converter) intersects: Annotated[ Optional[str], Query( @@ -221,14 +221,14 @@ class BaseSearchGetRequest(APIRequest, DatetimeMixin): }, }, ), - ] = attr.ib(default=None) - datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime) + ] = attrs.field(default=None) + datetime: DateTimeQueryType = attrs.field(default=None, validator=_validate_datetime) limit: Annotated[ Optional[Limit], Query( description="Limits the number of results that are included in each page of the response (capped to 10_000)." # noqa: E501 ), - ] = attr.ib(default=10) + ] = attrs.field(default=10) class BaseSearchPostRequest(Search): diff --git a/stac_fastapi/types/tests/test_limit.py b/stac_fastapi/types/tests/test_limit.py index d4c03f33e..5e5419c86 100644 --- a/stac_fastapi/types/tests/test_limit.py +++ b/stac_fastapi/types/tests/test_limit.py @@ -1,4 +1,5 @@ import pytest +from attrs import asdict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient from pydantic import ValidationError @@ -31,7 +32,7 @@ def test_limit_get_request(): @app.get("/test") def route(model=Depends(BaseSearchGetRequest)): - return model + return asdict(model) with TestClient(app) as client: resp = client.get(