Skip to content

Commit 4f2b84f

Browse files
committed
Simplifying patch.
1 parent 5976d3c commit 4f2b84f

File tree

5 files changed

+102
-101
lines changed

5 files changed

+102
-101
lines changed

stac_fastapi/core/stac_fastapi/core/base_database_logic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ async def json_patch_item(
4848
item_id: str,
4949
operations: List,
5050
base_url: str,
51+
create_nest: bool = False,
5152
refresh: bool = True,
5253
) -> Dict:
5354
"""Patch a item in the database follows RF6902."""
@@ -94,6 +95,7 @@ async def json_patch_collection(
9495
collection_id: str,
9596
operations: List,
9697
base_url: str,
98+
create_nest: bool = False,
9799
refresh: bool = True,
98100
) -> Dict:
99101
"""Patch a collection in the database follows RF6902."""

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ async def merge_patch_item(
886886
item_id=item_id,
887887
operations=operations,
888888
base_url=base_url,
889+
create_nest=True,
889890
refresh=refresh,
890891
)
891892

@@ -895,6 +896,7 @@ async def json_patch_item(
895896
item_id: str,
896897
operations: List[PatchOperation],
897898
base_url: str,
899+
create_nest: bool = False,
898900
refresh: bool = True,
899901
) -> Item:
900902
"""Database logic for json patching an item following RF6902.
@@ -929,7 +931,7 @@ async def json_patch_item(
929931
else:
930932
script_operations.append(operation)
931933

932-
script = operations_to_script(script_operations)
934+
script = operations_to_script(script_operations, create_nest=create_nest)
933935

934936
try:
935937
search_response = await self.client.search(
@@ -1265,6 +1267,7 @@ async def merge_patch_collection(
12651267
collection_id=collection_id,
12661268
operations=operations,
12671269
base_url=base_url,
1270+
create_nest=True,
12681271
refresh=refresh,
12691272
)
12701273

@@ -1273,6 +1276,7 @@ async def json_patch_collection(
12731276
collection_id: str,
12741277
operations: List[PatchOperation],
12751278
base_url: str,
1279+
create_nest: bool = False,
12761280
refresh: bool = True,
12771281
) -> Collection:
12781282
"""Database logic for json patching a collection following RF6902.
@@ -1300,7 +1304,7 @@ async def json_patch_collection(
13001304
else:
13011305
script_operations.append(operation)
13021306

1303-
script = operations_to_script(script_operations)
1307+
script = operations_to_script(script_operations, create_nest=create_nest)
13041308

13051309
try:
13061310
await self.client.update(

stac_fastapi/opensearch/stac_fastapi/opensearch/database_logic.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,7 @@ async def merge_patch_item(
869869
item_id=item_id,
870870
operations=operations,
871871
base_url=base_url,
872+
create_nest=True,
872873
refresh=refresh,
873874
)
874875

@@ -878,6 +879,7 @@ async def json_patch_item(
878879
item_id: str,
879880
operations: List[PatchOperation],
880881
base_url: str,
882+
create_nest: bool = False,
881883
refresh: bool = True,
882884
) -> Item:
883885
"""Database logic for json patching an item following RF6902.
@@ -912,7 +914,7 @@ async def json_patch_item(
912914
else:
913915
script_operations.append(operation)
914916

915-
script = operations_to_script(script_operations)
917+
script = operations_to_script(script_operations, create_nest=create_nest)
916918

917919
try:
918920
search_response = await self.client.search(
@@ -1220,6 +1222,7 @@ async def merge_patch_collection(
12201222
collection_id=collection_id,
12211223
operations=operations,
12221224
base_url=base_url,
1225+
create_nest=True,
12231226
refresh=refresh,
12241227
)
12251228

@@ -1228,6 +1231,7 @@ async def json_patch_collection(
12281231
collection_id: str,
12291232
operations: List[PatchOperation],
12301233
base_url: str,
1234+
create_nest: bool = False,
12311235
refresh: bool = True,
12321236
) -> Collection:
12331237
"""Database logic for json patching a collection following RF6902.
@@ -1255,7 +1259,7 @@ async def json_patch_collection(
12551259
else:
12561260
script_operations.append(operation)
12571261

1258-
script = operations_to_script(script_operations)
1262+
script = operations_to_script(script_operations, create_nest=create_nest)
12591263

12601264
try:
12611265
await self.client.update(

stac_fastapi/sfeos_helpers/stac_fastapi/sfeos_helpers/database/utils.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def check_commands(
9090
op: str,
9191
path: ElasticPath,
9292
from_path: bool = False,
93+
create_nest: bool = False,
9394
) -> None:
9495
"""Add Elasticsearch checks to operation.
9596
@@ -101,24 +102,43 @@ def check_commands(
101102
102103
"""
103104
if path.nest:
104-
commands.add(
105-
f"if (!ctx._source.containsKey('{path.nest}'))"
106-
f"{{Debug.explain('{path.nest} does not exist');}}"
107-
)
105+
part_nest = ""
106+
107+
for index, path_part in enumerate(path.parts):
108+
109+
# Create nested dictionaries if not present for merge operations
110+
if create_nest and not from_path:
111+
value = "[:]"
112+
for sub_part in reversed(path.parts[index + 1 :]):
113+
value = f"['{sub_part}': {value}]"
114+
115+
commands.add(
116+
f"if (!ctx._source{part_nest}.containsKey('{path_part}'))"
117+
f"{{ctx._source{part_nest}['{path_part}'] = {value};}}"
118+
f"{'' if index == len(path.parts) - 1 else' else '}"
119+
)
108120

109-
if path.index or op in ["remove", "replace", "test"] or from_path:
121+
else:
122+
commands.add(
123+
f"if (!ctx._source{part_nest}.containsKey('{path_part}'))"
124+
f"{{Debug.explain('{path_part} in {path.nest} does not exist');}}"
125+
)
126+
127+
part_nest += f"['{path_part}']"
128+
129+
if path.index or from_path or op in ["remove", "replace", "test"]:
110130
commands.add(
111131
f"if (!ctx._source{path.es_nest}.containsKey('{path.key}'))"
112-
f"{{Debug.explain('{path.key} does not exist in {path.nest}');}}"
132+
f"{{Debug.explain('{path.key} does not exist in {path.nest}');}}"
113133
)
114134

115135
if from_path and path.index is not None:
116136
commands.add(
117137
f"if ((ctx._source{path.es_location} instanceof ArrayList"
118-
f" && ctx._source{path.es_location}.size() < {path.index})"
138+
f" && ctx._source{path.es_location}.size() < {abs(path.index)})"
119139
f" || (!(ctx._source{path.es_location} instanceof ArrayList)"
120140
f" && !ctx._source{path.es_location}.containsKey('{path.index}')))"
121-
f"{{Debug.explain('{path.path} does not exist');}}"
141+
f"{{Debug.explain('{path.es_location} does not exist');}}"
122142
)
123143

124144

@@ -132,7 +152,7 @@ def remove_commands(commands: ESCommandSet, path: ElasticPath) -> None:
132152
"""
133153
if path.index is not None:
134154
commands.add(
135-
f"def {path.variable_name} = ctx._source{path.es_location}.remove({path.index});"
155+
f"def {path.variable_name} = ctx._source{path.es_location}.remove({path.es_index});"
136156
)
137157

138158
else:
@@ -160,7 +180,7 @@ def add_commands(
160180
value = (
161181
from_path.variable_name
162182
if operation.op == "move"
163-
else f"ctx._source.{from_path.es_path}"
183+
else f"ctx._source{from_path.es_location}"
164184
)
165185
else:
166186
value = f"params.{path.param_key}"
@@ -169,12 +189,12 @@ def add_commands(
169189
if path.index is not None:
170190
commands.add(
171191
f"if (ctx._source{path.es_location} instanceof ArrayList)"
172-
f"{{ctx._source{path.es_location}.{'add' if operation.op in ['add', 'move'] else 'set'}({path.index}, {value})}}"
173-
f"else{{ctx._source.{path.es_path} = {value}}}"
192+
f"{{ctx._source{path.es_location}.{'add' if operation.op in ['add', 'move'] else 'set'}({path.es_index}, {value})}}"
193+
f"else{{ctx._source{path.es_location}['{path.index}'] = {value}}}"
174194
)
175195

176196
else:
177-
commands.add(f"ctx._source.{path.es_path} = {value};")
197+
commands.add(f"ctx._source{path.es_location} = {value};")
178198

179199

180200
def test_commands(
@@ -191,13 +211,13 @@ def test_commands(
191211
params[path.param_key] = operation.value
192212

193213
commands.add(
194-
f"if (ctx._source.{path.es_path} != {value})"
195-
f"{{Debug.explain('Test failed `{path.path}` | "
196-
f"{operation.json_value} != ' + ctx._source.{path.es_path});}}"
214+
f"if (ctx._source{path.es_location} != {value})"
215+
f"{{Debug.explain('Test failed `{path.location}` | "
216+
f"{operation.json_value} != ' + ctx._source{path.es_location});}}"
197217
)
198218

199219

200-
def operations_to_script(operations: List) -> Dict:
220+
def operations_to_script(operations: List, create_nest: bool = False) -> Dict:
201221
"""Convert list of operation to painless script.
202222
203223
Args:
@@ -215,10 +235,16 @@ def operations_to_script(operations: List) -> Dict:
215235
ElasticPath(path=operation.from_) if hasattr(operation, "from_") else None
216236
)
217237

218-
check_commands(commands=commands, op=operation.op, path=path)
238+
check_commands(
239+
commands=commands, op=operation.op, path=path, create_nest=create_nest
240+
)
219241
if from_path is not None:
220242
check_commands(
221-
commands=commands, op=operation.op, path=from_path, from_path=True
243+
commands=commands,
244+
op=operation.op,
245+
path=from_path,
246+
from_path=True,
247+
create_nest=create_nest,
222248
)
223249

224250
if operation.op in ["remove", "move"]:
Lines changed: 43 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""patch helpers."""
22

33
import re
4-
from typing import Any, Dict, Optional, Union
4+
from typing import Any, Dict, Optional
55

6-
from pydantic import BaseModel, computed_field, model_validator
6+
from pydantic import BaseModel, model_validator
77

88
regex = re.compile(r"([^.' ]*:[^.'[ ]*)\.?")
99
replacements = str.maketrans({"/": "", ".": "", ":": "", "[": "", "]": ""})
@@ -71,16 +71,25 @@ class ElasticPath(BaseModel):
7171
7272
"""
7373

74-
path: str
75-
nest: Optional[str] = None
76-
partition: Optional[str] = None
74+
parts: list[str] = []
75+
7776
key: Optional[str] = None
77+
nest: Optional[str] = None
78+
location: Optional[str] = None
79+
index: Optional[int] = None
7880

79-
es_path: Optional[str] = None
80-
es_nest: Optional[str] = None
8181
es_key: Optional[str] = None
82+
es_nest: Optional[str] = None
83+
es_location: Optional[str] = None
84+
es_index: Optional[str] = None
85+
86+
variable_name: Optional[str] = None
87+
param_key: Optional[str] = None
88+
89+
class Config:
90+
"""Class config."""
8291

83-
index_: Optional[int] = None
92+
frozen = True
8493

8594
@model_validator(mode="before")
8695
@classmethod
@@ -90,77 +99,33 @@ def validate_model(cls, data: Any):
9099
Args:
91100
data (Any): input data
92101
"""
93-
data["path"] = data["path"].lstrip("/").replace("/", ".")
94-
data["nest"], data["partition"], data["key"] = data["path"].rpartition(".")
102+
data["parts"] = data["path"].lstrip("/").split("/")
103+
data["key"] = data["parts"].pop(-1)
95104

96105
if data["key"].lstrip("-").isdigit() or data["key"] == "-":
97-
data["index_"] = -1 if data["key"] == "-" else int(data["key"])
98-
data["path"] = f"{data['nest']}[{data['index_']}]"
99-
data["nest"], data["partition"], data["key"] = data["nest"].rpartition(".")
100-
101-
data["es_path"] = to_es(data["path"])
102-
data["es_nest"] = f".{to_es(data['nest'])}" if data["nest"] else ""
103-
data["es_key"] = to_es(data["key"])
106+
data["index"] = -1 if data["key"] == "-" else int(data["key"])
107+
data["key"] = data["parts"].pop(-1)
108+
109+
data["nest"] = ".".join(data["parts"])
110+
data["location"] = data["nest"] + "." + data["key"]
111+
112+
data["es_key"] = f"['{data['key']}']"
113+
data["es_nest"] = "".join([f"['{part}']" for part in data["parts"]])
114+
data["es_location"] = data["es_nest"] + data["es_key"]
115+
data[
116+
"variable_name"
117+
] = f"{data['nest'].replace('.','_').replace(':','_')}_{data['key'].replace(':','_')}"
118+
data["param_key"] = data["location"].translate(replacements)
119+
120+
if "index" in data:
121+
data["es_index"] = (
122+
f"ctx._source{data['es_location']}.size() - {-data['index']}"
123+
if data["index"] < 0
124+
else data["index"]
125+
)
126+
127+
data[
128+
"variable_name"
129+
] = f"{data['location'].replace('.','_').replace(':','_')}_{data['index']}"
104130

105131
return data
106-
107-
@computed_field # type: ignore[misc]
108-
@property
109-
def index(self) -> Union[int, str, None]:
110-
"""Compute location of path.
111-
112-
Returns:
113-
str: path index
114-
"""
115-
if self.index_ and self.index_ < 0:
116-
117-
return f"ctx._source.{self.location}.size() - {-self.index_}"
118-
119-
return self.index_
120-
121-
@computed_field # type: ignore[misc]
122-
@property
123-
def location(self) -> str:
124-
"""Compute location of path.
125-
126-
Returns:
127-
str: path location
128-
"""
129-
return self.nest + self.partition + self.key
130-
131-
@computed_field # type: ignore[misc]
132-
@property
133-
def es_location(self) -> str:
134-
"""Compute location of path.
135-
136-
Returns:
137-
str: path location
138-
"""
139-
if self.es_key and ":" in self.es_key:
140-
return self.es_nest + self.es_key
141-
return self.es_nest + self.partition + self.es_key
142-
143-
@computed_field # type: ignore[misc]
144-
@property
145-
def variable_name(self) -> str:
146-
"""Variable name for scripting.
147-
148-
Returns:
149-
str: variable name
150-
"""
151-
if self.index is not None:
152-
return f"{self.location.replace('.','_').replace(':','_')}_{self.index}"
153-
154-
return (
155-
f"{self.nest.replace('.','_').replace(':','_')}_{self.key.replace(':','_')}"
156-
)
157-
158-
@computed_field # type: ignore[misc]
159-
@property
160-
def param_key(self) -> str:
161-
"""Param key for scripting.
162-
163-
Returns:
164-
str: param key
165-
"""
166-
return self.path.translate(replacements)

0 commit comments

Comments
 (0)