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.
Files changed (45) hide show
  1. infrahub/cli/db.py +2 -0
  2. infrahub/cli/patch.py +153 -0
  3. infrahub/computed_attribute/models.py +81 -1
  4. infrahub/computed_attribute/tasks.py +34 -53
  5. infrahub/core/node/__init__.py +4 -1
  6. infrahub/core/query/ipam.py +7 -5
  7. infrahub/patch/__init__.py +0 -0
  8. infrahub/patch/constants.py +13 -0
  9. infrahub/patch/edge_adder.py +64 -0
  10. infrahub/patch/edge_deleter.py +33 -0
  11. infrahub/patch/edge_updater.py +28 -0
  12. infrahub/patch/models.py +98 -0
  13. infrahub/patch/plan_reader.py +107 -0
  14. infrahub/patch/plan_writer.py +92 -0
  15. infrahub/patch/queries/__init__.py +0 -0
  16. infrahub/patch/queries/base.py +17 -0
  17. infrahub/patch/runner.py +254 -0
  18. infrahub/patch/vertex_adder.py +61 -0
  19. infrahub/patch/vertex_deleter.py +33 -0
  20. infrahub/patch/vertex_updater.py +28 -0
  21. infrahub_sdk/checks.py +1 -1
  22. infrahub_sdk/ctl/cli_commands.py +2 -2
  23. infrahub_sdk/ctl/menu.py +56 -13
  24. infrahub_sdk/ctl/object.py +55 -5
  25. infrahub_sdk/ctl/utils.py +22 -1
  26. infrahub_sdk/exceptions.py +19 -1
  27. infrahub_sdk/node.py +42 -26
  28. infrahub_sdk/protocols_generator/__init__.py +0 -0
  29. infrahub_sdk/protocols_generator/constants.py +28 -0
  30. infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
  31. infrahub_sdk/protocols_generator/template.j2 +114 -0
  32. infrahub_sdk/schema/__init__.py +110 -74
  33. infrahub_sdk/schema/main.py +36 -2
  34. infrahub_sdk/schema/repository.py +2 -0
  35. infrahub_sdk/spec/menu.py +3 -3
  36. infrahub_sdk/spec/object.py +522 -41
  37. infrahub_sdk/testing/docker.py +4 -5
  38. infrahub_sdk/testing/schemas/animal.py +7 -0
  39. infrahub_sdk/yaml.py +63 -7
  40. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/METADATA +1 -1
  41. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/RECORD +44 -27
  42. infrahub_sdk/ctl/constants.py +0 -115
  43. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/LICENSE.txt +0 -0
  44. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/WHEEL +0 -0
  45. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.7.dist-info}/entry_points.txt +0 -0
@@ -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: ...
@@ -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)