Skip to content

Commit a2d6f48

Browse files
committed
Simplify conversion logic.
1 parent f1c320a commit a2d6f48

File tree

3 files changed

+152
-55
lines changed

3 files changed

+152
-55
lines changed

stac_fastapi/core/stac_fastapi/core/models/patch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def validate_model(cls, data: Any):
3636
data["path"] = f"{data['nest']}[{data['index']}]"
3737
data["nest"], data["partition"], data["key"] = data["nest"].rpartition(".")
3838

39+
return data
40+
3941
@computed_field # type: ignore[misc]
4042
@property
4143
def location(self) -> str:

stac_fastapi/core/stac_fastapi/core/utilities.py

Lines changed: 149 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44
such as converting bounding boxes to polygon representations.
55
"""
66

7+
import re
78
from typing import Any, Dict, List, Optional, Set, Union
89

910
from stac_fastapi.core.models.patch import ElasticPath
10-
from stac_fastapi.types.stac import Item, PatchAddReplaceTest, PatchRemove
11+
from stac_fastapi.types.stac import (
12+
Item,
13+
PatchAddReplaceTest,
14+
PatchOperation,
15+
PatchRemove,
16+
)
1117

1218
MAX_LIMIT = 10000
1319

@@ -166,29 +172,149 @@ def merge_to_operations(data: Dict) -> List:
166172
return operations
167173

168174

169-
def add_script_checks(source: str, op: str, path: ElasticPath) -> str:
175+
def check_commands(
176+
commands: List[str],
177+
op: str,
178+
path: ElasticPath,
179+
from_path: bool = False,
180+
) -> None:
170181
"""Add Elasticsearch checks to operation.
171182
172183
Args:
173-
source (str): current source of Elasticsearch script
184+
commands (List[str]): current commands
174185
op (str): the operation of script
175186
path (Dict): path of variable to run operation on
187+
from_path (bool): True if path is a from path
176188
177-
Returns:
178-
Dict: update source of Elasticsearch script
179189
"""
180190
if path.nest:
181-
source += (
191+
commands.append(
182192
f"if (!ctx._source.containsKey('{path.nest}'))"
183193
f"{{Debug.explain('{path.nest} does not exist');}}"
184194
)
185195

186-
if path.index or op != "add":
187-
source += (
196+
if path.index or op in ["remove", "replace", "test"] or from_path:
197+
commands.append(
188198
f"if (!ctx._source.{path.nest}.containsKey('{path.key}'))"
189-
f"{{Debug.explain('{path.path} does not exist');}}"
199+
f"{{Debug.explain('{path.key} does not exist in {path.nest}');}}"
200+
)
201+
202+
203+
def copy_commands(
204+
commands: List[str],
205+
operation: PatchOperation,
206+
path: ElasticPath,
207+
from_path: ElasticPath,
208+
) -> None:
209+
"""Copy value from path to from path.
210+
211+
Args:
212+
commands (List[str]): current commands
213+
operation (PatchOperation): Operation to be converted
214+
op_path (ElasticPath): Path to copy to
215+
from_path (ElasticPath): Path to copy from
216+
217+
"""
218+
check_commands(operation.op, from_path, True)
219+
220+
if from_path.index:
221+
commands.append(
222+
f"if ((ctx._source.{from_path.location} instanceof ArrayList"
223+
f" && ctx._source.{from_path.location}.size() < {from_path.index})"
224+
f" || (!ctx._source.{from_path.location}.containsKey('{from_path.index}'))"
225+
f"{{Debug.explain('{from_path.path} does not exist');}}"
226+
)
227+
228+
if path.index:
229+
commands.append(
230+
f"if (ctx._source.{path.location} instanceof ArrayList)"
231+
f"{{ctx._source.{path.location}.add({path.index}, {from_path.path})}}"
232+
f"else{{ctx._source.{path.path} = {from_path.path}}}"
233+
)
234+
235+
else:
236+
commands.append(f"ctx._source.{path.path} = ctx._source.{from_path.path};")
237+
238+
239+
def remove_commands(commands: List[str], path: ElasticPath) -> None:
240+
"""Remove value at path.
241+
242+
Args:
243+
commands (List[str]): current commands
244+
path (ElasticPath): Path to value to be removed
245+
246+
"""
247+
if path.index:
248+
commands.append(f"ctx._source.{path.location}.remove('{path.index}');")
249+
250+
else:
251+
commands.append(f"ctx._source.{path.nest}.remove('{path.key}');")
252+
253+
254+
def add_commands(
255+
commands: List[str], operation: PatchOperation, path: ElasticPath
256+
) -> None:
257+
"""Add value at path.
258+
259+
Args:
260+
commands (List[str]): current commands
261+
operation (PatchOperation): operation to run
262+
path (ElasticPath): path for value to be added
263+
264+
"""
265+
if path.index:
266+
commands.append(
267+
f"if (ctx._source.{path.location} instanceof ArrayList)"
268+
f"{{ctx._source.{path.location}.add({path.index}, {operation.json_value})}}"
269+
f"else{{ctx._source.{path.path} = {operation.json_value}}}"
190270
)
191271

272+
else:
273+
commands.append(f"ctx._source.{path.path} = {operation.json_value};")
274+
275+
276+
def test_commands(
277+
commands: List[str], operation: PatchOperation, path: ElasticPath
278+
) -> None:
279+
"""Test value at path.
280+
281+
Args:
282+
commands (List[str]): current commands
283+
operation (PatchOperation): operation to run
284+
path (ElasticPath): path for value to be tested
285+
"""
286+
commands.append(
287+
f"if (ctx._source.{path.location} != {operation.json_value})"
288+
f"{{Debug.explain('Test failed for: {path.path} | "
289+
f"{operation.json_value} != ' + ctx._source.{path.location});}}"
290+
)
291+
292+
293+
def commands_to_source(commands: List[str]) -> str:
294+
"""Convert list of commands to Elasticsearch script source.
295+
296+
Args:
297+
commands (List[str]): List of Elasticearch commands
298+
299+
Returns:
300+
str: Elasticsearch script source
301+
"""
302+
seen: Set[str] = set()
303+
seen_add = seen.add
304+
regex = re.compile(r"([^.' ]*:[^.' ]*)[. ]")
305+
source = ""
306+
307+
# filter duplicate lines
308+
for command in commands:
309+
if command not in seen:
310+
seen_add(command)
311+
# extension terms with using `:` must be swapped out
312+
if matches := regex.findall(command):
313+
for match in matches:
314+
command = command.replace(f".{match}", f"['{match}']")
315+
316+
source += command
317+
192318
return source
193319

194320

@@ -201,60 +327,29 @@ def operations_to_script(operations: List) -> Dict:
201327
Returns:
202328
Dict: elasticsearch update script.
203329
"""
204-
source = ""
330+
commands: List = []
205331
for operation in operations:
206-
op_path = ElasticPath(path=operation.path)
207-
source = add_script_checks(source, operation.op, op_path)
208-
209-
if hasattr(operation, "from"):
210-
from_path = ElasticPath(path=(getattr(operation, "from")))
211-
source = add_script_checks(source, operation.op, from_path)
212-
if from_path.index:
213-
source += (
214-
f"if ((ctx._source.{from_path.location} instanceof ArrayList"
215-
f" && ctx._source.{from_path.location}.size() < {from_path.index})"
216-
f" || (!ctx._source.{from_path.location}.containsKey('{from_path.index}'))"
217-
f"{{Debug.explain('{from_path.path} does not exist');}}"
218-
)
332+
path = ElasticPath(path=operation.path)
333+
from_path = (
334+
ElasticPath(path=operation.from_) if hasattr(operation, "from_") else None
335+
)
219336

