Skip to content

Commit 423b587

Browse files
Patch endpoints (#744)
* Adding patch endpoints. * Adding annotated from main. * Fixing and adding tests. * Updating changelog. * Fixing ruff errors. * Ruff format. * Switching to List for python 3.8. * Updating docs make file. * Switching from Item/Collection to Dict to allow partial updates. * Ruff format fix. * Fixing broken tests. * Adding missing asyncs for patchs. * Moving request to kwargs for patch item and collection. * Switching to TypedDict. * Adding hearder parameter to the input models. * Removing print statement. Adding default for content_type. * Removing basemodel from patch types. Moving transaction types back to extensions.transaction. Adding literals for content_type. Removing sys check for TypedDict import. * Fixing imports. * Moving models to correct locations. * Switching from attrs to basemodel for patch operations. * Switching to stac.PartialItem etc. * Updating PatchMoveCopy model. * Updating type for 3.8. * Switching to StacBaseModels for patch operations. * pre-commits. * Add json dump of operation value. * remove computed field decorator. * Adding default "not implemented" for JSON Patch. * pre-commit. * Add default raise not implement to collection JSON Patch. * removing json merge and patch. * content_type None default. * Fixing test. * back to PartialItem & PatchOperation * using openapi_extra for Content-Type. * PartialCollection not PartialItem. * Adding merge to operations. * Adding example code for patch. * Adding default None to partials. * patch not update for backend. * patch_collection not patch_item. * Add tests for merge to operations. * Switch to model_json_schema for patch schema. * update from main * update changelog * Removing numberMatched & numberReturned. * fix openapi schemas --------- Co-authored-by: vincentsarago <[email protected]>
1 parent 5333806 commit 423b587

File tree

6 files changed

+549
-17
lines changed

6 files changed

+549
-17
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Changed
66

7+
* 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) in the `TransactionExtension`
78
- remove support of `cql-json` in Filter extension ([#840](https://github.com/stac-utils/stac-fastapi/pull/840))
89

910
### Fixed
@@ -175,7 +176,6 @@
175176
## [3.0.0] - 2024-07-29
176177

177178
Full changelog: https://stac-utils.github.io/stac-fastapi/migrations/v3.0.0/#changelog
178-
179179
**Changes since 3.0.0b3:**
180180

181181
### Changed

stac_fastapi/api/tests/test_api.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33

44
from stac_fastapi.api.app import StacApi
55
from stac_fastapi.api.models import ItemCollectionUri, create_request_model
6-
from stac_fastapi.extensions.core import (
7-
TokenPaginationExtension,
8-
TransactionExtension,
9-
)
6+
from stac_fastapi.extensions.core import TokenPaginationExtension, TransactionExtension
107
from stac_fastapi.types import config, core
118

129

@@ -430,6 +427,9 @@ def create_item(self, *args, **kwargs):
430427
def update_item(self, *args, **kwargs):
431428
return "dummy response"
432429

430+
def patch_item(self, *args, **kwargs):
431+
return "dummy response"
432+
433433
def delete_item(self, *args, **kwargs):
434434
return "dummy response"
435435

@@ -439,6 +439,9 @@ def create_collection(self, *args, **kwargs):
439439
def update_collection(self, *args, **kwargs):
440440
return "dummy response"
441441

442+
def patch_collection(self, *args, **kwargs):
443+
return "dummy response"
444+
442445
def delete_collection(self, *args, **kwargs):
443446
return "dummy response"
444447

stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py

Lines changed: 184 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import attr
77
from fastapi import APIRouter, Body, FastAPI
8+
from pydantic import TypeAdapter
89
from stac_pydantic import Collection, Item, ItemCollection
910
from stac_pydantic.shared import MimeTypes
1011
from starlette.responses import Response
@@ -15,6 +16,7 @@
1516
from stac_fastapi.types.config import ApiSettings
1617
from stac_fastapi.types.core import AsyncBaseTransactionsClient, BaseTransactionsClient
1718
from stac_fastapi.types.extension import ApiExtension
19+
from stac_fastapi.types.stac import PartialCollection, PartialItem, PatchOperation
1820

1921

2022
class TransactionConformanceClasses(str, Enum):
@@ -42,13 +44,112 @@ class PutItem(ItemUri):
4244
item: Annotated[Item, Body()] = attr.ib(default=None)
4345

4446

47+
@attr.s
48+
class PatchItem(ItemUri):
49+
"""Patch Item."""
50+
51+
patch: Annotated[
52+
Union[PartialItem, List[PatchOperation]],
53+
Body(),
54+
] = attr.ib(default=None)
55+
56+
4557
@attr.s
4658
class PutCollection(CollectionUri):
4759
"""Update Collection."""
4860

4961
collection: Annotated[Collection, Body()] = attr.ib(default=None)
5062

5163

64+
@attr.s
65+
class PatchCollection(CollectionUri):
66+
"""Patch Collection."""
67+
68+
patch: Annotated[
69+
Union[PartialCollection, List[PatchOperation]],
70+
Body(),
71+
] = attr.ib(default=None)
72+
73+
74+
_patch_item_schema = TypeAdapter(List[PatchOperation]).json_schema() | {
75+
"examples": [
76+
[
77+
{
78+
"op": "add",
79+
"path": "/properties/foo",
80+
"value": "bar",
81+
},
82+
{
83+
"op": "replace",
84+
"path": "/properties/foo",
85+
"value": "bar",
86+
},
87+
{
88+
"op": "test",
89+
"path": "/properties/foo",
90+
"value": "bar",
91+
},
92+
{
93+
"op": "copy",
94+
"path": "/properties/foo",
95+
"from": "/properties/bar",
96+
},
97+
{
98+
"op": "move",
99+
"path": "/properties/foo",
100+
"from": "/properties/bar",
101+
},
102+
{
103+
"op": "remove",
104+
"path": "/properties/foo",
105+
},
106+
]
107+
]
108+
}
109+
# ref: https://github.com/pydantic/pydantic/issues/889
110+
_patch_item_schema["items"]["anyOf"] = list(_patch_item_schema["$defs"].values())
111+
112+
_patch_collection_schema = TypeAdapter(List[PatchOperation]).json_schema() | {
113+
"examples": [
114+
[
115+
{
116+
"op": "add",
117+
"path": "/summeries/foo",
118+
"value": "bar",
119+
},
120+
{
121+
"op": "replace",
122+
"path": "/summeries/foo",
123+
"value": "bar",
124+
},
125+
{
126+
"op": "test",
127+
"path": "/summeries/foo",
128+
"value": "bar",
129+
},
130+
{
131+
"op": "copy",
132+
"path": "/summeries/foo",
133+
"from": "/summeries/bar",
134+
},
135+
{
136+
"op": "move",
137+
"path": "/summeries/foo",
138+
"from": "/summeries/bar",
139+
},
140+
{
141+
"op": "remove",
142+
"path": "/summeries/foo",
143+
},
144+
]
145+
]
146+
}
147+
# ref: https://github.com/pydantic/pydantic/issues/889
148+
_patch_collection_schema["items"]["anyOf"] = list(
149+
_patch_collection_schema["$defs"].values()
150+
)
151+
152+
52153
@attr.s
53154
class TransactionExtension(ApiExtension):
54155
"""Transaction Extension.
@@ -126,6 +227,47 @@ def register_update_item(self):
126227
endpoint=create_async_endpoint(self.client.update_item, PutItem),
127228
)
128229

230+
def register_patch_item(self):
231+
"""Register patch item endpoint (PATCH
232+
/collections/{collection_id}/items/{item_id})."""
233+
self.router.add_api_route(
234+
name="Patch Item",
235+
path="/collections/{collection_id}/items/{item_id}",
236+
response_model=Item if self.settings.enable_response_models else None,
237+
responses={
238+
200: {
239+
"content": {
240+
MimeTypes.geojson.value: {},
241+
},
242+
"model": Item,
243+
}
244+
},
245+
openapi_extra={
246+
"requestBody": {
247+
"content": {
248+
"application/json-patch+json": {
249+
"schema": _patch_item_schema,
250+
},
251+
"application/merge-patch+json": {
252+
"schema": PartialItem.model_json_schema(),
253+
},
254+
"application/json": {
255+
"schema": PartialItem.model_json_schema(),
256+
},
257+
},
258+
"required": True,
259+
},
260+
},
261+
response_class=self.response_class,
262+
response_model_exclude_unset=True,
263+
response_model_exclude_none=True,
264+
methods=["PATCH"],
265+
endpoint=create_async_endpoint(
266+
self.client.patch_item,
267+
PatchItem,
268+
),
269+
)
270+
129271
def register_delete_item(self):
130272
"""Register delete item endpoint (DELETE
131273
/collections/{collection_id}/items/{item_id})."""
@@ -148,11 +290,6 @@ def register_delete_item(self):
148290
endpoint=create_async_endpoint(self.client.delete_item, ItemUri),
149291
)
150292

151-
def register_patch_item(self):
152-
"""Register patch item endpoint (PATCH
153-
/collections/{collection_id}/items/{item_id})."""
154-
raise NotImplementedError
155-
156293
def register_create_collection(self):
157294
"""Register create collection endpoint (POST /collections)."""
158295
self.router.add_api_route(
@@ -196,6 +333,46 @@ def register_update_collection(self):
196333
endpoint=create_async_endpoint(self.client.update_collection, PutCollection),
197334
)
198335

336+
def register_patch_collection(self):
337+
"""Register patch collection endpoint (PATCH /collections/{collection_id})."""
338+
self.router.add_api_route(
339+
name="Patch Collection",
340+
path="/collections/{collection_id}",
341+
response_model=Collection if self.settings.enable_response_models else None,
342+
responses={
343+
200: {
344+
"content": {
345+
MimeTypes.geojson.value: {},
346+
},
347+
"model": Collection,
348+
}
349+
},
350+
openapi_extra={
351+
"requestBody": {
352+
"content": {
353+
"application/json-patch+json": {
354+
"schema": _patch_collection_schema,
355+
},
356+
"application/merge-patch+json": {
357+
"schema": PartialCollection.model_json_schema(),
358+
},
359+
"application/json": {
360+
"schema": PartialCollection.model_json_schema(),
361+
},
362+
},
363+
"required": True,
364+
},
365+
},
366+
response_class=self.response_class,
367+
response_model_exclude_unset=True,
368+
response_model_exclude_none=True,
369+
methods=["PATCH"],
370+
endpoint=create_async_endpoint(
371+
self.client.patch_collection,
372+
PatchCollection,
373+
),
374+
)
375+
199376
def register_delete_collection(self):
200377
"""Register delete collection endpoint (DELETE /collections/{collection_id})."""
201378
self.router.add_api_route(
@@ -217,10 +394,6 @@ def register_delete_collection(self):
217394
endpoint=create_async_endpoint(self.client.delete_collection, CollectionUri),
218395
)
219396

220-
def register_patch_collection(self):
221-
"""Register patch collection endpoint (PATCH /collections/{collection_id})."""
222-
raise NotImplementedError
223-
224397
def register(self, app: FastAPI) -> None:
225398
"""Register the extension with a FastAPI application.
226399
@@ -233,8 +406,10 @@ def register(self, app: FastAPI) -> None:
233406
self.router.prefix = app.state.router_prefix
234407
self.register_create_item()
235408
self.register_update_item()
409+
self.register_patch_item()
236410
self.register_delete_item()
237411
self.register_create_collection()
238412
self.register_update_collection()
413+
self.register_patch_collection()
239414
self.register_delete_collection()
240415
app.include_router(self.router, tags=["Transaction Extension"])

0 commit comments

Comments
 (0)