Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Patch endpoints #291

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dockerfiles/Dockerfile.dev.es
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ FROM python:3.10-slim
# update apt pkgs, and install build-essential for ciso8601
RUN apt-get update && \
apt-get -y upgrade && \
apt-get install -y build-essential git && \
apt-get -y install build-essential git && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

Expand Down
3 changes: 2 additions & 1 deletion dockerfiles/Dockerfile.dev.os
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ FROM python:3.10-slim
# update apt pkgs, and install build-essential for ciso8601
RUN apt-get update && \
apt-get -y upgrade && \
apt-get install -y build-essential && \
apt-get -y install build-essential && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

RUN apt-get -y install git
# update certs used by Requests
ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

Expand Down
6 changes: 3 additions & 3 deletions stac_fastapi/core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
"attrs>=23.2.0",
"pydantic[dotenv]",
"stac_pydantic>=3",
"stac-fastapi.types==3.0.0",
"stac-fastapi.api==3.0.0",
"stac-fastapi.extensions==3.0.0",
"stac-fastapi.types@git+https://github.com/stac-utils/stac-fastapi.git@refs/pull/744/head#subdirectory=stac_fastapi/types",
"stac-fastapi.api@git+https://github.com/stac-utils/stac-fastapi.git@refs/pull/744/head#subdirectory=stac_fastapi/api",
"stac-fastapi.extensions@git+https://github.com/stac-utils/stac-fastapi.git@refs/pull/744/head#subdirectory=stac_fastapi/extensions",
"orjson",
"overrides",
"geojson-pydantic",
Expand Down
48 changes: 47 additions & 1 deletion stac_fastapi/core/stac_fastapi/core/base_database_logic.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Base database logic."""

import abc
from typing import Any, Dict, Iterable, Optional
from typing import Any, Dict, Iterable, List, Optional


class BaseDatabaseLogic(abc.ABC):
Expand Down Expand Up @@ -29,6 +29,30 @@ async def create_item(self, item: Dict, refresh: bool = False) -> None:
"""Create an item in the database."""
pass

@abc.abstractmethod
async def merge_patch_item(
self,
collection_id: str,
item_id: str,
item: Dict,
base_url: str,
refresh: bool = True,
) -> Dict:
"""Patch a item in the database follows RF7396."""
pass

@abc.abstractmethod
async def json_patch_item(
self,
collection_id: str,
item_id: str,
operations: List,
base_url: str,
refresh: bool = True,
) -> Dict:
"""Patch a item in the database follows RF6902."""
pass

@abc.abstractmethod
async def delete_item(
self, item_id: str, collection_id: str, refresh: bool = False
Expand All @@ -41,6 +65,28 @@ async def create_collection(self, collection: Dict, refresh: bool = False) -> No
"""Create a collection in the database."""
pass

@abc.abstractmethod
async def merge_patch_collection(
self,
collection_id: str,
collection: Dict,
base_url: str,
refresh: bool = True,
) -> Dict:
"""Patch a collection in the database follows RF7396."""
pass

@abc.abstractmethod
async def json_patch_collection(
self,
collection_id: str,
operations: List,
base_url: str,
refresh: bool = True,
) -> Dict:
"""Patch a collection in the database follows RF6902."""
pass

@abc.abstractmethod
async def find_collection(self, collection_id: str) -> Dict:
"""Find a collection in the database."""
Expand Down
106 changes: 106 additions & 0 deletions stac_fastapi/core/stac_fastapi/core/core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Core client."""

