From 0cddff7eecb9adbf992abaaddc6831c6202eff48 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Tue, 20 Aug 2024 12:43:19 +0100 Subject: [PATCH 01/24] Adding patch endpoints. --- .../extensions/core/transaction.py | 97 ++++-- stac_fastapi/types/stac_fastapi/types/core.py | 317 ++++++++++++++++++ .../types/stac_fastapi/types/transaction.py | 65 ++++ 3 files changed, 445 insertions(+), 34 deletions(-) create mode 100644 stac_fastapi/types/stac_fastapi/types/transaction.py diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index a1c2391f..44721be0 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -3,8 +3,8 @@ from typing import List, Optional, Type, Union import attr -from fastapi import APIRouter, Body, FastAPI -from stac_pydantic import Collection, Item, ItemCollection +from fastapi import APIRouter, FastAPI +from stac_pydantic import Collection, Item from stac_pydantic.shared import MimeTypes from starlette.responses import JSONResponse, Response @@ -13,27 +13,12 @@ from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransactionsClient from stac_fastapi.types.extension import ApiExtension - - -@attr.s -class PostItem(CollectionUri): - """Create Item.""" - - item: Union[Item, ItemCollection] = attr.ib(default=Body(None)) - - -@attr.s -class PutItem(ItemUri): - """Update Item.""" - - item: Item = attr.ib(default=Body(None)) - - -@attr.s -class PutCollection(CollectionUri): - """Update Collection.""" - - collection: Collection = attr.ib(default=Body(None)) +from stac_fastapi.types.transaction import ( + PatchOperation, + PostItem, + PutPatchCollection, + PutPatchItem, +) @attr.s @@ -110,7 +95,32 @@ def register_update_item(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], - endpoint=create_async_endpoint(self.client.update_item, PutItem), + endpoint=create_async_endpoint(self.client.update_item, PutPatchItem), + ) + + def register_patch_item(self): + """Register patch item endpoint (PATCH + /collections/{collection_id}/items/{item_id}).""" + self.router.add_api_route( + name="Patch Item", + path="/collections/{collection_id}/items/{item_id}", + response_model=Item if self.settings.enable_response_models else None, + responses={ + 200: { + "content": { + MimeTypes.geojson.value: {}, + }, + "model": Item, + } + }, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["PATCH"], + endpoint=create_async_endpoint( + self.client.patch_item, + Union[PutPatchItem, List[PatchOperation]], + ), ) def register_delete_item(self): @@ -135,11 +145,6 @@ def register_delete_item(self): endpoint=create_async_endpoint(self.client.delete_item, ItemUri), ) - def register_patch_item(self): - """Register patch item endpoint (PATCH - /collections/{collection_id}/items/{item_id}).""" - raise NotImplementedError - def register_create_collection(self): """Register create collection endpoint (POST /collections).""" self.router.add_api_route( @@ -180,7 +185,33 @@ def register_update_collection(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], - endpoint=create_async_endpoint(self.client.update_collection, PutCollection), + endpoint=create_async_endpoint( + self.client.update_collection, PutPatchCollection + ), + ) + + def register_patch_collection(self): + """Register patch collection endpoint (PATCH /collections/{collection_id}).""" + self.router.add_api_route( + name="Patch Collection", + path="/collections/{collection_id}", + response_model=Collection if self.settings.enable_response_models else None, + responses={ + 200: { + "content": { + MimeTypes.geojson.value: {}, + }, + "model": Collection, + } + }, + response_class=self.response_class, + response_model_exclude_unset=True, + response_model_exclude_none=True, + methods=["PATCH"], + endpoint=create_async_endpoint( + self.client.patch_collection, + Union[PutPatchCollection, List[PatchOperation]], + ), ) def register_delete_collection(self): @@ -204,10 +235,6 @@ def register_delete_collection(self): endpoint=create_async_endpoint(self.client.delete_collection, CollectionUri), ) - def register_patch_collection(self): - """Register patch collection endpoint (PATCH /collections/{collection_id}).""" - raise NotImplementedError - def register(self, app: FastAPI) -> None: """Register the extension with a FastAPI application. @@ -220,8 +247,10 @@ def register(self, app: FastAPI) -> None: self.router.prefix = app.state.router_prefix self.register_create_item() self.register_update_item() + self.register_patch_item() self.register_delete_item() self.register_create_collection() self.register_update_collection() + self.register_patch_collection() self.register_delete_collection() app.include_router(self.router, tags=["Transaction Extension"]) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 003a765e..6d536b94 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -22,6 +22,7 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest +from stac_fastapi.types.transaction import PatchOperation __all__ = [ "NumType", @@ -83,6 +84,93 @@ def update_item( """ ... + def patch_item( + self, + item_id: str, + collection_id: str, + patch: Union[Item, list[PatchOperation]], + request: Request, + **kwargs, + ) -> Optional[Union[stac.Item, Response]]: + """Update an item from a collection. + + Called with `PATCH /collections/{collection_id}/items/{item_id}` + + Args: + item_id: id of the item. + collection_id: id of the collection. + patch: either the partial item or list of patch operations. + + Returns: + The patched item. + """ + content_type = request.headers["content-type"] + if isinstance(patch, list) and content_type == "application/json-patch+json": + return self.json_patch_item( + item_id, + collection_id, + patch, + **kwargs, + ) + + elif isinstance(patch, Item) and content_type in [ + "application/merge-patch+json", + "application/json", + ]: + return self.merge_patch_item( + item_id, + collection_id, + patch, + **kwargs, + ) + + else: + raise NotImplementedError("Content-Type and body combination not implemented") + + @abc.abstractmethod + def merge_patch_item( + self, + item_id: str, + collection_id: str, + item: Item, + **kwargs, + ) -> Optional[Union[stac.Item, Response]]: + """Update an item from a collection. + + Called with `PATCH /collections/{collection_id}/items/{item_id}` + + Args: + item_id: id of the item. + collection_id: id of the collection. + item: the partial item. + + Returns: + The patched item. + """ + ... + + @abc.abstractmethod + def json_patch_item( + self, + item_id: str, + collection_id: str, + operations: list[PatchOperation], + **kwargs, + ) -> Optional[Union[stac.Item, Response]]: + """Update an item from a collection. + + Called with `PATCH /collections/{collection_id}/items/{item_id}` + + Args: + item_id: id of the item. + collection_id: id of the collection. + operations: list of patch operations. + + Returns: + The patched item. + """ + ... + @abc.abstractmethod def delete_item( self, item_id: str, collection_id: str, **kwargs @@ -136,6 +224,77 @@ def update_collection( """ ... + def patch_collection( + self, + collection_id: str, + patch: Union[Collection, list[PatchOperation]], + request: Request, + **kwargs, + ) -> Optional[Union[stac.Collection, Response]]: + """Update a collection. + + Called with `PATCH /collections/{collection_id}` + + Args: + collection_id: id of the collection. + patch: either the partial collection or list of patch operations. + + Returns: + The patched collection. + """ + content_type = request.headers["content-type"] + if isinstance(patch, list) and content_type == "application/json-patch+json": + self.json_patch_collection(collection_id, patch, **kwargs) + + elif isinstance(patch, Collection) and content_type in [ + "application/merge-patch+json", + "application/json", + ]: + self.merge_patch_collection(collection_id, patch, **kwargs) + + else: + raise NotImplementedError("Content-Type and body combination not implemented") + + @abc.abstractmethod + def merge_patch_collection( + self, + collection_id: str, + collection: Collection, + **kwargs, + ) -> Optional[Union[stac.Collection, Response]]: + """Update a collection. + + Called with `PATCH /collections/{collection_id}` + + Args: + collection_id: id of the collection. + collection: the partial collection. + + Returns: + The patched collection. + """ + ... + + @abc.abstractmethod + def json_patch_collection( + self, + collection_id: str, + operations: list[PatchOperation], + **kwargs, + ) -> Optional[Union[stac.Collection, Response]]: + """Update a collection. + + Called with `PATCH /collections/{collection_id}` + + Args: + collection_id: id of the collection. + operations: list of patch operations. + + Returns: + The patched collection. + """ + ... + @abc.abstractmethod def delete_collection( self, collection_id: str, **kwargs @@ -196,6 +355,93 @@ async def update_item( """ ... + def patch_item( + self, + item_id: str, + collection_id: str, + patch: Union[Item, list[PatchOperation]], + request: Request, + **kwargs, + ) -> Optional[Union[stac.Item, Response]]: + """Update an item from a collection. + + Called with `PATCH /collections/{collection_id}/items/{item_id}` + + Args: + item_id: id of the item. + collection_id: id of the collection. + patch: either the partial item or list of patch operations. + + Returns: + The patched item. + """ + content_type = request.headers["content-type"] + if isinstance(patch, list) and content_type == "application/json-patch+json": + return self.json_patch_item( + item_id, + collection_id, + patch, + **kwargs, + ) + + elif isinstance(patch, Item) and content_type in [ + "application/merge-patch+json", + "application/json", + ]: + return self.merge_patch_item( + item_id, + collection_id, + patch, + **kwargs, + ) + + else: + raise NotImplementedError("Content-Type and body combination not implemented") + + @abc.abstractmethod + def merge_patch_item( + self, + item_id: str, + collection_id: str, + item: Item, + **kwargs, + ) -> Optional[Union[stac.Item, Response]]: + """Update an item from a collection. + + Called with `PATCH /collections/{collection_id}/items/{item_id}` + + Args: + item_id: id of the item. + collection_id: id of the collection. + item: the partial item. + + Returns: + The patched item. + """ + ... + + @abc.abstractmethod + def json_patch_item( + self, + item_id: str, + collection_id: str, + operations: list[PatchOperation], + **kwargs, + ) -> Optional[Union[stac.Item, Response]]: + """Update an item from a collection. + + Called with `PATCH /collections/{collection_id}/items/{item_id}` + + Args: + item_id: id of the item. + collection_id: id of the collection. + operations: list of patch operations. + + Returns: + The patched item. + """ + ... + @abc.abstractmethod async def delete_item( self, item_id: str, collection_id: str, **kwargs @@ -249,6 +495,77 @@ async def update_collection( """ ... + def patch_collection( + self, + collection_id: str, + patch: Union[Collection, list[PatchOperation]], + request: Request, + **kwargs, + ) -> Optional[Union[stac.Collection, Response]]: + """Update a collection. + + Called with `PATCH /collections/{collection_id}` + + Args: + collection_id: id of the collection. + patch: either the partial collection or list of patch operations. + + Returns: + The patched collection. + """ + content_type = request.headers["content-type"] + if isinstance(patch, list) and content_type == "application/json-patch+json": + self.json_patch_collection(collection_id, patch, **kwargs) + + elif isinstance(patch, Collection) and content_type in [ + "application/merge-patch+json", + "application/json", + ]: + self.merge_patch_collection(collection_id, patch, **kwargs) + + else: + raise NotImplementedError("Content-Type and body combination not implemented") + + @abc.abstractmethod + def merge_patch_collection( + self, + collection_id: str, + collection: Collection, + **kwargs, + ) -> Optional[Union[stac.Collection, Response]]: + """Update a collection. + + Called with `PATCH /collections/{collection_id}` + + Args: + collection_id: id of the collection. + collection: the partial collection. + + Returns: + The patched collection. + """ + ... + + @abc.abstractmethod + def json_patch_collection( + self, + collection_id: str, + operations: list[PatchOperation], + **kwargs, + ) -> Optional[Union[stac.Collection, Response]]: + """Update a collection. + + Called with `PATCH /collections/{collection_id}` + + Args: + collection_id: id of the collection. + operations: list of patch operations. + + Returns: + The patched collection. + """ + ... + @abc.abstractmethod async def delete_collection( self, collection_id: str, **kwargs diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py new file mode 100644 index 00000000..a7f8cf25 --- /dev/null +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -0,0 +1,65 @@ +"""Transaction extension types.""" + +from typing import Any, Literal, Union + +import attr +from fastapi import Body +from pydantic import BaseModel +from stac_pydantic import Collection, Item, ItemCollection + +from stac_fastapi.api.models import CollectionUri, ItemUri + + +@attr.s +class PostItem(CollectionUri): + """Create Item.""" + + item: Union[Item, ItemCollection] = attr.ib(default=Body(None)) + + +@attr.s +class PutPatchItem(ItemUri): + """Update Item.""" + + item: Item = attr.ib(default=Body(None)) + + +@attr.s +class PatchOperation(BaseModel): + """Update Operation.""" + + path: str = attr.ib() + + +@attr.s +class PatchAddReplaceTest(PatchOperation): + """Add, Replace or Test Operation.""" + + op: Literal["add", "replace", "test"] = attr.ib() + value: Any = attr.ib() + + +@attr.s +class PatchRemove(PatchOperation): + """Remove Operation.""" + + op: Literal["remove"] = attr.ib() + + +@attr.s +class PatchMoveCopy(PatchOperation): + """Move or Copy Operation.""" + + op: Literal["move", "copy"] = attr.ib() + + def __init__(self, *args, **kwargs): + """Init function to add 'from' field.""" + super().__init__(*args, **kwargs) + self.__setattr__("from", kwargs["from"]) + + +@attr.s +class PutPatchCollection(CollectionUri): + """Update Collection.""" + + collection: Collection = attr.ib(default=Body(None)) From 0c5de643a301619d4a2c87eb49048ed0061d9ff0 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Tue, 20 Aug 2024 14:30:07 +0100 Subject: [PATCH 02/24] Adding annotated from main. --- stac_fastapi/types/stac_fastapi/types/transaction.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py index a7f8cf25..e49a7d5c 100644 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -6,6 +6,7 @@ from fastapi import Body from pydantic import BaseModel from stac_pydantic import Collection, Item, ItemCollection +from typing_extensions import Annotated from stac_fastapi.api.models import CollectionUri, ItemUri @@ -14,14 +15,14 @@ class PostItem(CollectionUri): """Create Item.""" - item: Union[Item, ItemCollection] = attr.ib(default=Body(None)) + item: Annotated[Union[Item, ItemCollection], Body()] = attr.ib(default=None) @attr.s class PutPatchItem(ItemUri): """Update Item.""" - item: Item = attr.ib(default=Body(None)) + item: Annotated[Item, Body()] = attr.ib(default=None) @attr.s @@ -62,4 +63,4 @@ def __init__(self, *args, **kwargs): class PutPatchCollection(CollectionUri): """Update Collection.""" - collection: Collection = attr.ib(default=Body(None)) + collection: Annotated[Collection, Body()] = attr.ib(default=None) From bf2ddbbef368865f52ebe2d8ef63fd137b1049ac Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 21 Aug 2024 13:05:55 +0100 Subject: [PATCH 03/24] Fixing and adding tests. --- stac_fastapi/api/tests/test_api.py | 18 ++- .../extensions/core/transaction.py | 15 +-- .../extensions/tests/test_transaction.py | 103 +++++++++++++++++- stac_fastapi/types/stac_fastapi/types/core.py | 42 ++++--- .../types/stac_fastapi/types/transaction.py | 59 ++++++---- 5 files changed, 187 insertions(+), 50 deletions(-) diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index 7db4d9a5..a06f3899 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -3,10 +3,8 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import ItemCollectionUri, create_request_model -from stac_fastapi.extensions.core import ( - TokenPaginationExtension, - TransactionExtension, -) +from stac_fastapi.extensions.core import (TokenPaginationExtension, + TransactionExtension) from stac_fastapi.types import config, core @@ -430,6 +428,12 @@ def create_item(self, *args, **kwargs): def update_item(self, *args, **kwargs): return "dummy response" + def json_patch_item(self, *args, **kwargs): + return "dummy response" + + def merge_patch_item(self, *args, **kwargs): + return "dummy response" + def delete_item(self, *args, **kwargs): return "dummy response" @@ -439,6 +443,12 @@ def create_collection(self, *args, **kwargs): def update_collection(self, *args, **kwargs): return "dummy response" + def merge_patch_collection(self, *args, **kwargs): + return "dummy response" + + def json_patch_collection(self, *args, **kwargs): + return "dummy response" + def delete_collection(self, *args, **kwargs): return "dummy response" diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 098147a2..3315097c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -14,10 +14,11 @@ from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransactionsClient from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.transaction import ( - PatchOperation, + PatchCollection, + PatchItem, PostItem, - PutPatchCollection, - PutPatchItem, + PutCollection, + PutItem, ) @@ -95,7 +96,7 @@ def register_update_item(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], - endpoint=create_async_endpoint(self.client.update_item, PutPatchItem), + endpoint=create_async_endpoint(self.client.update_item, PutItem), ) def register_patch_item(self): @@ -119,7 +120,7 @@ def register_patch_item(self): methods=["PATCH"], endpoint=create_async_endpoint( self.client.patch_item, - Union[PutPatchItem, List[PatchOperation]], + PatchItem, ), ) @@ -186,7 +187,7 @@ def register_update_collection(self): response_model_exclude_none=True, methods=["PUT"], endpoint=create_async_endpoint( - self.client.update_collection, PutPatchCollection + self.client.update_collection, PutCollection ), ) @@ -210,7 +211,7 @@ def register_patch_collection(self): methods=["PATCH"], endpoint=create_async_endpoint( self.client.patch_collection, - Union[PutPatchCollection, List[PatchOperation]], + PatchCollection, ), ) diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index 689e519d..4e80d88a 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -1,5 +1,5 @@ import json -from typing import Iterator, Union +from typing import Iterator, List, Union import pytest from stac_pydantic import Collection @@ -11,6 +11,7 @@ from stac_fastapi.extensions.core import TransactionExtension from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.core import BaseCoreClient, BaseTransactionsClient +from stac_fastapi.types.transaction import PatchOperation class DummyCoreClient(BaseCoreClient): @@ -46,6 +47,32 @@ def update_item(self, collection_id: str, item_id: str, item: Item, **kwargs): "type": item.type, } + def merge_patch_item( + self, + collection_id: str, + item_id: str, + item: Collection, + **kwargs, + ): + return { + "path_collection_id": collection_id, + "path_item_id": item_id, + "type": item.type, + } + + def json_patch_item( + self, + collection_id: str, + item_id: str, + operations: List[PatchOperation], + **kwargs, + ): + return { + "path_collection_id": collection_id, + "path_item_id": item_id, + "first_op_type": operations[0].op, + } + def delete_item(self, item_id: str, collection_id: str, **kwargs): return { "path_collection_id": collection_id, @@ -58,6 +85,25 @@ def create_collection(self, collection: Collection, **kwargs): def update_collection(self, collection_id: str, collection: Collection, **kwargs): return {"path_collection_id": collection_id, "type": collection.type} + def merge_patch_collection( + self, + collection_id: str, + collection: Collection, + **kwargs, + ): + return {"path_collection_id": collection_id, "type": collection.type} + + def json_patch_collection( + self, + collection_id: str, + operations: List[PatchOperation], + **kwargs, + ): + return { + "path_collection_id": collection_id, + "first_op_type": operations[0].op, + } + def delete_collection(self, collection_id: str, **kwargs): return {"path_collection_id": collection_id} @@ -88,6 +134,32 @@ def test_update_item(client: TestClient, item: Item) -> None: assert response.json()["type"] == "Feature" +def test_merge_patch_item(client: TestClient, item: Item) -> None: + response = client.patch( + "/collections/a-collection/items/an-item", content=json.dumps(item) + ) + assert response.is_success, response.text + assert response.json()["path_collection_id"] == "a-collection" + assert response.json()["path_item_id"] == "an-item" + assert response.json()["type"] == "Feature" + + +def test_json_patch_item(client: TestClient) -> None: + operations = [ + {"op": "add", "path": "properties.new_prop", "value": "new_prop_value"} + ] + headers = {"Content-Type": "application/json-patch+json"} + response = client.patch( + "/collections/a-collection/items/an-item", + headers=headers, + content=json.dumps(operations), + ) + assert response.is_success, response.text + assert response.json()["path_collection_id"] == "a-collection" + assert response.json()["path_item_id"] == "an-item" + assert response.json()["first_op_type"] == "add" + + def test_delete_item(client: TestClient) -> None: response = client.delete("/collections/a-collection/items/an-item") assert response.is_success, response.text @@ -108,6 +180,31 @@ def test_update_collection(client: TestClient, collection: Collection) -> None: assert response.json()["type"] == "Collection" +def test_merge_patch_collection(client: TestClient, collection: Collection) -> None: + response = client.patch( + "/collections/a-collection", + content=json.dumps(collection), + ) + assert response.is_success, response.text + assert response.json()["path_collection_id"] == "a-collection" + assert response.json()["type"] == "Collection" + + +def test_json_patch_collection(client: TestClient) -> None: + operations = [ + {"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"} + ] + headers = {"Content-Type": "application/json-patch+json"} + response = client.patch( + "/collections/a-collection/items/an-item", + headers=headers, + content=json.dumps(operations), + ) + assert response.is_success, response.text + assert response.json()["path_collection_id"] == "a-collection" + assert response.json()["first_op_type"] == "add" + + def test_delete_collection(client: TestClient, collection: Collection) -> None: response = client.delete("/collections/a-collection") assert response.is_success, response.text @@ -176,7 +273,9 @@ def collection() -> Collection: "description": "A test collection", "extent": { "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": {"interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]}, + "temporal": { + "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]] + }, }, "links": [], "assets": {}, diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 5d3399a7..55ee1ca6 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -84,8 +84,8 @@ def update_item( def patch_item( self, - item_id: str, collection_id: str, + item_id: str, patch: Union[Item, list[PatchOperation]], request: Request, **kwargs, @@ -102,11 +102,11 @@ def patch_item( Returns: The patched item. """ - content_type = request.headers["content-type"] + content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_item( - item_id, collection_id, + item_id, patch, **kwargs, ) @@ -116,20 +116,22 @@ def patch_item( "application/json", ]: return self.merge_patch_item( - item_id, collection_id, + item_id, patch, **kwargs, ) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_item( self, - item_id: str, collection_id: str, + item_id: str, item: Item, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -150,8 +152,8 @@ def merge_patch_item( @abc.abstractmethod def json_patch_item( self, - item_id: str, collection_id: str, + item_id: str, operations: list[PatchOperation], **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -240,18 +242,20 @@ def patch_collection( Returns: The patched collection. """ - content_type = request.headers["content-type"] + content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": - self.json_patch_collection(collection_id, patch, **kwargs) + return self.json_patch_collection(collection_id, patch, **kwargs) elif isinstance(patch, Collection) and content_type in [ "application/merge-patch+json", "application/json", ]: - self.merge_patch_collection(collection_id, patch, **kwargs) + return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_collection( @@ -373,7 +377,7 @@ def patch_item( Returns: The patched item. """ - content_type = request.headers["content-type"] + content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_item( item_id, @@ -394,7 +398,9 @@ def patch_item( ) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_item( @@ -511,18 +517,20 @@ def patch_collection( Returns: The patched collection. """ - content_type = request.headers["content-type"] + content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": - self.json_patch_collection(collection_id, patch, **kwargs) + return self.json_patch_collection(collection_id, patch, **kwargs) elif isinstance(patch, Collection) and content_type in [ "application/merge-patch+json", "application/json", ]: - self.merge_patch_collection(collection_id, patch, **kwargs) + return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_collection( diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py index e49a7d5c..2ace9203 100644 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -1,6 +1,6 @@ """Transaction extension types.""" -from typing import Any, Literal, Union +from typing import Any, List, Literal, NewType, Union import attr from fastapi import Body @@ -19,48 +19,67 @@ class PostItem(CollectionUri): @attr.s -class PutPatchItem(ItemUri): - """Update Item.""" - - item: Annotated[Item, Body()] = attr.ib(default=None) - - -@attr.s -class PatchOperation(BaseModel): - """Update Operation.""" - - path: str = attr.ib() - - -@attr.s -class PatchAddReplaceTest(PatchOperation): +class PatchAddReplaceTest(BaseModel): """Add, Replace or Test Operation.""" + path: str = attr.ib() op: Literal["add", "replace", "test"] = attr.ib() value: Any = attr.ib() @attr.s -class PatchRemove(PatchOperation): +class PatchRemove(BaseModel): """Remove Operation.""" + path: str = attr.ib() op: Literal["remove"] = attr.ib() @attr.s -class PatchMoveCopy(PatchOperation): +class PatchMoveCopy(BaseModel): """Move or Copy Operation.""" + path: str = attr.ib() op: Literal["move", "copy"] = attr.ib() - def __init__(self, *args, **kwargs): + def __attrs_init__(self, *args, **kwargs): """Init function to add 'from' field.""" super().__init__(*args, **kwargs) self.__setattr__("from", kwargs["from"]) +PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove] + + +@attr.s +class PutItem(ItemUri): + """Update Item.""" + + item: Annotated[Item, Body()] = attr.ib(default=None) + + @attr.s -class PutPatchCollection(CollectionUri): +class PatchItem(ItemUri): + """Patch Item.""" + + patch: Annotated[ + Union[Item, List[PatchOperation]], + Body(), + ] = attr.ib(default=None) + + +@attr.s +class PutCollection(CollectionUri): """Update Collection.""" collection: Annotated[Collection, Body()] = attr.ib(default=None) + + +@attr.s +class PatchCollection(CollectionUri): + """Patch Collection.""" + + patch: Annotated[ + Union[Collection, List[PatchOperation]], + Body(), + ] = attr.ib(default=None) From 632d5a5081e0bfb7010f443285462f232817f988 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 21 Aug 2024 13:10:55 +0100 Subject: [PATCH 04/24] Updating changelog. --- CHANGES.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 992d23c3..a6b237b3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,10 +2,11 @@ ## [Unreleased] -## [3.0.0] - 2024-07-29 +* Add Item and Collection `PATCH` endpoints with support for [RFC 6902](https://tools.ietf.org/html/rfc6902) and [RFC 7396](https://tools.ietf.org/html/rfc7386) -Full changelog: https://stac-utils.github.io/stac-fastapi/migrations/v3.0.0/#changelog +## [3.0.0] - 2024-07-29 +Full changelog: https://stac-utils.github.io/stac-fastapi/migrations/v3.0.0/#changelog **Changes since 3.0.0b3:** ### Changed From 5f2b4fa46d97212546df3de6a3fcdf326c45bd46 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 21 Aug 2024 13:12:26 +0100 Subject: [PATCH 05/24] Fixing ruff errors. --- stac_fastapi/api/tests/test_api.py | 7 +++---- stac_fastapi/types/stac_fastapi/types/transaction.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/stac_fastapi/api/tests/test_api.py b/stac_fastapi/api/tests/test_api.py index a06f3899..dc37ccdc 100644 --- a/stac_fastapi/api/tests/test_api.py +++ b/stac_fastapi/api/tests/test_api.py @@ -3,8 +3,7 @@ from stac_fastapi.api.app import StacApi from stac_fastapi.api.models import ItemCollectionUri, create_request_model -from stac_fastapi.extensions.core import (TokenPaginationExtension, - TransactionExtension) +from stac_fastapi.extensions.core import TokenPaginationExtension, TransactionExtension from stac_fastapi.types import config, core @@ -430,7 +429,7 @@ def update_item(self, *args, **kwargs): def json_patch_item(self, *args, **kwargs): return "dummy response" - + def merge_patch_item(self, *args, **kwargs): return "dummy response" @@ -445,7 +444,7 @@ def update_collection(self, *args, **kwargs): def merge_patch_collection(self, *args, **kwargs): return "dummy response" - + def json_patch_collection(self, *args, **kwargs): return "dummy response" diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py index 2ace9203..3a9bd3c8 100644 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -1,6 +1,6 @@ """Transaction extension types.""" -from typing import Any, List, Literal, NewType, Union +from typing import Any, List, Literal, Union import attr from fastapi import Body From 010e2cbeacaf3ff6dff2a2c7df30484c82680349 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 21 Aug 2024 13:23:40 +0100 Subject: [PATCH 06/24] Ruff format. --- .../stac_fastapi/extensions/core/transaction.py | 8 ++------ .../extensions/tests/test_transaction.py | 12 +++--------- stac_fastapi/types/stac_fastapi/types/core.py | 16 ++++------------ 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 3315097c..d5343b1c 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -186,9 +186,7 @@ def register_update_collection(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["PUT"], - endpoint=create_async_endpoint( - self.client.update_collection, PutCollection - ), + endpoint=create_async_endpoint(self.client.update_collection, PutCollection), ) def register_patch_collection(self): @@ -233,9 +231,7 @@ def register_delete_collection(self): response_model_exclude_unset=True, response_model_exclude_none=True, methods=["DELETE"], - endpoint=create_async_endpoint( - self.client.delete_collection, CollectionUri - ), + endpoint=create_async_endpoint(self.client.delete_collection, CollectionUri), ) def register(self, app: FastAPI) -> None: diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index 4e80d88a..71f97074 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -145,9 +145,7 @@ def test_merge_patch_item(client: TestClient, item: Item) -> None: def test_json_patch_item(client: TestClient) -> None: - operations = [ - {"op": "add", "path": "properties.new_prop", "value": "new_prop_value"} - ] + operations = [{"op": "add", "path": "properties.new_prop", "value": "new_prop_value"}] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -191,9 +189,7 @@ def test_merge_patch_collection(client: TestClient, collection: Collection) -> N def test_json_patch_collection(client: TestClient) -> None: - operations = [ - {"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"} - ] + operations = [{"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"}] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -273,9 +269,7 @@ def collection() -> Collection: "description": "A test collection", "extent": { "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": { - "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]] - }, + "temporal": {"interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]}, }, "links": [], "assets": {}, diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 55ee1ca6..993f2151 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -123,9 +123,7 @@ def patch_item( ) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_item( @@ -253,9 +251,7 @@ def patch_collection( return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_collection( @@ -398,9 +394,7 @@ def patch_item( ) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_item( @@ -528,9 +522,7 @@ def patch_collection( return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_collection( From 0ccded052320f585cfca66f4f933229a43ade4a5 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 21 Aug 2024 13:26:23 +0100 Subject: [PATCH 07/24] Switching to List for python 3.8. --- stac_fastapi/types/stac_fastapi/types/core.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 993f2151..e9830edc 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -86,7 +86,7 @@ def patch_item( self, collection_id: str, item_id: str, - patch: Union[Item, list[PatchOperation]], + patch: Union[Item, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -152,7 +152,7 @@ def json_patch_item( self, collection_id: str, item_id: str, - operations: list[PatchOperation], + operations: List[PatchOperation], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -225,7 +225,7 @@ def update_collection( def patch_collection( self, collection_id: str, - patch: Union[Collection, list[PatchOperation]], + patch: Union[Collection, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: @@ -277,7 +277,7 @@ def merge_patch_collection( def json_patch_collection( self, collection_id: str, - operations: list[PatchOperation], + operations: List[PatchOperation], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -357,7 +357,7 @@ def patch_item( self, item_id: str, collection_id: str, - patch: Union[Item, list[PatchOperation]], + patch: Union[Item, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -423,7 +423,7 @@ def json_patch_item( self, item_id: str, collection_id: str, - operations: list[PatchOperation], + operations: List[PatchOperation], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -496,7 +496,7 @@ async def update_collection( def patch_collection( self, collection_id: str, - patch: Union[Collection, list[PatchOperation]], + patch: Union[Collection, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: @@ -548,7 +548,7 @@ def merge_patch_collection( def json_patch_collection( self, collection_id: str, - operations: list[PatchOperation], + operations: List[PatchOperation], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. From 1b46754a36e0d03f731efb2e8490f46b7906a634 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 21 Aug 2024 13:45:08 +0100 Subject: [PATCH 08/24] Updating docs make file. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index eef5dae3..a835bb52 100644 --- a/Makefile +++ b/Makefile @@ -11,12 +11,12 @@ install: .PHONY: docs-image docs-image: - docker-compose -f docker-compose.docs.yml \ + docker compose -f docker-compose.docs.yml \ build .PHONY: docs docs: docs-image - docker-compose -f docker-compose.docs.yml \ + docker compose -f docker-compose.docs.yml \ run docs .PHONY: test From 79c769cd255ff652be1614d111bf81182bf9d75b Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Fri, 23 Aug 2024 14:32:48 +0100 Subject: [PATCH 09/24] Switching from Item/Collection to Dict to allow partial updates. --- .../extensions/tests/test_transaction.py | 22 +++++--- stac_fastapi/types/stac_fastapi/types/core.py | 50 +++++++++++-------- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index 71f97074..e98aaf69 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -1,5 +1,5 @@ import json -from typing import Iterator, List, Union +from typing import Dict, Iterator, List, Union import pytest from stac_pydantic import Collection @@ -51,13 +51,13 @@ def merge_patch_item( self, collection_id: str, item_id: str, - item: Collection, + item: Dict, **kwargs, ): return { "path_collection_id": collection_id, "path_item_id": item_id, - "type": item.type, + "type": item["type"], } def json_patch_item( @@ -88,10 +88,10 @@ def update_collection(self, collection_id: str, collection: Collection, **kwargs def merge_patch_collection( self, collection_id: str, - collection: Collection, + collection: Dict, **kwargs, ): - return {"path_collection_id": collection_id, "type": collection.type} + return {"path_collection_id": collection_id, "type": collection["type"]} def json_patch_collection( self, @@ -145,7 +145,9 @@ def test_merge_patch_item(client: TestClient, item: Item) -> None: def test_json_patch_item(client: TestClient) -> None: - operations = [{"op": "add", "path": "properties.new_prop", "value": "new_prop_value"}] + operations = [ + {"op": "add", "path": "properties.new_prop", "value": "new_prop_value"} + ] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -189,7 +191,9 @@ def test_merge_patch_collection(client: TestClient, collection: Collection) -> N def test_json_patch_collection(client: TestClient) -> None: - operations = [{"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"}] + operations = [ + {"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"} + ] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -269,7 +273,9 @@ def collection() -> Collection: "description": "A test collection", "extent": { "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": {"interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]}, + "temporal": { + "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]] + }, }, "links": [], "assets": {}, diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index e9830edc..53f835f2 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -86,7 +86,7 @@ def patch_item( self, collection_id: str, item_id: str, - patch: Union[Item, List[PatchOperation]], + patch: Union[dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -111,7 +111,7 @@ def patch_item( **kwargs, ) - elif isinstance(patch, Item) and content_type in [ + elif isinstance(patch, dict) and content_type in [ "application/merge-patch+json", "application/json", ]: @@ -123,14 +123,16 @@ def patch_item( ) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_item( self, collection_id: str, item_id: str, - item: Item, + item: Dict, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -225,7 +227,7 @@ def update_collection( def patch_collection( self, collection_id: str, - patch: Union[Collection, List[PatchOperation]], + patch: Union[dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: @@ -244,20 +246,22 @@ def patch_collection( if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_collection(collection_id, patch, **kwargs) - elif isinstance(patch, Collection) and content_type in [ + elif isinstance(patch, dict) and content_type in [ "application/merge-patch+json", "application/json", ]: return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_collection( self, collection_id: str, - collection: Collection, + collection: Dict, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -355,9 +359,9 @@ async def update_item( def patch_item( self, - item_id: str, collection_id: str, - patch: Union[Item, List[PatchOperation]], + item_id: str, + patch: Union[dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -376,32 +380,34 @@ def patch_item( content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_item( - item_id, collection_id, + item_id, patch, **kwargs, ) - elif isinstance(patch, Item) and content_type in [ + elif isinstance(patch, dict) and content_type in [ "application/merge-patch+json", "application/json", ]: return self.merge_patch_item( - item_id, collection_id, + item_id, patch, **kwargs, ) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_item( self, - item_id: str, collection_id: str, - item: Item, + item_id: str, + item: Dict, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -421,8 +427,8 @@ def merge_patch_item( @abc.abstractmethod def json_patch_item( self, - item_id: str, collection_id: str, + item_id: str, operations: List[PatchOperation], **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -496,7 +502,7 @@ async def update_collection( def patch_collection( self, collection_id: str, - patch: Union[Collection, List[PatchOperation]], + patch: Union[dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: @@ -515,20 +521,22 @@ def patch_collection( if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_collection(collection_id, patch, **kwargs) - elif isinstance(patch, Collection) and content_type in [ + elif isinstance(patch, dict) and content_type in [ "application/merge-patch+json", "application/json", ]: return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError("Content-Type and body combination not implemented") + raise NotImplementedError( + "Content-Type and body combination not implemented" + ) @abc.abstractmethod def merge_patch_collection( self, collection_id: str, - collection: Collection, + collection: Dict, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. From 68a65a083c669cec50593c2df18adf133f93fa3c Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Fri, 23 Aug 2024 14:36:07 +0100 Subject: [PATCH 10/24] Ruff format fix. --- .../extensions/tests/test_transaction.py | 12 +++--------- stac_fastapi/types/stac_fastapi/types/core.py | 16 ++++------------ 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index e98aaf69..199b2924 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -145,9 +145,7 @@ def test_merge_patch_item(client: TestClient, item: Item) -> None: def test_json_patch_item(client: TestClient) -> None: - operations = [ - {"op": "add", "path": "properties.new_prop", "value": "new_prop_value"} - ] + operations = [{"op": "add", "path": "properties.new_prop", "value": "new_prop_value"}] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -191,9 +189,7 @@ def test_merge_patch_collection(client: TestClient, collection: Collection) -> N def test_json_patch_collection(client: TestClient) -> None: - operations = [ - {"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"} - ] + operations = [{"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"}] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -273,9 +269,7 @@ def collection() -> Collection: "description": "A test collection", "extent": { "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": { - "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]] - }, + "temporal": {"interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]}, }, "links": [], "assets": {}, diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 53f835f2..90002f95 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -123,9 +123,7 @@ def patch_item( ) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_item( @@ -253,9 +251,7 @@ def patch_collection( return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_collection( @@ -398,9 +394,7 @@ def patch_item( ) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_item( @@ -528,9 +522,7 @@ def patch_collection( return self.merge_patch_collection(collection_id, patch, **kwargs) else: - raise NotImplementedError( - "Content-Type and body combination not implemented" - ) + raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod def merge_patch_collection( From 88b40d4c9149338720a01d1d99f2ff61d12f8c3f Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Fri, 23 Aug 2024 14:45:14 +0100 Subject: [PATCH 11/24] Fixing broken tests. --- stac_fastapi/types/stac_fastapi/types/core.py | 9 +++++---- stac_fastapi/types/stac_fastapi/types/transaction.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 90002f95..59fdaf4e 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -86,7 +86,7 @@ def patch_item( self, collection_id: str, item_id: str, - patch: Union[dict, List[PatchOperation]], + patch: Union[Dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -225,7 +225,7 @@ def update_collection( def patch_collection( self, collection_id: str, - patch: Union[dict, List[PatchOperation]], + patch: Union[Dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: @@ -357,7 +357,7 @@ def patch_item( self, collection_id: str, item_id: str, - patch: Union[dict, List[PatchOperation]], + patch: Union[Dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: @@ -373,6 +373,7 @@ def patch_item( Returns: The patched item. """ + print(type(patch)) content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_item( @@ -496,7 +497,7 @@ async def update_collection( def patch_collection( self, collection_id: str, - patch: Union[dict, List[PatchOperation]], + patch: Union[Dict, List[PatchOperation]], request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py index 3a9bd3c8..ed2862f3 100644 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -1,6 +1,6 @@ """Transaction extension types.""" -from typing import Any, List, Literal, Union +from typing import Any, Dict, List, Literal, Union import attr from fastapi import Body @@ -63,7 +63,7 @@ class PatchItem(ItemUri): """Patch Item.""" patch: Annotated[ - Union[Item, List[PatchOperation]], + Union[Dict, List[PatchOperation]], Body(), ] = attr.ib(default=None) @@ -80,6 +80,6 @@ class PatchCollection(CollectionUri): """Patch Collection.""" patch: Annotated[ - Union[Collection, List[PatchOperation]], + Union[Dict, List[PatchOperation]], Body(), ] = attr.ib(default=None) From b7bcbd58e45a8a6564b83f5efd83ea7eadbdac08 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Fri, 23 Aug 2024 15:18:44 +0100 Subject: [PATCH 12/24] Adding missing asyncs for patchs. --- stac_fastapi/types/stac_fastapi/types/core.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 59fdaf4e..d5e10e9c 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -353,7 +353,7 @@ async def update_item( """ ... - def patch_item( + async def patch_item( self, collection_id: str, item_id: str, @@ -376,7 +376,7 @@ def patch_item( print(type(patch)) content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": - return self.json_patch_item( + return await self.json_patch_item( collection_id, item_id, patch, @@ -387,7 +387,7 @@ def patch_item( "application/merge-patch+json", "application/json", ]: - return self.merge_patch_item( + return await self.merge_patch_item( collection_id, item_id, patch, @@ -398,7 +398,7 @@ def patch_item( raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod - def merge_patch_item( + async def merge_patch_item( self, collection_id: str, item_id: str, @@ -420,7 +420,7 @@ def merge_patch_item( ... @abc.abstractmethod - def json_patch_item( + async def json_patch_item( self, collection_id: str, item_id: str, @@ -494,7 +494,7 @@ async def update_collection( """ ... - def patch_collection( + async def patch_collection( self, collection_id: str, patch: Union[Dict, List[PatchOperation]], @@ -514,19 +514,19 @@ def patch_collection( """ content_type = request.headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": - return self.json_patch_collection(collection_id, patch, **kwargs) + return await self.json_patch_collection(collection_id, patch, **kwargs) elif isinstance(patch, dict) and content_type in [ "application/merge-patch+json", "application/json", ]: - return self.merge_patch_collection(collection_id, patch, **kwargs) + return await self.merge_patch_collection(collection_id, patch, **kwargs) else: raise NotImplementedError("Content-Type and body combination not implemented") @abc.abstractmethod - def merge_patch_collection( + async def merge_patch_collection( self, collection_id: str, collection: Dict, @@ -546,7 +546,7 @@ def merge_patch_collection( ... @abc.abstractmethod - def json_patch_collection( + async def json_patch_collection( self, collection_id: str, operations: List[PatchOperation], From cfc31c6c0215270d1d552d61fc0f92190218b00b Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Fri, 23 Aug 2024 15:26:13 +0100 Subject: [PATCH 13/24] Moving request to kwargs for patch item and collection. --- stac_fastapi/types/stac_fastapi/types/core.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index d5e10e9c..11d44542 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -87,7 +87,6 @@ def patch_item( collection_id: str, item_id: str, patch: Union[Dict, List[PatchOperation]], - request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -102,7 +101,7 @@ def patch_item( Returns: The patched item. """ - content_type = request.headers.get("content-type", "application/json") + content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_item( collection_id, @@ -226,7 +225,6 @@ def patch_collection( self, collection_id: str, patch: Union[Dict, List[PatchOperation]], - request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -240,7 +238,7 @@ def patch_collection( Returns: The patched collection. """ - content_type = request.headers.get("content-type", "application/json") + content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_collection(collection_id, patch, **kwargs) @@ -358,7 +356,6 @@ async def patch_item( collection_id: str, item_id: str, patch: Union[Dict, List[PatchOperation]], - request: Request, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -374,7 +371,7 @@ async def patch_item( The patched item. """ print(type(patch)) - content_type = request.headers.get("content-type", "application/json") + content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return await self.json_patch_item( collection_id, @@ -498,7 +495,6 @@ async def patch_collection( self, collection_id: str, patch: Union[Dict, List[PatchOperation]], - request: Request, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -512,7 +508,7 @@ async def patch_collection( Returns: The patched collection. """ - content_type = request.headers.get("content-type", "application/json") + content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return await self.json_patch_collection(collection_id, patch, **kwargs) From 81dbcad3c1c7d921055d974287eabec3d87659e0 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 11:13:04 +0100 Subject: [PATCH 14/24] Switching to TypedDict. --- stac_fastapi/types/stac_fastapi/types/core.py | 48 ++++++++++++++----- .../types/stac_fastapi/types/transaction.py | 46 ++++++++++++++++-- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 11d44542..892e4d3b 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -7,6 +7,7 @@ import attr from fastapi import Request from geojson_pydantic.geometries import Geometry +from pydantic import TypeAdapter from stac_pydantic import Collection, Item, ItemCollection from stac_pydantic.api.version import STAC_API_VERSION from stac_pydantic.links import Relations @@ -20,7 +21,11 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.transaction import PatchOperation +from stac_fastapi.types.transaction import ( + PartialCollection, + PartialItem, + PatchOperation, +) __all__ = [ "NumType", @@ -86,7 +91,7 @@ def patch_item( self, collection_id: str, item_id: str, - patch: Union[Dict, List[PatchOperation]], + patch: Union[PartialItem, List[PatchOperation]], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -114,6 +119,9 @@ def patch_item( "application/merge-patch+json", "application/json", ]: + partialItemValidator = TypeAdapter(PartialItem) + + patch = partialItemValidator.validate_python(patch) return self.merge_patch_item( collection_id, item_id, @@ -129,7 +137,7 @@ def merge_patch_item( self, collection_id: str, item_id: str, - item: Dict, + item: PartialItem, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -224,7 +232,7 @@ def update_collection( def patch_collection( self, collection_id: str, - patch: Union[Dict, List[PatchOperation]], + patch: Union[PartialCollection, List[PatchOperation]], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -246,7 +254,14 @@ def patch_collection( "application/merge-patch+json", "application/json", ]: - return self.merge_patch_collection(collection_id, patch, **kwargs) + partialCollectionValidator = TypeAdapter(PartialCollection) + + patch = partialCollectionValidator.validate_python(patch) + return self.merge_patch_collection( + collection_id, + patch, + **kwargs, + ) else: raise NotImplementedError("Content-Type and body combination not implemented") @@ -255,7 +270,7 @@ def patch_collection( def merge_patch_collection( self, collection_id: str, - collection: Dict, + collection: PartialCollection, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -355,7 +370,7 @@ async def patch_item( self, collection_id: str, item_id: str, - patch: Union[Dict, List[PatchOperation]], + patch: Union[PartialItem, List[PatchOperation]], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -370,7 +385,6 @@ async def patch_item( Returns: The patched item. """ - print(type(patch)) content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return await self.json_patch_item( @@ -384,6 +398,9 @@ async def patch_item( "application/merge-patch+json", "application/json", ]: + partialItemValidator = TypeAdapter(PartialItem) + + patch = partialItemValidator.validate_python(patch) return await self.merge_patch_item( collection_id, item_id, @@ -399,7 +416,7 @@ async def merge_patch_item( self, collection_id: str, item_id: str, - item: Dict, + item: PartialItem, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -494,7 +511,7 @@ async def update_collection( async def patch_collection( self, collection_id: str, - patch: Union[Dict, List[PatchOperation]], + patch: Union[PartialCollection, List[PatchOperation]], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -510,7 +527,14 @@ async def patch_collection( """ content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": - return await self.json_patch_collection(collection_id, patch, **kwargs) + partialCollectionValidator = TypeAdapter(PartialCollection) + + patch = partialCollectionValidator.validate_python(patch) + return await self.json_patch_collection( + collection_id, + patch, + **kwargs, + ) elif isinstance(patch, dict) and content_type in [ "application/merge-patch+json", @@ -525,7 +549,7 @@ async def patch_collection( async def merge_patch_collection( self, collection_id: str, - collection: Dict, + collection: PartialCollection, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py index ed2862f3..d92893f5 100644 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -1,15 +1,55 @@ """Transaction extension types.""" -from typing import Any, Dict, List, Literal, Union +import sys +from typing import Any, Dict, List, Literal, Optional, Union import attr from fastapi import Body from pydantic import BaseModel from stac_pydantic import Collection, Item, ItemCollection +from stac_pydantic.shared import BBox from typing_extensions import Annotated from stac_fastapi.api.models import CollectionUri, ItemUri +if sys.version_info < (3, 12, 0): + from typing_extensions import TypedDict +else: + from typing import TypedDict + + +class PartialCollection(TypedDict, total=False): + """Partial STAC Collection.""" + + type: Optional[str] + stac_version: Optional[str] + stac_extensions: Optional[List[str]] + id: Optional[str] + title: Optional[str] + description: Optional[str] + links: List[Dict[str, Any]] + keywords: Optional[List[str]] + license: Optional[str] + providers: Optional[List[Dict[str, Any]]] + extent: Optional[Dict[str, Any]] + summaries: Optional[Dict[str, Any]] + assets: Optional[Dict[str, Any]] + + +class PartialItem(TypedDict, total=False): + """Partial STAC Item.""" + + type: Optional[Literal["Feature"]] + stac_version: Optional[str] + stac_extensions: Optional[List[str]] + id: Optional[str] + geometry: Optional[Dict[str, Any]] + bbox: Optional[BBox] + properties: Optional[Dict[str, Any]] + links: Optional[List[Dict[str, Any]]] + assets: Optional[Dict[str, Any]] + collection: Optional[str] + @attr.s class PostItem(CollectionUri): @@ -63,7 +103,7 @@ class PatchItem(ItemUri): """Patch Item.""" patch: Annotated[ - Union[Dict, List[PatchOperation]], + Union[PartialItem, List[PatchOperation]], Body(), ] = attr.ib(default=None) @@ -80,6 +120,6 @@ class PatchCollection(CollectionUri): """Patch Collection.""" patch: Annotated[ - Union[Dict, List[PatchOperation]], + Union[PartialCollection, List[PatchOperation]], Body(), ] = attr.ib(default=None) From bce099c65a5eb2c03cc786d2700272a394fad6db Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 14:28:20 +0100 Subject: [PATCH 15/24] Adding hearder parameter to the input models. --- stac_fastapi/types/stac_fastapi/types/core.py | 15 ++++++--------- .../types/stac_fastapi/types/transaction.py | 10 +++++++++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 892e4d3b..f5f573c4 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -21,11 +21,7 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.transaction import ( - PartialCollection, - PartialItem, - PatchOperation, -) +from stac_fastapi.types.transaction import PartialCollection, PartialItem, PatchOperation __all__ = [ "NumType", @@ -92,6 +88,7 @@ def patch_item( collection_id: str, item_id: str, patch: Union[PartialItem, List[PatchOperation]], + content_type: Optional[str], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -106,7 +103,6 @@ def patch_item( Returns: The patched item. """ - content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_item( collection_id, @@ -233,6 +229,7 @@ def patch_collection( self, collection_id: str, patch: Union[PartialCollection, List[PatchOperation]], + content_type: Optional[str], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -246,7 +243,6 @@ def patch_collection( Returns: The patched collection. """ - content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": return self.json_patch_collection(collection_id, patch, **kwargs) @@ -371,6 +367,7 @@ async def patch_item( collection_id: str, item_id: str, patch: Union[PartialItem, List[PatchOperation]], + content_type: Optional[str], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -385,7 +382,7 @@ async def patch_item( Returns: The patched item. """ - content_type = kwargs["request"].headers.get("content-type", "application/json") + print("content_type", content_type) if isinstance(patch, list) and content_type == "application/json-patch+json": return await self.json_patch_item( collection_id, @@ -512,6 +509,7 @@ async def patch_collection( self, collection_id: str, patch: Union[PartialCollection, List[PatchOperation]], + content_type: Optional[str], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -525,7 +523,6 @@ async def patch_collection( Returns: The patched collection. """ - content_type = kwargs["request"].headers.get("content-type", "application/json") if isinstance(patch, list) and content_type == "application/json-patch+json": partialCollectionValidator = TypeAdapter(PartialCollection) diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py index d92893f5..f8237605 100644 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ b/stac_fastapi/types/stac_fastapi/types/transaction.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List, Literal, Optional, Union import attr -from fastapi import Body +from fastapi import Body, Header from pydantic import BaseModel from stac_pydantic import Collection, Item, ItemCollection from stac_pydantic.shared import BBox @@ -106,6 +106,10 @@ class PatchItem(ItemUri): Union[PartialItem, List[PatchOperation]], Body(), ] = attr.ib(default=None) + content_type: Annotated[ + Optional[str], + Header(), + ] = attr.ib(default="application/json") @attr.s @@ -123,3 +127,7 @@ class PatchCollection(CollectionUri): Union[PartialCollection, List[PatchOperation]], Body(), ] = attr.ib(default=None) + content_type: Annotated[ + Optional[str], + Header(), + ] = attr.ib(default="application/json") From 336df706b21d5d60ebd146b0174e5424666151b4 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 15:01:47 +0100 Subject: [PATCH 16/24] Removing print statement. Adding default for content_type. --- stac_fastapi/types/stac_fastapi/types/core.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index f5f573c4..4b97532d 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -21,7 +21,11 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.transaction import PartialCollection, PartialItem, PatchOperation +from stac_fastapi.types.transaction import ( + PartialCollection, + PartialItem, + PatchOperation, +) __all__ = [ "NumType", @@ -88,7 +92,7 @@ def patch_item( collection_id: str, item_id: str, patch: Union[PartialItem, List[PatchOperation]], - content_type: Optional[str], + content_type: Optional[str] = "application/json", **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -229,7 +233,7 @@ def patch_collection( self, collection_id: str, patch: Union[PartialCollection, List[PatchOperation]], - content_type: Optional[str], + content_type: Optional[str] = "application/json", **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -367,7 +371,7 @@ async def patch_item( collection_id: str, item_id: str, patch: Union[PartialItem, List[PatchOperation]], - content_type: Optional[str], + content_type: Optional[str] = "application/json", **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -382,7 +386,6 @@ async def patch_item( Returns: The patched item. """ - print("content_type", content_type) if isinstance(patch, list) and content_type == "application/json-patch+json": return await self.json_patch_item( collection_id, @@ -509,7 +512,7 @@ async def patch_collection( self, collection_id: str, patch: Union[PartialCollection, List[PatchOperation]], - content_type: Optional[str], + content_type: Optional[str] = "application/json", **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. From 36b7167cabd9635c0f2d432f913b54fcfd8d0783 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 15:14:54 +0100 Subject: [PATCH 17/24] Removing basemodel from patch types. Moving transaction types back to extensions.transaction. Adding literals for content_type. Removing sys check for TypedDict import. --- .../extensions/core/transaction.py | 143 ++++++++++++++++-- stac_fastapi/types/stac_fastapi/types/stac.py | 11 +- .../types/stac_fastapi/types/transaction.py | 133 ---------------- 3 files changed, 133 insertions(+), 154 deletions(-) delete mode 100644 stac_fastapi/types/stac_fastapi/types/transaction.py diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index d5343b1c..0c18d310 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -1,25 +1,146 @@ """Transaction extension.""" -from typing import List, Optional, Type, Union +from typing import Any, Dict, List, Literal, Optional, Type, Union import attr -from fastapi import APIRouter, FastAPI -from stac_pydantic import Collection, Item -from stac_pydantic.shared import MimeTypes +from fastapi import APIRouter, Body, FastAPI, Header +from stac_pydantic import Collection, Item, ItemCollection +from stac_pydantic.shared import BBox, MimeTypes from starlette.responses import JSONResponse, Response +from typing_extensions import Annotated, TypedDict from stac_fastapi.api.models import CollectionUri, ItemUri from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransactionsClient from stac_fastapi.types.extension import ApiExtension -from stac_fastapi.types.transaction import ( - PatchCollection, - PatchItem, - PostItem, - PutCollection, - PutItem, -) + + +class PartialCollection(TypedDict, total=False): + """Partial STAC Collection.""" + + type: Optional[str] + stac_version: Optional[str] + stac_extensions: Optional[List[str]] + id: Optional[str] + title: Optional[str] + description: Optional[str] + links: List[Dict[str, Any]] + keywords: Optional[List[str]] + license: Optional[str] + providers: Optional[List[Dict[str, Any]]] + extent: Optional[Dict[str, Any]] + summaries: Optional[Dict[str, Any]] + assets: Optional[Dict[str, Any]] + + +class PartialItem(TypedDict, total=False): + """Partial STAC Item.""" + + type: Optional[Literal["Feature"]] + stac_version: Optional[str] + stac_extensions: Optional[List[str]] + id: Optional[str] + geometry: Optional[Dict[str, Any]] + bbox: Optional[BBox] + properties: Optional[Dict[str, Any]] + links: Optional[List[Dict[str, Any]]] + assets: Optional[Dict[str, Any]] + collection: Optional[str] + + +@attr.s +class PostItem(CollectionUri): + """Create Item.""" + + item: Annotated[Union[Item, ItemCollection], Body()] = attr.ib(default=None) + + +@attr.s +class PatchAddReplaceTest: + """Add, Replace or Test Operation.""" + + path: str = attr.ib() + op: Literal["add", "replace", "test"] = attr.ib() + value: Any = attr.ib() + + +@attr.s +class PatchRemove: + """Remove Operation.""" + + path: str = attr.ib() + op: Literal["remove"] = attr.ib() + + +@attr.s +class PatchMoveCopy: + """Move or Copy Operation.""" + + path: str = attr.ib() + op: Literal["move", "copy"] = attr.ib() + + def __attrs_init__(self, *args, **kwargs): + """Init function to add 'from' field.""" + super().__init__(*args, **kwargs) + self.__setattr__("from", kwargs["from"]) + + +PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove] + + +@attr.s +class PutItem(ItemUri): + """Update Item.""" + + item: Annotated[Item, Body()] = attr.ib(default=None) + + +@attr.s +class PatchItem(ItemUri): + """Patch Item.""" + + patch: Annotated[ + Union[PartialItem, List[PatchOperation]], + Body(), + ] = attr.ib(default=None) + content_type: Annotated[ + Optional[ + Literal[ + "application/json-patch+json", + "application/merge-patch+json", + "application/json", + ] + ], + Header(), + ] = attr.ib(default="application/json") + + +@attr.s +class PutCollection(CollectionUri): + """Update Collection.""" + + collection: Annotated[Collection, Body()] = attr.ib(default=None) + + +@attr.s +class PatchCollection(CollectionUri): + """Patch Collection.""" + + patch: Annotated[ + Union[PartialCollection, List[PatchOperation]], + Body(), + ] = attr.ib(default=None) + content_type: Annotated[ + Optional[ + Literal[ + "application/json-patch+json", + "application/merge-patch+json", + "application/json", + ] + ], + Header(), + ] = attr.ib(default="application/json") @attr.s diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index b9c93fd8..977ed679 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -1,18 +1,9 @@ """STAC types.""" -import sys from typing import Any, Dict, List, Literal, Optional, Union from stac_pydantic.shared import BBox - -# Avoids a Pydantic error: -# TypeError: You should use `typing_extensions.TypedDict` instead of -# `typing.TypedDict` with Python < 3.12.0. Without it, there is no way to -# differentiate required and optional fields when subclassed. -if sys.version_info < (3, 12, 0): - from typing_extensions import TypedDict -else: - from typing import TypedDict +from typing_extensions import TypedDict NumType = Union[float, int] diff --git a/stac_fastapi/types/stac_fastapi/types/transaction.py b/stac_fastapi/types/stac_fastapi/types/transaction.py deleted file mode 100644 index f8237605..00000000 --- a/stac_fastapi/types/stac_fastapi/types/transaction.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Transaction extension types.""" - -import sys -from typing import Any, Dict, List, Literal, Optional, Union - -import attr -from fastapi import Body, Header -from pydantic import BaseModel -from stac_pydantic import Collection, Item, ItemCollection -from stac_pydantic.shared import BBox -from typing_extensions import Annotated - -from stac_fastapi.api.models import CollectionUri, ItemUri - -if sys.version_info < (3, 12, 0): - from typing_extensions import TypedDict -else: - from typing import TypedDict - - -class PartialCollection(TypedDict, total=False): - """Partial STAC Collection.""" - - type: Optional[str] - stac_version: Optional[str] - stac_extensions: Optional[List[str]] - id: Optional[str] - title: Optional[str] - description: Optional[str] - links: List[Dict[str, Any]] - keywords: Optional[List[str]] - license: Optional[str] - providers: Optional[List[Dict[str, Any]]] - extent: Optional[Dict[str, Any]] - summaries: Optional[Dict[str, Any]] - assets: Optional[Dict[str, Any]] - - -class PartialItem(TypedDict, total=False): - """Partial STAC Item.""" - - type: Optional[Literal["Feature"]] - stac_version: Optional[str] - stac_extensions: Optional[List[str]] - id: Optional[str] - geometry: Optional[Dict[str, Any]] - bbox: Optional[BBox] - properties: Optional[Dict[str, Any]] - links: Optional[List[Dict[str, Any]]] - assets: Optional[Dict[str, Any]] - collection: Optional[str] - - -@attr.s -class PostItem(CollectionUri): - """Create Item.""" - - item: Annotated[Union[Item, ItemCollection], Body()] = attr.ib(default=None) - - -@attr.s -class PatchAddReplaceTest(BaseModel): - """Add, Replace or Test Operation.""" - - path: str = attr.ib() - op: Literal["add", "replace", "test"] = attr.ib() - value: Any = attr.ib() - - -@attr.s -class PatchRemove(BaseModel): - """Remove Operation.""" - - path: str = attr.ib() - op: Literal["remove"] = attr.ib() - - -@attr.s -class PatchMoveCopy(BaseModel): - """Move or Copy Operation.""" - - path: str = attr.ib() - op: Literal["move", "copy"] = attr.ib() - - def __attrs_init__(self, *args, **kwargs): - """Init function to add 'from' field.""" - super().__init__(*args, **kwargs) - self.__setattr__("from", kwargs["from"]) - - -PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove] - - -@attr.s -class PutItem(ItemUri): - """Update Item.""" - - item: Annotated[Item, Body()] = attr.ib(default=None) - - -@attr.s -class PatchItem(ItemUri): - """Patch Item.""" - - patch: Annotated[ - Union[PartialItem, List[PatchOperation]], - Body(), - ] = attr.ib(default=None) - content_type: Annotated[ - Optional[str], - Header(), - ] = attr.ib(default="application/json") - - -@attr.s -class PutCollection(CollectionUri): - """Update Collection.""" - - collection: Annotated[Collection, Body()] = attr.ib(default=None) - - -@attr.s -class PatchCollection(CollectionUri): - """Patch Collection.""" - - patch: Annotated[ - Union[PartialCollection, List[PatchOperation]], - Body(), - ] = attr.ib(default=None) - content_type: Annotated[ - Optional[str], - Header(), - ] = attr.ib(default="application/json") From 0ecf3e5641e56130c4476ffefc8ba9b39b6d0ced Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 15:20:08 +0100 Subject: [PATCH 18/24] Fixing imports. --- stac_fastapi/types/stac_fastapi/types/core.py | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 4b97532d..46d68df8 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -1,7 +1,7 @@ """Base clients.""" import abc -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union from urllib.parse import urljoin import attr @@ -17,15 +17,15 @@ 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.rfc3339 import DateTimeType -from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.transaction import ( +from stac_fastapi.types.extension import ( + ApiExtension, PartialCollection, PartialItem, PatchOperation, ) +from stac_fastapi.types.requests import get_base_url +from stac_fastapi.types.rfc3339 import DateTimeType +from stac_fastapi.types.search import BaseSearchPostRequest __all__ = [ "NumType", @@ -92,7 +92,13 @@ def patch_item( collection_id: str, item_id: str, patch: Union[PartialItem, List[PatchOperation]], - content_type: Optional[str] = "application/json", + content_type: Optional[ + Literal[ + "application/json-patch+json", + "application/merge-patch+json", + "application/json", + ] + ] = "application/json", **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -233,7 +239,13 @@ def patch_collection( self, collection_id: str, patch: Union[PartialCollection, List[PatchOperation]], - content_type: Optional[str] = "application/json", + content_type: Optional[ + Literal[ + "application/json-patch+json", + "application/merge-patch+json", + "application/json", + ] + ] = "application/json", **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -371,7 +383,13 @@ async def patch_item( collection_id: str, item_id: str, patch: Union[PartialItem, List[PatchOperation]], - content_type: Optional[str] = "application/json", + content_type: Optional[ + Literal[ + "application/json-patch+json", + "application/merge-patch+json", + "application/json", + ] + ] = "application/json", **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -512,7 +530,13 @@ async def patch_collection( self, collection_id: str, patch: Union[PartialCollection, List[PatchOperation]], - content_type: Optional[str] = "application/json", + content_type: Optional[ + Literal[ + "application/json-patch+json", + "application/merge-patch+json", + "application/json", + ] + ] = "application/json", **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. From 47a0b48dc8f48ca1a3992a0e7ca8b533789fbc64 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 15:36:32 +0100 Subject: [PATCH 19/24] Moving models to correct locations. --- .../extensions/core/transaction.py | 73 +------------------ .../extensions/tests/test_transaction.py | 14 +++- stac_fastapi/types/stac_fastapi/types/core.py | 9 +-- stac_fastapi/types/stac_fastapi/types/stac.py | 67 +++++++++++++++++ 4 files changed, 84 insertions(+), 79 deletions(-) diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py index 0c18d310..c0233d36 100644 --- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py +++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py @@ -1,52 +1,20 @@ """Transaction extension.""" -from typing import Any, Dict, List, Literal, Optional, Type, Union +from typing import List, Literal, Optional, Type, Union import attr from fastapi import APIRouter, Body, FastAPI, Header from stac_pydantic import Collection, Item, ItemCollection -from stac_pydantic.shared import BBox, MimeTypes +from stac_pydantic.shared import MimeTypes from starlette.responses import JSONResponse, Response -from typing_extensions import Annotated, TypedDict +from typing_extensions import Annotated from stac_fastapi.api.models import CollectionUri, ItemUri from stac_fastapi.api.routes import create_async_endpoint from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransactionsClient from stac_fastapi.types.extension import ApiExtension - - -class PartialCollection(TypedDict, total=False): - """Partial STAC Collection.""" - - type: Optional[str] - stac_version: Optional[str] - stac_extensions: Optional[List[str]] - id: Optional[str] - title: Optional[str] - description: Optional[str] - links: List[Dict[str, Any]] - keywords: Optional[List[str]] - license: Optional[str] - providers: Optional[List[Dict[str, Any]]] - extent: Optional[Dict[str, Any]] - summaries: Optional[Dict[str, Any]] - assets: Optional[Dict[str, Any]] - - -class PartialItem(TypedDict, total=False): - """Partial STAC Item.""" - - type: Optional[Literal["Feature"]] - stac_version: Optional[str] - stac_extensions: Optional[List[str]] - id: Optional[str] - geometry: Optional[Dict[str, Any]] - bbox: Optional[BBox] - properties: Optional[Dict[str, Any]] - links: Optional[List[Dict[str, Any]]] - assets: Optional[Dict[str, Any]] - collection: Optional[str] +from stac_fastapi.types.stac import PartialCollection, PartialItem, PatchOperation @attr.s @@ -56,39 +24,6 @@ class PostItem(CollectionUri): item: Annotated[Union[Item, ItemCollection], Body()] = attr.ib(default=None) -@attr.s -class PatchAddReplaceTest: - """Add, Replace or Test Operation.""" - - path: str = attr.ib() - op: Literal["add", "replace", "test"] = attr.ib() - value: Any = attr.ib() - - -@attr.s -class PatchRemove: - """Remove Operation.""" - - path: str = attr.ib() - op: Literal["remove"] = attr.ib() - - -@attr.s -class PatchMoveCopy: - """Move or Copy Operation.""" - - path: str = attr.ib() - op: Literal["move", "copy"] = attr.ib() - - def __attrs_init__(self, *args, **kwargs): - """Init function to add 'from' field.""" - super().__init__(*args, **kwargs) - self.__setattr__("from", kwargs["from"]) - - -PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove] - - @attr.s class PutItem(ItemUri): """Update Item.""" diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index 199b2924..db2a9f1b 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -11,7 +11,7 @@ from stac_fastapi.extensions.core import TransactionExtension from stac_fastapi.types.config import ApiSettings from stac_fastapi.types.core import BaseCoreClient, BaseTransactionsClient -from stac_fastapi.types.transaction import PatchOperation +from stac_fastapi.types.stac import PatchOperation class DummyCoreClient(BaseCoreClient): @@ -145,7 +145,9 @@ def test_merge_patch_item(client: TestClient, item: Item) -> None: def test_json_patch_item(client: TestClient) -> None: - operations = [{"op": "add", "path": "properties.new_prop", "value": "new_prop_value"}] + operations = [ + {"op": "add", "path": "properties.new_prop", "value": "new_prop_value"} + ] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -189,7 +191,9 @@ def test_merge_patch_collection(client: TestClient, collection: Collection) -> N def test_json_patch_collection(client: TestClient) -> None: - operations = [{"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"}] + operations = [ + {"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"} + ] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -269,7 +273,9 @@ def collection() -> Collection: "description": "A test collection", "extent": { "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": {"interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]}, + "temporal": { + "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]] + }, }, "links": [], "assets": {}, diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 46d68df8..4595cd37 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -17,15 +17,12 @@ 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, - PartialCollection, - PartialItem, - PatchOperation, -) +from stac_fastapi.types.extension import ApiExtension from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest +from stac_fastapi.types.stac import (PartialCollection, PartialItem, + PatchOperation) __all__ = [ "NumType", diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index 977ed679..f4df87e1 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -2,6 +2,7 @@ from typing import Any, Dict, List, Literal, Optional, Union +import attr from stac_pydantic.shared import BBox from typing_extensions import TypedDict @@ -74,3 +75,69 @@ class Collections(TypedDict, total=False): collections: List[Collection] links: List[Dict[str, Any]] + + +class PartialCollection(TypedDict, total=False): + """Partial STAC Collection.""" + + type: Optional[str] + stac_version: Optional[str] + stac_extensions: Optional[List[str]] + id: Optional[str] + title: Optional[str] + description: Optional[str] + links: List[Dict[str, Any]] + keywords: Optional[List[str]] + license: Optional[str] + providers: Optional[List[Dict[str, Any]]] + extent: Optional[Dict[str, Any]] + summaries: Optional[Dict[str, Any]] + assets: Optional[Dict[str, Any]] + + +class PartialItem(TypedDict, total=False): + """Partial STAC Item.""" + + type: Optional[Literal["Feature"]] + stac_version: Optional[str] + stac_extensions: Optional[List[str]] + id: Optional[str] + geometry: Optional[Dict[str, Any]] + bbox: Optional[BBox] + properties: Optional[Dict[str, Any]] + links: Optional[List[Dict[str, Any]]] + assets: Optional[Dict[str, Any]] + collection: Optional[str] + + +@attr.s +class PatchAddReplaceTest: + """Add, Replace or Test Operation.""" + + path: str = attr.ib() + op: Literal["add", "replace", "test"] = attr.ib() + value: Any = attr.ib() + + +@attr.s +class PatchRemove: + """Remove Operation.""" + + path: str = attr.ib() + op: Literal["remove"] = attr.ib() + + +@attr.s +class PatchMoveCopy: + """Move or Copy Operation.""" + + path: str = attr.ib() + op: Literal["move", "copy"] = attr.ib() + + def __attrs_init__(self, *args, **kwargs): + """Init function to add 'from' field.""" + super().__init__(*args, **kwargs) + self.__setattr__("from", kwargs["from"]) + + +PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove] From 13a23775773fb45c56ed27e62ef9d6720641300e Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 15:46:25 +0100 Subject: [PATCH 20/24] Switching from attrs to basemodel for patch operations. --- .../extensions/tests/test_transaction.py | 12 +++------ stac_fastapi/types/stac_fastapi/types/core.py | 3 +-- stac_fastapi/types/stac_fastapi/types/stac.py | 27 +++++++++---------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/stac_fastapi/extensions/tests/test_transaction.py b/stac_fastapi/extensions/tests/test_transaction.py index db2a9f1b..afac5735 100644 --- a/stac_fastapi/extensions/tests/test_transaction.py +++ b/stac_fastapi/extensions/tests/test_transaction.py @@ -145,9 +145,7 @@ def test_merge_patch_item(client: TestClient, item: Item) -> None: def test_json_patch_item(client: TestClient) -> None: - operations = [ - {"op": "add", "path": "properties.new_prop", "value": "new_prop_value"} - ] + operations = [{"op": "add", "path": "properties.new_prop", "value": "new_prop_value"}] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -191,9 +189,7 @@ def test_merge_patch_collection(client: TestClient, collection: Collection) -> N def test_json_patch_collection(client: TestClient) -> None: - operations = [ - {"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"} - ] + operations = [{"op": "add", "path": "summaries.new_prop", "value": "new_prop_value"}] headers = {"Content-Type": "application/json-patch+json"} response = client.patch( "/collections/a-collection/items/an-item", @@ -273,9 +269,7 @@ def collection() -> Collection: "description": "A test collection", "extent": { "spatial": {"bbox": [[-180, -90, 180, 90]]}, - "temporal": { - "interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]] - }, + "temporal": {"interval": [["2000-01-01T00:00:00Z", "2024-01-01T00:00:00Z"]]}, }, "links": [], "assets": {}, diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index 4595cd37..eb6c598c 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -21,8 +21,7 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.stac import (PartialCollection, PartialItem, - PatchOperation) +from stac_fastapi.types.stac import PartialCollection, PartialItem, PatchOperation __all__ = [ "NumType", diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index f4df87e1..9c7ec6d3 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Literal, Optional, Union -import attr +from pydantic import BaseModel from stac_pydantic.shared import BBox from typing_extensions import TypedDict @@ -110,31 +110,28 @@ class PartialItem(TypedDict, total=False): collection: Optional[str] -@attr.s -class PatchAddReplaceTest: +class PatchAddReplaceTest(BaseModel): """Add, Replace or Test Operation.""" - path: str = attr.ib() - op: Literal["add", "replace", "test"] = attr.ib() - value: Any = attr.ib() + path: str + op: Literal["add", "replace", "test"] + value: Any -@attr.s -class PatchRemove: +class PatchRemove(BaseModel): """Remove Operation.""" - path: str = attr.ib() - op: Literal["remove"] = attr.ib() + path: str + op: Literal["remove"] -@attr.s -class PatchMoveCopy: +class PatchMoveCopy(BaseModel): """Move or Copy Operation.""" - path: str = attr.ib() - op: Literal["move", "copy"] = attr.ib() + path: str + op: Literal["move", "copy"] - def __attrs_init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs): """Init function to add 'from' field.""" super().__init__(*args, **kwargs) self.__setattr__("from", kwargs["from"]) From 7e59d13cd72a7433ca95530c30ed83b0c6fcabf5 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 28 Aug 2024 15:48:58 +0100 Subject: [PATCH 21/24] Switching to stac.PartialItem etc. --- stac_fastapi/types/stac_fastapi/types/core.py | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/core.py b/stac_fastapi/types/stac_fastapi/types/core.py index eb6c598c..a83a7e12 100644 --- a/stac_fastapi/types/stac_fastapi/types/core.py +++ b/stac_fastapi/types/stac_fastapi/types/core.py @@ -21,7 +21,6 @@ from stac_fastapi.types.requests import get_base_url from stac_fastapi.types.rfc3339 import DateTimeType from stac_fastapi.types.search import BaseSearchPostRequest -from stac_fastapi.types.stac import PartialCollection, PartialItem, PatchOperation __all__ = [ "NumType", @@ -87,7 +86,7 @@ def patch_item( self, collection_id: str, item_id: str, - patch: Union[PartialItem, List[PatchOperation]], + patch: Union[stac.PartialItem, List[stac.PatchOperation]], content_type: Optional[ Literal[ "application/json-patch+json", @@ -121,7 +120,7 @@ def patch_item( "application/merge-patch+json", "application/json", ]: - partialItemValidator = TypeAdapter(PartialItem) + partialItemValidator = TypeAdapter(stac.PartialItem) patch = partialItemValidator.validate_python(patch) return self.merge_patch_item( @@ -139,7 +138,7 @@ def merge_patch_item( self, collection_id: str, item_id: str, - item: PartialItem, + item: stac.PartialItem, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -161,7 +160,7 @@ def json_patch_item( self, collection_id: str, item_id: str, - operations: List[PatchOperation], + operations: List[stac.PatchOperation], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -234,7 +233,7 @@ def update_collection( def patch_collection( self, collection_id: str, - patch: Union[PartialCollection, List[PatchOperation]], + patch: Union[stac.PartialCollection, List[stac.PatchOperation]], content_type: Optional[ Literal[ "application/json-patch+json", @@ -262,7 +261,7 @@ def patch_collection( "application/merge-patch+json", "application/json", ]: - partialCollectionValidator = TypeAdapter(PartialCollection) + partialCollectionValidator = TypeAdapter(stac.PartialCollection) patch = partialCollectionValidator.validate_python(patch) return self.merge_patch_collection( @@ -278,7 +277,7 @@ def patch_collection( def merge_patch_collection( self, collection_id: str, - collection: PartialCollection, + collection: stac.PartialCollection, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -298,7 +297,7 @@ def merge_patch_collection( def json_patch_collection( self, collection_id: str, - operations: List[PatchOperation], + operations: List[stac.PatchOperation], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -378,7 +377,7 @@ async def patch_item( self, collection_id: str, item_id: str, - patch: Union[PartialItem, List[PatchOperation]], + patch: Union[stac.PartialItem, List[stac.PatchOperation]], content_type: Optional[ Literal[ "application/json-patch+json", @@ -412,7 +411,7 @@ async def patch_item( "application/merge-patch+json", "application/json", ]: - partialItemValidator = TypeAdapter(PartialItem) + partialItemValidator = TypeAdapter(stac.PartialItem) patch = partialItemValidator.validate_python(patch) return await self.merge_patch_item( @@ -430,7 +429,7 @@ async def merge_patch_item( self, collection_id: str, item_id: str, - item: PartialItem, + item: stac.PartialItem, **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -452,7 +451,7 @@ async def json_patch_item( self, collection_id: str, item_id: str, - operations: List[PatchOperation], + operations: List[stac.PatchOperation], **kwargs, ) -> Optional[Union[stac.Item, Response]]: """Update an item from a collection. @@ -525,7 +524,7 @@ async def update_collection( async def patch_collection( self, collection_id: str, - patch: Union[PartialCollection, List[PatchOperation]], + patch: Union[stac.PartialCollection, List[stac.PatchOperation]], content_type: Optional[ Literal[ "application/json-patch+json", @@ -547,7 +546,7 @@ async def patch_collection( The patched collection. """ if isinstance(patch, list) and content_type == "application/json-patch+json": - partialCollectionValidator = TypeAdapter(PartialCollection) + partialCollectionValidator = TypeAdapter(stac.PartialCollection) patch = partialCollectionValidator.validate_python(patch) return await self.json_patch_collection( @@ -569,7 +568,7 @@ async def patch_collection( async def merge_patch_collection( self, collection_id: str, - collection: PartialCollection, + collection: stac.PartialCollection, **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. @@ -589,7 +588,7 @@ async def merge_patch_collection( async def json_patch_collection( self, collection_id: str, - operations: List[PatchOperation], + operations: List[stac.PatchOperation], **kwargs, ) -> Optional[Union[stac.Collection, Response]]: """Update a collection. From 9d011eb72c78285c858a38d960b9a8a577301b00 Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Tue, 3 Sep 2024 15:53:19 +0100 Subject: [PATCH 22/24] Updating PatchMoveCopy model. --- stac_fastapi/types/stac_fastapi/types/stac.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index 9c7ec6d3..be715a18 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Literal, Optional, Union -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field from stac_pydantic.shared import BBox from typing_extensions import TypedDict @@ -128,13 +128,15 @@ class PatchRemove(BaseModel): class PatchMoveCopy(BaseModel): """Move or Copy Operation.""" + model_config = ConfigDict(populate_by_name=True) + path: str op: Literal["move", "copy"] + from_: str = Field(alias="from") - def __init__(self, *args, **kwargs): - """Init function to add 'from' field.""" - super().__init__(*args, **kwargs) - self.__setattr__("from", kwargs["from"]) + def model_dump(self, by_alias=True, **kwargs) -> dict[str, Any]: + """Override by_alias default to True""" + return super().model_dump(by_alias=by_alias, **kwargs) PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove] From e325cb2315f6e7fc39cc39227891c3daa80170bc Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 4 Sep 2024 15:44:05 +0100 Subject: [PATCH 23/24] Updating type for 3.8. --- stac_fastapi/types/stac_fastapi/types/stac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index be715a18..559d178d 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -134,7 +134,7 @@ class PatchMoveCopy(BaseModel): op: Literal["move", "copy"] from_: str = Field(alias="from") - def model_dump(self, by_alias=True, **kwargs) -> dict[str, Any]: + def model_dump(self, by_alias=True, **kwargs) -> Dict[str, Any]: """Override by_alias default to True""" return super().model_dump(by_alias=by_alias, **kwargs) From fefd493eebda22ed173cd76932f029cf4f428bfc Mon Sep 17 00:00:00 2001 From: rhysrevans3 Date: Wed, 18 Sep 2024 14:49:49 +0100 Subject: [PATCH 24/24] Switching to StacBaseModels for patch operations. --- stac_fastapi/types/stac_fastapi/types/stac.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/stac_fastapi/types/stac_fastapi/types/stac.py b/stac_fastapi/types/stac_fastapi/types/stac.py index 559d178d..10021591 100644 --- a/stac_fastapi/types/stac_fastapi/types/stac.py +++ b/stac_fastapi/types/stac_fastapi/types/stac.py @@ -2,8 +2,8 @@ from typing import Any, Dict, List, Literal, Optional, Union -from pydantic import BaseModel, ConfigDict, Field -from stac_pydantic.shared import BBox +from pydantic import ConfigDict, Field +from stac_pydantic.shared import BBox, StacBaseModel from typing_extensions import TypedDict NumType = Union[float, int] @@ -110,7 +110,7 @@ class PartialItem(TypedDict, total=False): collection: Optional[str] -class PatchAddReplaceTest(BaseModel): +class PatchAddReplaceTest(StacBaseModel): """Add, Replace or Test Operation.""" path: str @@ -118,14 +118,14 @@ class PatchAddReplaceTest(BaseModel): value: Any -class PatchRemove(BaseModel): +class PatchRemove(StacBaseModel): """Remove Operation.""" path: str op: Literal["remove"] -class PatchMoveCopy(BaseModel): +class PatchMoveCopy(StacBaseModel): """Move or Copy Operation.""" model_config = ConfigDict(populate_by_name=True) @@ -134,9 +134,5 @@ class PatchMoveCopy(BaseModel): op: Literal["move", "copy"] from_: str = Field(alias="from") - def model_dump(self, by_alias=True, **kwargs) -> Dict[str, Any]: - """Override by_alias default to True""" - return super().model_dump(by_alias=by_alias, **kwargs) - PatchOperation = Union[PatchAddReplaceTest, PatchMoveCopy, PatchRemove]