infrahub-server 1.2.6__py3-none-any.whl → 1.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/cli/db.py +2 -0
- infrahub/cli/patch.py +153 -0
- infrahub/computed_attribute/models.py +81 -1
- infrahub/computed_attribute/tasks.py +34 -53
- infrahub/core/node/__init__.py +4 -1
- infrahub/core/query/ipam.py +7 -5
- infrahub/patch/__init__.py +0 -0
- infrahub/patch/constants.py +13 -0
- infrahub/patch/edge_adder.py +64 -0
- infrahub/patch/edge_deleter.py +33 -0
- infrahub/patch/edge_updater.py +28 -0
- infrahub/patch/models.py +98 -0
- infrahub/patch/plan_reader.py +107 -0
- infrahub/patch/plan_writer.py +92 -0
- infrahub/patch/queries/__init__.py +0 -0
- infrahub/patch/queries/base.py +17 -0
- infrahub/patch/runner.py +254 -0
- infrahub/patch/vertex_adder.py +61 -0
- infrahub/patch/vertex_deleter.py +33 -0
- infrahub/patch/vertex_updater.py +28 -0
- infrahub_sdk/checks.py +1 -1
- infrahub_sdk/ctl/cli_commands.py +2 -2
- infrahub_sdk/ctl/menu.py +56 -13
- infrahub_sdk/ctl/object.py +55 -5
- infrahub_sdk/ctl/utils.py +22 -1
- infrahub_sdk/exceptions.py +19 -1
- infrahub_sdk/node.py +42 -26
- infrahub_sdk/protocols_generator/__init__.py +0 -0
- infrahub_sdk/protocols_generator/constants.py +28 -0
- infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
- infrahub_sdk/protocols_generator/template.j2 +114 -0
- infrahub_sdk/schema/__init__.py +110 -74
- infrahub_sdk/schema/main.py +36 -2
- infrahub_sdk/schema/repository.py +2 -0
- infrahub_sdk/spec/menu.py +3 -3
- infrahub_sdk/spec/object.py +522 -41
- infrahub_sdk/testing/docker.py +4 -5
- infrahub_sdk/testing/schemas/animal.py +7 -0
- infrahub_sdk/yaml.py +63 -7
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/METADATA +1 -1
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/RECORD +44 -27
- infrahub_sdk/ctl/constants.py +0 -115
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/WHEEL +0 -0
- {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/entry_points.txt +0 -0
infrahub/patch/models.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
PropertyPrimitives = str | bool | int | float | None
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def str_uuid() -> str:
|
|
8
|
+
return str(uuid4())
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class VertexToAdd:
|
|
13
|
+
labels: list[str]
|
|
14
|
+
after_props: dict[str, PropertyPrimitives]
|
|
15
|
+
identifier: str = field(default_factory=str_uuid)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class VertexToUpdate:
|
|
20
|
+
db_id: str
|
|
21
|
+
before_props: dict[str, PropertyPrimitives]
|
|
22
|
+
after_props: dict[str, PropertyPrimitives]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class VertexToDelete:
|
|
27
|
+
db_id: str
|
|
28
|
+
labels: list[str]
|
|
29
|
+
before_props: dict[str, PropertyPrimitives]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class EdgeToAdd:
|
|
34
|
+
from_id: str
|
|
35
|
+
to_id: str
|
|
36
|
+
edge_type: str
|
|
37
|
+
after_props: dict[str, PropertyPrimitives]
|
|
38
|
+
identifier: str = field(default_factory=str_uuid)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class EdgeToUpdate:
|
|
43
|
+
db_id: str
|
|
44
|
+
before_props: dict[str, PropertyPrimitives]
|
|
45
|
+
after_props: dict[str, PropertyPrimitives]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class EdgeToDelete:
|
|
50
|
+
db_id: str
|
|
51
|
+
from_id: str
|
|
52
|
+
to_id: str
|
|
53
|
+
edge_type: str
|
|
54
|
+
before_props: dict[str, PropertyPrimitives]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class PatchPlan:
|
|
59
|
+
name: str
|
|
60
|
+
vertices_to_add: list[VertexToAdd] = field(default_factory=list)
|
|
61
|
+
vertices_to_update: list[VertexToUpdate] = field(default_factory=list)
|
|
62
|
+
vertices_to_delete: list[VertexToDelete] = field(default_factory=list)
|
|
63
|
+
edges_to_add: list[EdgeToAdd] = field(default_factory=list)
|
|
64
|
+
edges_to_update: list[EdgeToUpdate] = field(default_factory=list)
|
|
65
|
+
edges_to_delete: list[EdgeToDelete] = field(default_factory=list)
|
|
66
|
+
added_element_db_id_map: dict[str, str] = field(default_factory=dict)
|
|
67
|
+
deleted_db_ids: set[str] = field(default_factory=set)
|
|
68
|
+
reverted_deleted_db_id_map: dict[str, str] = field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
def get_database_id_for_added_element(self, abstract_id: str) -> str:
|
|
71
|
+
return self.added_element_db_id_map.get(abstract_id, abstract_id)
|
|
72
|
+
|
|
73
|
+
def has_element_been_added(self, identifier: str) -> bool:
|
|
74
|
+
return identifier in self.added_element_db_id_map
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def added_vertices(self) -> list[VertexToAdd]:
|
|
78
|
+
return [v for v in self.vertices_to_add if self.has_element_been_added(v.identifier)]
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def added_edges(self) -> list[EdgeToAdd]:
|
|
82
|
+
return [e for e in self.edges_to_add if self.has_element_been_added(e.identifier)]
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def deleted_vertices(self) -> list[VertexToDelete]:
|
|
86
|
+
return [v for v in self.vertices_to_delete if v.db_id in self.deleted_db_ids]
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def deleted_edges(self) -> list[EdgeToDelete]:
|
|
90
|
+
return [e for e in self.edges_to_delete if e.db_id in self.deleted_db_ids]
|
|
91
|
+
|
|
92
|
+
def drop_added_db_ids(self, db_ids_to_drop: set[str]) -> None:
|
|
93
|
+
self.added_element_db_id_map = {
|
|
94
|
+
k: v for k, v in self.added_element_db_id_map.items() if v not in db_ids_to_drop
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def drop_deleted_db_ids(self, db_ids_to_drop: set[str]) -> None:
|
|
98
|
+
self.deleted_db_ids -= db_ids_to_drop
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Generator
|
|
4
|
+
|
|
5
|
+
from .constants import PatchPlanFilename
|
|
6
|
+
from .models import EdgeToAdd, EdgeToDelete, EdgeToUpdate, PatchPlan, VertexToAdd, VertexToDelete, VertexToUpdate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PatchPlanReader:
|
|
10
|
+
def read(self, patch_plan_directory: Path) -> PatchPlan:
|
|
11
|
+
vertices_to_add = self._read_vertices_to_add(patch_plan_directory=patch_plan_directory)
|
|
12
|
+
vertices_to_delete = self._read_vertices_to_delete(patch_plan_directory=patch_plan_directory)
|
|
13
|
+
vertices_to_update = self._read_vertices_to_update(patch_plan_directory=patch_plan_directory)
|
|
14
|
+
edges_to_add = self._read_edges_to_add(patch_plan_directory=patch_plan_directory)
|
|
15
|
+
edges_to_delete = self._read_edges_to_delete(patch_plan_directory=patch_plan_directory)
|
|
16
|
+
edges_to_update = self._read_edges_to_update(patch_plan_directory=patch_plan_directory)
|
|
17
|
+
added_node_db_id_map = self._read_added_node_db_id_map(patch_plan_directory=patch_plan_directory)
|
|
18
|
+
deleted_db_ids = self._read_deleted_db_ids(patch_plan_directory=patch_plan_directory)
|
|
19
|
+
reverted_deleted_db_id_map = self._read_reverted_deleted_db_id_map(patch_plan_directory=patch_plan_directory)
|
|
20
|
+
|
|
21
|
+
return PatchPlan(
|
|
22
|
+
name="none",
|
|
23
|
+
vertices_to_add=vertices_to_add,
|
|
24
|
+
vertices_to_delete=vertices_to_delete,
|
|
25
|
+
vertices_to_update=vertices_to_update,
|
|
26
|
+
edges_to_add=edges_to_add,
|
|
27
|
+
edges_to_delete=edges_to_delete,
|
|
28
|
+
edges_to_update=edges_to_update,
|
|
29
|
+
added_element_db_id_map=added_node_db_id_map or {},
|
|
30
|
+
deleted_db_ids=deleted_db_ids or set(),
|
|
31
|
+
reverted_deleted_db_id_map=reverted_deleted_db_id_map or {},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _read_file_lines(self, patch_file: Path) -> Generator[str | None, None, None]:
|
|
35
|
+
if not patch_file.exists():
|
|
36
|
+
return
|
|
37
|
+
with patch_file.open() as f:
|
|
38
|
+
yield from f
|
|
39
|
+
|
|
40
|
+
def _read_vertices_to_add(self, patch_plan_directory: Path) -> list[VertexToAdd]:
|
|
41
|
+
file = patch_plan_directory / Path(PatchPlanFilename.VERTICES_TO_ADD.value)
|
|
42
|
+
vertices_to_add: list[VertexToAdd] = []
|
|
43
|
+
for raw_line in self._read_file_lines(patch_file=file):
|
|
44
|
+
if raw_line:
|
|
45
|
+
vertices_to_add.append(VertexToAdd(**json.loads(raw_line)))
|
|
46
|
+
return vertices_to_add
|
|
47
|
+
|
|
48
|
+
def _read_vertices_to_update(self, patch_plan_directory: Path) -> list[VertexToUpdate]:
|
|
49
|
+
file = patch_plan_directory / Path(PatchPlanFilename.VERTICES_TO_UPDATE.value)
|
|
50
|
+
vertices_to_update: list[VertexToUpdate] = []
|
|
51
|
+
for raw_line in self._read_file_lines(patch_file=file):
|
|
52
|
+
if raw_line:
|
|
53
|
+
vertices_to_update.append(VertexToUpdate(**json.loads(raw_line)))
|
|
54
|
+
return vertices_to_update
|
|
55
|
+
|
|
56
|
+
def _read_vertices_to_delete(self, patch_plan_directory: Path) -> list[VertexToDelete]:
|
|
57
|
+
file = patch_plan_directory / Path(PatchPlanFilename.VERTICES_TO_DELETE.value)
|
|
58
|
+
vertices_to_delete: list[VertexToDelete] = []
|
|
59
|
+
for raw_line in self._read_file_lines(patch_file=file):
|
|
60
|
+
if raw_line:
|
|
61
|
+
vertices_to_delete.append(VertexToDelete(**json.loads(raw_line)))
|
|
62
|
+
return vertices_to_delete
|
|
63
|
+
|
|
64
|
+
def _read_edges_to_add(self, patch_plan_directory: Path) -> list[EdgeToAdd]:
|
|
65
|
+
file = patch_plan_directory / Path(PatchPlanFilename.EDGES_TO_ADD.value)
|
|
66
|
+
edges_to_add: list[EdgeToAdd] = []
|
|
67
|
+
for raw_line in self._read_file_lines(patch_file=file):
|
|
68
|
+
if raw_line:
|
|
69
|
+
edges_to_add.append(EdgeToAdd(**json.loads(raw_line)))
|
|
70
|
+
return edges_to_add
|
|
71
|
+
|
|
72
|
+
def _read_edges_to_delete(self, patch_plan_directory: Path) -> list[EdgeToDelete]:
|
|
73
|
+
file = patch_plan_directory / Path(PatchPlanFilename.EDGES_TO_DELETE.value)
|
|
74
|
+
edges_to_delete: list[EdgeToDelete] = []
|
|
75
|
+
for raw_line in self._read_file_lines(patch_file=file):
|
|
76
|
+
if raw_line:
|
|
77
|
+
edges_to_delete.append(EdgeToDelete(**json.loads(raw_line)))
|
|
78
|
+
return edges_to_delete
|
|
79
|
+
|
|
80
|
+
def _read_edges_to_update(self, patch_plan_directory: Path) -> list[EdgeToUpdate]:
|
|
81
|
+
file = patch_plan_directory / Path(PatchPlanFilename.EDGES_TO_UPDATE.value)
|
|
82
|
+
edges_to_update: list[EdgeToUpdate] = []
|
|
83
|
+
for raw_line in self._read_file_lines(patch_file=file):
|
|
84
|
+
if raw_line:
|
|
85
|
+
edges_to_update.append(EdgeToUpdate(**json.loads(raw_line)))
|
|
86
|
+
return edges_to_update
|
|
87
|
+
|
|
88
|
+
def _read_added_node_db_id_map(self, patch_plan_directory: Path) -> dict[str, str] | None:
|
|
89
|
+
file = patch_plan_directory / Path(PatchPlanFilename.ADDED_DB_IDS.value)
|
|
90
|
+
if not file.exists():
|
|
91
|
+
return None
|
|
92
|
+
added_db_id_json = file.read_text()
|
|
93
|
+
return json.loads(added_db_id_json)
|
|
94
|
+
|
|
95
|
+
def _read_deleted_db_ids(self, patch_plan_directory: Path) -> set[str] | None:
|
|
96
|
+
file = patch_plan_directory / Path(PatchPlanFilename.DELETED_DB_IDS.value)
|
|
97
|
+
if not file.exists():
|
|
98
|
+
return None
|
|
99
|
+
deleted_db_ids_json = file.read_text()
|
|
100
|
+
return set(json.loads(deleted_db_ids_json))
|
|
101
|
+
|
|
102
|
+
def _read_reverted_deleted_db_id_map(self, patch_plan_directory: Path) -> dict[str, str] | None:
|
|
103
|
+
file = patch_plan_directory / Path(PatchPlanFilename.REVERTED_DELETED_DB_IDS.value)
|
|
104
|
+
if not file.exists():
|
|
105
|
+
return None
|
|
106
|
+
reverted_deleted_db_id_json = file.read_text()
|
|
107
|
+
return json.loads(reverted_deleted_db_id_json)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .constants import PatchPlanFilename
|
|
8
|
+
from .models import EdgeToAdd, EdgeToDelete, EdgeToUpdate, PatchPlan, VertexToAdd, VertexToDelete, VertexToUpdate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PatchPlanWriter:
|
|
12
|
+
def write(self, patches_directory: Path, patch_plan: PatchPlan) -> Path:
|
|
13
|
+
timestamp_str = datetime.now(tz=timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
14
|
+
patch_name = f"patch-{patch_plan.name}-{timestamp_str}"
|
|
15
|
+
patch_plan_directory = patches_directory / Path(patch_name)
|
|
16
|
+
if not patch_plan_directory.exists():
|
|
17
|
+
patch_plan_directory.mkdir(parents=True)
|
|
18
|
+
if patch_plan.vertices_to_add:
|
|
19
|
+
self._write_vertices_to_add(
|
|
20
|
+
patch_plan_directory=patch_plan_directory, vertices_to_add=patch_plan.vertices_to_add
|
|
21
|
+
)
|
|
22
|
+
if patch_plan.vertices_to_delete:
|
|
23
|
+
self._write_vertices_to_delete(
|
|
24
|
+
patch_plan_directory=patch_plan_directory, vertices_to_delete=patch_plan.vertices_to_delete
|
|
25
|
+
)
|
|
26
|
+
if patch_plan.vertices_to_update:
|
|
27
|
+
self._write_vertices_to_update(
|
|
28
|
+
patch_plan_directory=patch_plan_directory, vertices_to_update=patch_plan.vertices_to_update
|
|
29
|
+
)
|
|
30
|
+
if patch_plan.edges_to_add:
|
|
31
|
+
self._write_edges_to_add(patch_plan_directory=patch_plan_directory, edges_to_add=patch_plan.edges_to_add)
|
|
32
|
+
if patch_plan.edges_to_delete:
|
|
33
|
+
self._write_edges_to_delete(
|
|
34
|
+
patch_plan_directory=patch_plan_directory, edges_to_delete=patch_plan.edges_to_delete
|
|
35
|
+
)
|
|
36
|
+
if patch_plan.edges_to_update:
|
|
37
|
+
self._write_edges_to_update(
|
|
38
|
+
patch_plan_directory=patch_plan_directory, edges_to_update=patch_plan.edges_to_update
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return patch_plan_directory
|
|
42
|
+
|
|
43
|
+
def write_added_db_id_map(self, patch_plan_directory: Path, db_id_map: dict[str, str]) -> None:
|
|
44
|
+
file = patch_plan_directory / Path(PatchPlanFilename.ADDED_DB_IDS.value)
|
|
45
|
+
file.touch(exist_ok=True)
|
|
46
|
+
with file.open(mode="w") as f:
|
|
47
|
+
f.write(json.dumps(db_id_map) + "\n")
|
|
48
|
+
|
|
49
|
+
def write_deleted_db_ids(self, patch_plan_directory: Path, deleted_ids: set[str]) -> None:
|
|
50
|
+
file = patch_plan_directory / Path(PatchPlanFilename.DELETED_DB_IDS.value)
|
|
51
|
+
file.touch(exist_ok=True)
|
|
52
|
+
with file.open(mode="w") as f:
|
|
53
|
+
f.write(json.dumps(list(deleted_ids)) + "\n")
|
|
54
|
+
|
|
55
|
+
def write_reverted_deleted_db_id_map(self, patch_plan_directory: Path, db_id_map: dict[str, str]) -> None:
|
|
56
|
+
file = patch_plan_directory / Path(PatchPlanFilename.REVERTED_DELETED_DB_IDS.value)
|
|
57
|
+
file.touch(exist_ok=True)
|
|
58
|
+
with file.open(mode="w") as f:
|
|
59
|
+
f.write(json.dumps(db_id_map) + "\n")
|
|
60
|
+
|
|
61
|
+
def _dataclass_to_json_line(self, dataclass_instance: Any) -> str:
|
|
62
|
+
return json.dumps(asdict(dataclass_instance)) + "\n"
|
|
63
|
+
|
|
64
|
+
def _write_to_file(self, file_path: Path, objects: list[Any]) -> None:
|
|
65
|
+
file_path.touch(exist_ok=True)
|
|
66
|
+
with file_path.open(mode="w") as f:
|
|
67
|
+
for obj in objects:
|
|
68
|
+
f.write(self._dataclass_to_json_line(obj))
|
|
69
|
+
|
|
70
|
+
def _write_vertices_to_add(self, patch_plan_directory: Path, vertices_to_add: list[VertexToAdd]) -> None:
|
|
71
|
+
file = patch_plan_directory / Path(PatchPlanFilename.VERTICES_TO_ADD.value)
|
|
72
|
+
self._write_to_file(file_path=file, objects=vertices_to_add)
|
|
73
|
+
|
|
74
|
+
def _write_vertices_to_delete(self, patch_plan_directory: Path, vertices_to_delete: list[VertexToDelete]) -> None:
|
|
75
|
+
file = patch_plan_directory / Path(PatchPlanFilename.VERTICES_TO_DELETE.value)
|
|
76
|
+
self._write_to_file(file_path=file, objects=vertices_to_delete)
|
|
77
|
+
|
|
78
|
+
def _write_vertices_to_update(self, patch_plan_directory: Path, vertices_to_update: list[VertexToUpdate]) -> None:
|
|
79
|
+
file = patch_plan_directory / Path(PatchPlanFilename.VERTICES_TO_UPDATE.value)
|
|
80
|
+
self._write_to_file(file_path=file, objects=vertices_to_update)
|
|
81
|
+
|
|
82
|
+
def _write_edges_to_add(self, patch_plan_directory: Path, edges_to_add: list[EdgeToAdd]) -> None:
|
|
83
|
+
file = patch_plan_directory / Path(PatchPlanFilename.EDGES_TO_ADD.value)
|
|
84
|
+
self._write_to_file(file_path=file, objects=edges_to_add)
|
|
85
|
+
|
|
86
|
+
def _write_edges_to_delete(self, patch_plan_directory: Path, edges_to_delete: list[EdgeToDelete]) -> None:
|
|
87
|
+
file = patch_plan_directory / Path(PatchPlanFilename.EDGES_TO_DELETE.value)
|
|
88
|
+
self._write_to_file(file_path=file, objects=edges_to_delete)
|
|
89
|
+
|
|
90
|
+
def _write_edges_to_update(self, patch_plan_directory: Path, edges_to_update: list[EdgeToUpdate]) -> None:
|
|
91
|
+
file = patch_plan_directory / Path(PatchPlanFilename.EDGES_TO_UPDATE.value)
|
|
92
|
+
self._write_to_file(file_path=file, objects=edges_to_update)
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from infrahub.database import InfrahubDatabase
|
|
4
|
+
|
|
5
|
+
from ..models import PatchPlan
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PatchQuery(ABC):
|
|
9
|
+
def __init__(self, db: InfrahubDatabase):
|
|
10
|
+
self.db = db
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def plan(self) -> PatchPlan: ...
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def name(self) -> str: ...
|
infrahub/patch/runner.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .edge_adder import PatchPlanEdgeAdder
|
|
4
|
+
from .edge_deleter import PatchPlanEdgeDeleter
|
|
5
|
+
from .edge_updater import PatchPlanEdgeUpdater
|
|
6
|
+
from .models import EdgeToAdd, EdgeToDelete, EdgeToUpdate, PatchPlan, VertexToAdd, VertexToDelete, VertexToUpdate
|
|
7
|
+
from .plan_reader import PatchPlanReader
|
|
8
|
+
from .plan_writer import PatchPlanWriter
|
|
9
|
+
from .queries.base import PatchQuery
|
|
10
|
+
from .vertex_adder import PatchPlanVertexAdder
|
|
11
|
+
from .vertex_deleter import PatchPlanVertexDeleter
|
|
12
|
+
from .vertex_updater import PatchPlanVertexUpdater
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PatchPlanEdgeDbIdTranslator:
|
|
16
|
+
def translate_to_db_ids(self, patch_plan: PatchPlan) -> None:
|
|
17
|
+
for edge_to_add in patch_plan.edges_to_add:
|
|
18
|
+
translated_from_id = patch_plan.get_database_id_for_added_element(abstract_id=edge_to_add.from_id)
|
|
19
|
+
edge_to_add.from_id = translated_from_id
|
|
20
|
+
translated_to_id = patch_plan.get_database_id_for_added_element(abstract_id=edge_to_add.to_id)
|
|
21
|
+
edge_to_add.to_id = translated_to_id
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PatchRunner:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
plan_writer: PatchPlanWriter,
|
|
28
|
+
plan_reader: PatchPlanReader,
|
|
29
|
+
edge_db_id_translator: PatchPlanEdgeDbIdTranslator,
|
|
30
|
+
vertex_adder: PatchPlanVertexAdder,
|
|
31
|
+
vertex_updater: PatchPlanVertexUpdater,
|
|
32
|
+
vertex_deleter: PatchPlanVertexDeleter,
|
|
33
|
+
edge_adder: PatchPlanEdgeAdder,
|
|
34
|
+
edge_updater: PatchPlanEdgeUpdater,
|
|
35
|
+
edge_deleter: PatchPlanEdgeDeleter,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.plan_writer = plan_writer
|
|
38
|
+
self.plan_reader = plan_reader
|
|
39
|
+
self.edge_db_id_translator = edge_db_id_translator
|
|
40
|
+
self.vertex_adder = vertex_adder
|
|
41
|
+
self.vertex_updater = vertex_updater
|
|
42
|
+
self.vertex_deleter = vertex_deleter
|
|
43
|
+
self.edge_adder = edge_adder
|
|
44
|
+
self.edge_updater = edge_updater
|
|
45
|
+
self.edge_deleter = edge_deleter
|
|
46
|
+
|
|
47
|
+
async def prepare_plan(self, patch_query: PatchQuery, directory: Path) -> Path:
|
|
48
|
+
patch_plan = await patch_query.plan()
|
|
49
|
+
return self.plan_writer.write(patches_directory=directory, patch_plan=patch_plan)
|
|
50
|
+
|
|
51
|
+
async def apply(self, patch_plan_directory: Path) -> PatchPlan:
|
|
52
|
+
patch_plan = self.plan_reader.read(patch_plan_directory)
|
|
53
|
+
await self._apply_vertices_to_add(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
54
|
+
await self._apply_edges_to_add(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
55
|
+
if patch_plan.vertices_to_update:
|
|
56
|
+
await self.vertex_updater.execute(vertices_to_update=patch_plan.vertices_to_update)
|
|
57
|
+
await self._apply_edges_to_delete(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
58
|
+
await self._apply_vertices_to_delete(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
59
|
+
if patch_plan.edges_to_update:
|
|
60
|
+
await self.edge_updater.execute(edges_to_update=patch_plan.edges_to_update)
|
|
61
|
+
return patch_plan
|
|
62
|
+
|
|
63
|
+
async def _apply_vertices_to_add(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
64
|
+
if not patch_plan.vertices_to_add:
|
|
65
|
+
return
|
|
66
|
+
unadded_vertices = [
|
|
67
|
+
v for v in patch_plan.vertices_to_add if not patch_plan.has_element_been_added(v.identifier)
|
|
68
|
+
]
|
|
69
|
+
try:
|
|
70
|
+
async for added_element_id_map in self.vertex_adder.execute(vertices_to_add=unadded_vertices):
|
|
71
|
+
patch_plan.added_element_db_id_map.update(added_element_id_map)
|
|
72
|
+
finally:
|
|
73
|
+
# record the added elements so that we do not double-add them if the patch is run again
|
|
74
|
+
self.plan_writer.write_added_db_id_map(
|
|
75
|
+
patch_plan_directory=patch_plan_directory, db_id_map=patch_plan.added_element_db_id_map
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def _apply_edges_to_add(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
79
|
+
if not patch_plan.edges_to_add:
|
|
80
|
+
return
|
|
81
|
+
self.edge_db_id_translator.translate_to_db_ids(patch_plan=patch_plan)
|
|
82
|
+
unadded_edges = [e for e in patch_plan.edges_to_add if not patch_plan.has_element_been_added(e.identifier)]
|
|
83
|
+
try:
|
|
84
|
+
async for added_element_id_map in self.edge_adder.execute(edges_to_add=unadded_edges):
|
|
85
|
+
patch_plan.added_element_db_id_map.update(added_element_id_map)
|
|
86
|
+
finally:
|
|
87
|
+
# record the added elements so that we do not double-add them if the patch is run again
|
|
88
|
+
self.plan_writer.write_added_db_id_map(
|
|
89
|
+
patch_plan_directory=patch_plan_directory, db_id_map=patch_plan.added_element_db_id_map
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def _apply_vertices_to_delete(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
93
|
+
if not patch_plan.vertices_to_delete:
|
|
94
|
+
return
|
|
95
|
+
try:
|
|
96
|
+
async for deleted_ids in self.vertex_deleter.execute(vertices_to_delete=patch_plan.vertices_to_delete):
|
|
97
|
+
patch_plan.deleted_db_ids |= deleted_ids
|
|
98
|
+
finally:
|
|
99
|
+
# record the deleted elements so that we know what to add if the patch is reverted
|
|
100
|
+
self.plan_writer.write_deleted_db_ids(
|
|
101
|
+
patch_plan_directory=patch_plan_directory, deleted_ids=patch_plan.deleted_db_ids
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
async def _apply_edges_to_delete(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
105
|
+
if not patch_plan.edges_to_delete:
|
|
106
|
+
return
|
|
107
|
+
try:
|
|
108
|
+
async for deleted_ids in self.edge_deleter.execute(edges_to_delete=patch_plan.edges_to_delete):
|
|
109
|
+
patch_plan.deleted_db_ids |= deleted_ids
|
|
110
|
+
finally:
|
|
111
|
+
# record the deleted elements so that we know what to add if the patch is reverted
|
|
112
|
+
self.plan_writer.write_deleted_db_ids(
|
|
113
|
+
patch_plan_directory=patch_plan_directory, deleted_ids=patch_plan.deleted_db_ids
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async def revert(self, patch_plan_directory: Path) -> PatchPlan:
|
|
117
|
+
"""Invert the PatchPlan to create the complement of every added/updated/deleted element and undo them"""
|
|
118
|
+
patch_plan = self.plan_reader.read(patch_plan_directory)
|
|
119
|
+
await self._revert_deleted_vertices(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
120
|
+
await self._revert_deleted_edges(
|
|
121
|
+
patch_plan=patch_plan,
|
|
122
|
+
patch_plan_directory=patch_plan_directory,
|
|
123
|
+
)
|
|
124
|
+
await self._revert_added_edges(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
125
|
+
await self._revert_added_vertices(patch_plan=patch_plan, patch_plan_directory=patch_plan_directory)
|
|
126
|
+
vertices_to_update = [
|
|
127
|
+
VertexToUpdate(
|
|
128
|
+
db_id=vertex_update_to_revert.db_id,
|
|
129
|
+
before_props=vertex_update_to_revert.after_props,
|
|
130
|
+
after_props=vertex_update_to_revert.before_props,
|
|
131
|
+
)
|
|
132
|
+
for vertex_update_to_revert in patch_plan.vertices_to_update
|
|
133
|
+
]
|
|
134
|
+
if vertices_to_update:
|
|
135
|
+
await self.vertex_updater.execute(vertices_to_update=vertices_to_update)
|
|
136
|
+
|
|
137
|
+
edges_to_update = [
|
|
138
|
+
EdgeToUpdate(
|
|
139
|
+
db_id=edge_update_to_revert.db_id,
|
|
140
|
+
before_props=edge_update_to_revert.after_props,
|
|
141
|
+
after_props=edge_update_to_revert.before_props,
|
|
142
|
+
)
|
|
143
|
+
for edge_update_to_revert in patch_plan.edges_to_update
|
|
144
|
+
]
|
|
145
|
+
if edges_to_update:
|
|
146
|
+
await self.edge_updater.execute(edges_to_update=edges_to_update)
|
|
147
|
+
if patch_plan.reverted_deleted_db_id_map:
|
|
148
|
+
patch_plan.reverted_deleted_db_id_map = {}
|
|
149
|
+
self.plan_writer.write_reverted_deleted_db_id_map(
|
|
150
|
+
patch_plan_directory=patch_plan_directory, db_id_map=patch_plan.reverted_deleted_db_id_map
|
|
151
|
+
)
|
|
152
|
+
return patch_plan
|
|
153
|
+
|
|
154
|
+
async def _revert_added_vertices(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
155
|
+
vertices_to_delete = [
|
|
156
|
+
VertexToDelete(
|
|
157
|
+
db_id=patch_plan.get_database_id_for_added_element(abstract_id=vertex_add_to_revert.identifier),
|
|
158
|
+
labels=vertex_add_to_revert.labels,
|
|
159
|
+
before_props=vertex_add_to_revert.after_props,
|
|
160
|
+
)
|
|
161
|
+
for vertex_add_to_revert in patch_plan.added_vertices
|
|
162
|
+
]
|
|
163
|
+
if not vertices_to_delete:
|
|
164
|
+
return
|
|
165
|
+
all_deleted_ids: set[str] = set()
|
|
166
|
+
try:
|
|
167
|
+
async for deleted_ids in self.vertex_deleter.execute(vertices_to_delete=vertices_to_delete):
|
|
168
|
+
all_deleted_ids |= deleted_ids
|
|
169
|
+
finally:
|
|
170
|
+
if all_deleted_ids:
|
|
171
|
+
patch_plan.drop_added_db_ids(db_ids_to_drop=all_deleted_ids)
|
|
172
|
+
self.plan_writer.write_added_db_id_map(
|
|
173
|
+
patch_plan_directory=patch_plan_directory, db_id_map=patch_plan.added_element_db_id_map
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def _revert_deleted_vertices(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
177
|
+
vertices_to_add = [
|
|
178
|
+
VertexToAdd(
|
|
179
|
+
labels=vertex_delete_to_revert.labels,
|
|
180
|
+
after_props=vertex_delete_to_revert.before_props,
|
|
181
|
+
identifier=vertex_delete_to_revert.db_id,
|
|
182
|
+
)
|
|
183
|
+
for vertex_delete_to_revert in patch_plan.deleted_vertices
|
|
184
|
+
]
|
|
185
|
+
if not vertices_to_add:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
deleted_to_undeleted_db_id_map: dict[str, str] = {}
|
|
189
|
+
try:
|
|
190
|
+
async for added_db_id_map in self.vertex_adder.execute(vertices_to_add=vertices_to_add):
|
|
191
|
+
deleted_to_undeleted_db_id_map.update(added_db_id_map)
|
|
192
|
+
finally:
|
|
193
|
+
if deleted_to_undeleted_db_id_map:
|
|
194
|
+
patch_plan.drop_deleted_db_ids(db_ids_to_drop=set(deleted_to_undeleted_db_id_map.keys()))
|
|
195
|
+
self.plan_writer.write_deleted_db_ids(
|
|
196
|
+
patch_plan_directory=patch_plan_directory, deleted_ids=patch_plan.deleted_db_ids
|
|
197
|
+
)
|
|
198
|
+
patch_plan.reverted_deleted_db_id_map.update(deleted_to_undeleted_db_id_map)
|
|
199
|
+
self.plan_writer.write_reverted_deleted_db_id_map(
|
|
200
|
+
patch_plan_directory=patch_plan_directory, db_id_map=patch_plan.reverted_deleted_db_id_map
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def _revert_added_edges(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
204
|
+
edges_to_delete = [
|
|
205
|
+
EdgeToDelete(
|
|
206
|
+
db_id=patch_plan.get_database_id_for_added_element(abstract_id=edge_add_to_revert.identifier),
|
|
207
|
+
from_id=edge_add_to_revert.from_id,
|
|
208
|
+
to_id=edge_add_to_revert.to_id,
|
|
209
|
+
edge_type=edge_add_to_revert.edge_type,
|
|
210
|
+
before_props=edge_add_to_revert.after_props,
|
|
211
|
+
)
|
|
212
|
+
for edge_add_to_revert in patch_plan.added_edges
|
|
213
|
+
]
|
|
214
|
+
if not edges_to_delete:
|
|
215
|
+
return
|
|
216
|
+
all_deleted_ids: set[str] = set()
|
|
217
|
+
try:
|
|
218
|
+
async for deleted_ids in self.edge_deleter.execute(edges_to_delete=edges_to_delete):
|
|
219
|
+
all_deleted_ids |= deleted_ids
|
|
220
|
+
finally:
|
|
221
|
+
if all_deleted_ids:
|
|
222
|
+
patch_plan.drop_added_db_ids(db_ids_to_drop=all_deleted_ids)
|
|
223
|
+
self.plan_writer.write_added_db_id_map(
|
|
224
|
+
patch_plan_directory=patch_plan_directory, db_id_map=patch_plan.added_element_db_id_map
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def _revert_deleted_edges(self, patch_plan: PatchPlan, patch_plan_directory: Path) -> None:
|
|
228
|
+
edges_to_add = [
|
|
229
|
+
EdgeToAdd(
|
|
230
|
+
identifier=edge_delete_to_revert.db_id,
|
|
231
|
+
from_id=patch_plan.reverted_deleted_db_id_map.get(
|
|
232
|
+
edge_delete_to_revert.from_id, edge_delete_to_revert.from_id
|
|
233
|
+
),
|
|
234
|
+
to_id=patch_plan.reverted_deleted_db_id_map.get(
|
|
235
|
+
edge_delete_to_revert.to_id, edge_delete_to_revert.to_id
|
|
236
|
+
),
|
|
237
|
+
edge_type=edge_delete_to_revert.edge_type,
|
|
238
|
+
after_props=edge_delete_to_revert.before_props,
|
|
239
|
+
)
|
|
240
|
+
for edge_delete_to_revert in patch_plan.deleted_edges
|
|
241
|
+
]
|
|
242
|
+
if not edges_to_add:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
undeleted_ids: set[str] = set()
|
|
246
|
+
try:
|
|
247
|
+
async for added_db_id_map in self.edge_adder.execute(edges_to_add=edges_to_add):
|
|
248
|
+
undeleted_ids |= set(added_db_id_map.keys())
|
|
249
|
+
finally:
|
|
250
|
+
if undeleted_ids:
|
|
251
|
+
patch_plan.drop_deleted_db_ids(db_ids_to_drop=undeleted_ids)
|
|
252
|
+
self.plan_writer.write_deleted_db_ids(
|
|
253
|
+
patch_plan_directory=patch_plan_directory, deleted_ids=patch_plan.deleted_db_ids
|
|
254
|
+
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from dataclasses import asdict
|
|
3
|
+
from typing import AsyncGenerator
|
|
4
|
+
|
|
5
|
+
from infrahub.core.query import QueryType
|
|
6
|
+
from infrahub.database import InfrahubDatabase
|
|
7
|
+
|
|
8
|
+
from .models import VertexToAdd
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PatchPlanVertexAdder:
|
|
12
|
+
def __init__(self, db: InfrahubDatabase, batch_size_limit: int = 1000) -> None:
|
|
13
|
+
self.db = db
|
|
14
|
+
self.batch_size_limit = batch_size_limit
|
|
15
|
+
|
|
16
|
+
async def _run_add_query(self, labels: list[str], vertices_to_add: list[VertexToAdd]) -> dict[str, str]:
|
|
17
|
+
labels_str = ":".join(labels)
|
|
18
|
+
serial_vertices_to_add: list[dict[str, str | int | bool]] = [asdict(v) for v in vertices_to_add]
|
|
19
|
+
query = """
|
|
20
|
+
UNWIND $vertices_to_add AS vertex_to_add
|
|
21
|
+
CREATE (v:%(labels)s)
|
|
22
|
+
SET v = vertex_to_add.after_props
|
|
23
|
+
RETURN vertex_to_add.identifier AS abstract_id, %(id_func_name)s(v) AS db_id
|
|
24
|
+
""" % {
|
|
25
|
+
"labels": labels_str,
|
|
26
|
+
"id_func_name": self.db.get_id_function_name(),
|
|
27
|
+
}
|
|
28
|
+
# use transaction to make sure we record the results before committing them
|
|
29
|
+
try:
|
|
30
|
+
txn_db = self.db.start_transaction()
|
|
31
|
+
async with txn_db as txn:
|
|
32
|
+
results = await txn.execute_query(
|
|
33
|
+
query=query, params={"vertices_to_add": serial_vertices_to_add}, type=QueryType.WRITE
|
|
34
|
+
)
|
|
35
|
+
abstract_to_concrete_id_map: dict[str, str] = {}
|
|
36
|
+
for result in results:
|
|
37
|
+
abstract_id = result.get("abstract_id")
|
|
38
|
+
concrete_id = result.get("db_id")
|
|
39
|
+
abstract_to_concrete_id_map[abstract_id] = concrete_id
|
|
40
|
+
finally:
|
|
41
|
+
await txn_db.close()
|
|
42
|
+
return abstract_to_concrete_id_map
|
|
43
|
+
|
|
44
|
+
async def execute(self, vertices_to_add: list[VertexToAdd]) -> AsyncGenerator[dict[str, str], None]:
|
|
45
|
+
"""
|
|
46
|
+
Create vertices_to_add on the database.
|
|
47
|
+
Returns a generator that yields dictionaries mapping VertexToAdd.identifier to the database-level ID of the newly created vertex.
|
|
48
|
+
"""
|
|
49
|
+
vertices_map_queue: dict[frozenset[str], list[VertexToAdd]] = defaultdict(list)
|
|
50
|
+
for vertex_to_add in vertices_to_add:
|
|
51
|
+
frozen_labels = frozenset(vertex_to_add.labels)
|
|
52
|
+
vertices_map_queue[frozen_labels].append(vertex_to_add)
|
|
53
|
+
if len(vertices_map_queue[frozen_labels]) > self.batch_size_limit:
|
|
54
|
+
yield await self._run_add_query(
|
|
55
|
+
labels=list(frozen_labels),
|
|
56
|
+
vertices_to_add=vertices_map_queue[frozen_labels],
|
|
57
|
+
)
|
|
58
|
+
vertices_map_queue[frozen_labels] = []
|
|
59
|
+
|
|
60
|
+
for frozen_labels, vertices_group in vertices_map_queue.items():
|
|
61
|
+
yield await self._run_add_query(labels=list(frozen_labels), vertices_to_add=vertices_group)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from infrahub.core.query import QueryType
|
|
4
|
+
from infrahub.database import InfrahubDatabase
|
|
5
|
+
|
|
6
|
+
from .models import VertexToDelete
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PatchPlanVertexDeleter:
|
|
10
|
+
def __init__(self, db: InfrahubDatabase, batch_size_limit: int = 1000) -> None:
|
|
11
|
+
self.db = db
|
|
12
|
+
self.batch_size_limit = batch_size_limit
|
|
13
|
+
|
|
14
|
+
async def _run_delete_query(self, ids_to_delete: list[str]) -> set[str]:
|
|
15
|
+
query = """
|
|
16
|
+
MATCH (n)
|
|
17
|
+
WHERE %(id_func_name)s(n) IN $ids_to_delete
|
|
18
|
+
DETACH DELETE n
|
|
19
|
+
RETURN %(id_func_name)s(n) AS deleted_id
|
|
20
|
+
""" % {"id_func_name": self.db.get_id_function_name()}
|
|
21
|
+
results = await self.db.execute_query(
|
|
22
|
+
query=query, params={"ids_to_delete": ids_to_delete}, type=QueryType.WRITE
|
|
23
|
+
)
|
|
24
|
+
deleted_ids: set[str] = set()
|
|
25
|
+
for result in results:
|
|
26
|
+
deleted_id = result.get("deleted_id")
|
|
27
|
+
deleted_ids.add(deleted_id)
|
|
28
|
+
return deleted_ids
|
|
29
|
+
|
|
30
|
+
async def execute(self, vertices_to_delete: list[VertexToDelete]) -> AsyncGenerator[set[str], None]:
|
|
31
|
+
for i in range(0, len(vertices_to_delete), self.batch_size_limit):
|
|
32
|
+
ids_to_delete = [v.db_id for v in vertices_to_delete[i : i + self.batch_size_limit]]
|
|
33
|
+
yield await self._run_delete_query(ids_to_delete=ids_to_delete)
|