import logging
from datetime import datetime as datetime_type
from datetime import timezone
Expand Down Expand Up @@ -721,6 +722,58 @@ async def update_item(

return ItemSerializer.db_to_stac(item, base_url)

@overrides
async def merge_patch_item(
self, collection_id: str, item_id: str, item: stac_types.PartialItem, **kwargs
) -> Optional[stac_types.Item]:
"""Patch an item in the collection following RF7396..

Args:
collection_id (str): The ID of the collection the item belongs to.
item_id (str): The ID of the item to be updated.
item (stac_types.PartialItem): The partial item data.
kwargs: Other optional arguments, including the request object.

Returns:
stac_types.Item: The patched item object.

"""
item = await self.database.merge_patch_item(
collection_id=collection_id,
item_id=item_id,
item=item,
base_url=str(kwargs["request"].base_url),
)
return ItemSerializer.db_to_stac(item, base_url=str(kwargs["request"].base_url))

@overrides
async def json_patch_item(
self,
collection_id: str,
item_id: str,
operations: List[stac_types.PatchOperation],
**kwargs,
) -> Optional[stac_types.Item]:
"""Patch an item in the collection following RF6902.

Args:
collection_id (str): The ID of the collection the item belongs to.
item_id (str): The ID of the item to be updated.
operations (List): List of operations to run on item.
kwargs: Other optional arguments, including the request object.

Returns:
stac_types.Item: The patched item object.

"""
item = await self.database.json_patch_item(
collection_id=collection_id,
item_id=item_id,
base_url=str(kwargs["request"].base_url),
operations=operations,
)
return ItemSerializer.db_to_stac(item, base_url=str(kwargs["request"].base_url))

@overrides
async def delete_item(
self, item_id: str, collection_id: str, **kwargs
Expand Down Expand Up @@ -801,6 +854,59 @@ async def update_collection(
extensions=[type(ext).__name__ for ext in self.database.extensions],
)

@overrides
async def merge_patch_collection(
self, collection_id: str, collection: stac_types.PartialCollection, **kwargs
) -> Optional[stac_types.Collection]:
"""Patch a collection following RF7396..

Args:
collection_id (str): The ID of the collection to patch.
collection (stac_types.Collection): The partial collection data.
kwargs: Other optional arguments, including the request object.

Returns:
stac_types.Collection: The patched collection object.

"""
collection = await self.database.merge_patch_collection(
collection_id=collection_id,
base_url=str(kwargs["request"].base_url),
collection=collection,
)

return CollectionSerializer.db_to_stac(
collection,
kwargs["request"],
extensions=[type(ext).__name__ for ext in self.database.extensions],
)

@overrides
async def json_patch_collection(
self, collection_id: str, operations: List[stac_types.PatchOperation], **kwargs
) -> Optional[stac_types.Collection]:
"""Patch a collection following RF6902.

Args:
collection_id (str): The ID of the collection to patch.
operations (List): List of operations to run on collection.
kwargs: Other optional arguments, including the request object.

Returns:
stac_types.Collection: The patched collection object.

"""
collection = await self.database.json_patch_collection(
collection_id=collection_id,
operations=operations,
base_url=str(kwargs["request"].base_url),
)
return CollectionSerializer.db_to_stac(
collection,
kwargs["request"],
extensions=[type(ext).__name__ for ext in self.database.extensions],
)

@overrides
async def delete_collection(
self, collection_id: str, **kwargs
Expand Down
66 changes: 65 additions & 1 deletion stac_fastapi/core/stac_fastapi/core/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
This module contains functions for transforming geospatial coordinates,
such as converting bounding boxes to polygon representations.
"""

import json
from typing import Any, Dict, List, Optional, Set, Union

from stac_fastapi.types.stac import Item
from stac_fastapi.types.stac import Item, PatchAddReplaceTest, PatchRemove

MAX_LIMIT = 10000

Expand Down Expand Up @@ -133,3 +135,65 @@ def dict_deep_update(merge_to: Dict[str, Any], merge_from: Dict[str, Any]) -> No
dict_deep_update(merge_to[k], merge_from[k])
else:
merge_to[k] = v


def merge_to_operations(data: Dict) -> List:
"""Convert merge operation to list of RF6902 operations.

Args:
data: dictionary to convert.

Returns:
List: list of RF6902 operations.
"""
operations = []

for key, value in data.copy().items():

if value is None:
operations.append(PatchRemove(op="remove", path=key))

elif isinstance(value, dict):
nested_operations = merge_to_operations(value)

for nested_operation in nested_operations:
nested_operation.path = f"{key}.{nested_operation.path}"
operations.append(nested_operation)

else:
operations.append(PatchAddReplaceTest(op="add", path=key, value=value))

return operations


def operations_to_script(operations: List) -> Dict:
"""Convert list of operation to painless script.

Args:
operations: List of RF6902 operations.

Returns:
Dict: elasticsearch update script.
"""
source = ""
for operation in operations:
nest, partition, key = operation.path.rpartition(".")
if nest:
source += f"if (!ctx._source.containsKey('{nest}')){{Debug.explain('{nest} does not exist');}}"

if operation.op != "add":
source += f"if (!ctx._source.{nest + partition}containsKey('{key}')){{Debug.explain('{operation.path} does not exist');}}"

if operation.op in ["copy", "move"]:
source += f"ctx._source.{operation.path} = ctx._source.{getattr(operation, 'from')};"

if operation.op in ["remove", "move"]:
source += f"ctx._source.{nest + partition}remove('{key}');"

if operation.op in ["add", "replace"]:
source += f"ctx._source.{operation.path} = {json.dumps(operation.value)};"

return {
"source": source,
"lang": "painless",
}
Loading
Loading