220-
if operation.op in ["copy", "move"]:
221-
if op_path.index:
222-
source += (
223-
f"if (ctx._source.{op_path.location} instanceof ArrayList)"
224-
f"{{ctx._source.{op_path.location}.add({op_path.index}, {from_path.path})}}"
225-
f"else{{ctx._source.{op_path.path} = {from_path.path}}}"
226-
)
337+
check_commands(commands, operation.op, path)
227338

228-
else:
229-
source += f"ctx._source.{op_path.path} = ctx._source.{from_path.path};"
339+
if operation.op in ["copy", "move"]:
340+
copy_commands(commands, operation, path, from_path)
230341

231342
if operation.op in ["remove", "move"]:
232-
remove_path = from_path if operation.op == "move" else op_path
233-
234-
if remove_path.index:
235-
source += (
236-
f"ctx._source.{remove_path.location}.remove('{remove_path.index}');"
237-
)
238-
239-
else:
240-
source += f"ctx._source.remove('{remove_path.location}');"
343+
remove_path = from_path if from_path else path
344+
remove_commands(commands, remove_path)
241345

242346
if operation.op in ["add", "replace"]:
243-
if op_path["index"]:
244-
source += (
245-
f"if (ctx._source.{op_path.location} instanceof ArrayList)"
246-
f"{{ctx._source.{op_path.location}.add({op_path.index}, {operation.json_value})}}"
247-
f"else{{ctx._source.{op_path.path} = {operation.json_value}}}"
248-
)
249-
250-
else:
251-
source += f"ctx._source.{op_path.path} = {operation.json_value};"
347+
add_commands(commands, operation, path)
252348

253349
if operation.op == "test":
254-
source += (
255-
f"if (ctx._source.{op_path.location} != {operation.json_value})"
256-
f"{{Debug.explain('Test failed for: {op_path.path} | {operation.json_value} != ctx._source.{op_path.location}');}}"
257-
)
350+
test_commands(commands, operation, path)
351+
352+
source = commands_to_source(commands)
258353

259354
return {
260355
"source": source,

stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/database_logic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ async def json_patch_item(
980980

981981
try:
982982
await self.client.update(
983-
index=index_by_collection_id(collection_id),
983+
index=index_alias_by_collection_id(collection_id),
984984
id=mk_item_id(item_id, collection_id),
985985
script=script,
986986
refresh=True,

0 commit comments

Comments
 (0)