infrahub-server 1.2.6__py3-none-any.whl → 1.2.8__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 (86) hide show
  1. infrahub/api/transformation.py +1 -0
  2. infrahub/artifacts/models.py +4 -0
  3. infrahub/cli/db.py +3 -1
  4. infrahub/cli/patch.py +153 -0
  5. infrahub/computed_attribute/models.py +81 -1
  6. infrahub/computed_attribute/tasks.py +35 -53
  7. infrahub/config.py +2 -1
  8. infrahub/constants/__init__.py +0 -0
  9. infrahub/core/constants/__init__.py +1 -0
  10. infrahub/core/graph/index.py +3 -1
  11. infrahub/core/manager.py +16 -5
  12. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +7 -8
  13. infrahub/core/node/__init__.py +4 -1
  14. infrahub/core/protocols.py +1 -0
  15. infrahub/core/query/ipam.py +7 -5
  16. infrahub/core/query/node.py +96 -29
  17. infrahub/core/schema/definitions/core/builtin.py +2 -4
  18. infrahub/core/schema/definitions/core/transform.py +1 -0
  19. infrahub/core/validators/aggregated_checker.py +2 -2
  20. infrahub/core/validators/uniqueness/query.py +8 -3
  21. infrahub/database/__init__.py +2 -10
  22. infrahub/database/index.py +1 -1
  23. infrahub/database/memgraph.py +2 -1
  24. infrahub/database/neo4j.py +1 -1
  25. infrahub/git/integrator.py +27 -3
  26. infrahub/git/models.py +4 -0
  27. infrahub/git/tasks.py +3 -0
  28. infrahub/git_credential/helper.py +2 -2
  29. infrahub/message_bus/operations/requests/proposed_change.py +6 -0
  30. infrahub/message_bus/types.py +3 -0
  31. infrahub/patch/__init__.py +0 -0
  32. infrahub/patch/constants.py +13 -0
  33. infrahub/patch/edge_adder.py +64 -0
  34. infrahub/patch/edge_deleter.py +33 -0
  35. infrahub/patch/edge_updater.py +28 -0
  36. infrahub/patch/models.py +98 -0
  37. infrahub/patch/plan_reader.py +107 -0
  38. infrahub/patch/plan_writer.py +92 -0
  39. infrahub/patch/queries/__init__.py +0 -0
  40. infrahub/patch/queries/base.py +17 -0
  41. infrahub/patch/queries/consolidate_duplicated_nodes.py +109 -0
  42. infrahub/patch/queries/delete_duplicated_edges.py +138 -0
  43. infrahub/patch/runner.py +254 -0
  44. infrahub/patch/vertex_adder.py +61 -0
  45. infrahub/patch/vertex_deleter.py +33 -0
  46. infrahub/patch/vertex_updater.py +28 -0
  47. infrahub/proposed_change/tasks.py +1 -0
  48. infrahub/server.py +3 -1
  49. infrahub/transformations/models.py +3 -0
  50. infrahub/transformations/tasks.py +1 -0
  51. infrahub/webhook/models.py +3 -0
  52. infrahub_sdk/checks.py +1 -1
  53. infrahub_sdk/client.py +4 -4
  54. infrahub_sdk/config.py +17 -0
  55. infrahub_sdk/ctl/cli_commands.py +9 -3
  56. infrahub_sdk/ctl/generator.py +2 -2
  57. infrahub_sdk/ctl/menu.py +56 -13
  58. infrahub_sdk/ctl/object.py +55 -5
  59. infrahub_sdk/ctl/utils.py +22 -1
  60. infrahub_sdk/exceptions.py +19 -1
  61. infrahub_sdk/generator.py +12 -66
  62. infrahub_sdk/node.py +42 -26
  63. infrahub_sdk/operation.py +80 -0
  64. infrahub_sdk/protocols.py +12 -0
  65. infrahub_sdk/protocols_generator/__init__.py +0 -0
  66. infrahub_sdk/protocols_generator/constants.py +28 -0
  67. infrahub_sdk/{code_generator.py → protocols_generator/generator.py} +47 -34
  68. infrahub_sdk/protocols_generator/template.j2 +114 -0
  69. infrahub_sdk/recorder.py +3 -0
  70. infrahub_sdk/schema/__init__.py +110 -74
  71. infrahub_sdk/schema/main.py +36 -2
  72. infrahub_sdk/schema/repository.py +6 -0
  73. infrahub_sdk/spec/menu.py +3 -3
  74. infrahub_sdk/spec/object.py +522 -41
  75. infrahub_sdk/testing/docker.py +4 -5
  76. infrahub_sdk/testing/schemas/animal.py +7 -0
  77. infrahub_sdk/transforms.py +15 -27
  78. infrahub_sdk/yaml.py +63 -7
  79. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/METADATA +2 -2
  80. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/RECORD +85 -64
  81. infrahub_testcontainers/docker-compose.test.yml +2 -0
  82. infrahub_sdk/ctl/constants.py +0 -115
  83. /infrahub/{database/constants.py → constants/database.py} +0 -0
  84. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/LICENSE.txt +0 -0
  85. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/WHEEL +0 -0
  86. {infrahub_server-1.2.6.dist-info → infrahub_server-1.2.8.dist-info}/entry_points.txt +0 -0
@@ -88,6 +88,7 @@ async def transform_python(
88
88
  branch=branch_params.branch.name,
89
89
  transform_location=f"{transform.file_path.value}::{transform.class_name.value}",
90
90
  timeout=transform.timeout.value,
91
+ convert_query_response=transform.convert_query_response.value or False,
91
92
  data=data,
92
93
  )
93
94
 
@@ -12,6 +12,10 @@ class CheckArtifactCreate(BaseModel):
12
12
  content_type: str = Field(..., description="Content type of the artifact")
13
13
  transform_type: str = Field(..., description="The type of transform associated with this artifact")
14
14
  transform_location: str = Field(..., description="The transforms location within the repository")
15
+ convert_query_response: bool = Field(
16
+ default=False,
17
+ description="Indicate if the query response should be converted to InfrahubNode objects for Python transforms",
18
+ )
15
19
  repository_id: str = Field(..., description="The unique ID of the Repository")
16
20
  repository_name: str = Field(..., description="The name of the Repository")
17
21
  repository_kind: str = Field(..., description="The kind of the Repository")
infrahub/cli/db.py CHANGED
@@ -54,12 +54,14 @@ from infrahub.services.adapters.message_bus.local import BusSimulator
54
54
  from infrahub.services.adapters.workflow.local import WorkflowLocalExecution
55
55
 
56
56
  from .constants import ERROR_BADGE, FAILED_BADGE, SUCCESS_BADGE
57
+ from .patch import patch_app
57
58
 
58
59
  if TYPE_CHECKING:
59
60
  from infrahub.cli.context import CliContext
60
61
  from infrahub.database import InfrahubDatabase
61
62
 
62
63
  app = AsyncTyper()
64
+ app.add_typer(patch_app, name="patch")
63
65
 
64
66
  PERMISSIONS_AVAILABLE = ["read", "write", "admin"]
65
67
 
@@ -410,7 +412,7 @@ async def update_core_schema(
410
412
  update_db=True,
411
413
  )
412
414
  default_branch.update_schema_hash()
413
- rprint("The Core Schema has been updated")
415
+ rprint("The Core Schema has been updated, make sure to rebase any open branches after the upgrade")
414
416
  if debug:
415
417
  rprint(f"New schema hash: {default_branch.active_schema_hash.main}")
416
418
  await default_branch.save(db=dbt)
infrahub/cli/patch.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import inspect
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import typer
10
+ from infrahub_sdk.async_typer import AsyncTyper
11
+ from rich import print as rprint
12
+
13
+ from infrahub import config
14
+ from infrahub.patch.edge_adder import PatchPlanEdgeAdder
15
+ from infrahub.patch.edge_deleter import PatchPlanEdgeDeleter
16
+ from infrahub.patch.edge_updater import PatchPlanEdgeUpdater
17
+ from infrahub.patch.plan_reader import PatchPlanReader
18
+ from infrahub.patch.plan_writer import PatchPlanWriter
19
+ from infrahub.patch.queries.base import PatchQuery
20
+ from infrahub.patch.runner import (
21
+ PatchPlanEdgeDbIdTranslator,
22
+ PatchRunner,
23
+ )
24
+ from infrahub.patch.vertex_adder import PatchPlanVertexAdder
25
+ from infrahub.patch.vertex_deleter import PatchPlanVertexDeleter
26
+ from infrahub.patch.vertex_updater import PatchPlanVertexUpdater
27
+
28
+ from .constants import ERROR_BADGE, SUCCESS_BADGE
29
+
30
+ if TYPE_CHECKING:
31
+ from infrahub.cli.context import CliContext
32
+ from infrahub.database import InfrahubDatabase
33
+
34
+
35
+ patch_app = AsyncTyper(help="Commands for planning, applying, and reverting database patches")
36
+
37
+
38
+ def get_patch_runner(db: InfrahubDatabase) -> PatchRunner:
39
+ return PatchRunner(
40
+ plan_writer=PatchPlanWriter(),
41
+ plan_reader=PatchPlanReader(),
42
+ edge_db_id_translator=PatchPlanEdgeDbIdTranslator(),
43
+ vertex_adder=PatchPlanVertexAdder(db=db),
44
+ vertex_deleter=PatchPlanVertexDeleter(db=db),
45
+ vertex_updater=PatchPlanVertexUpdater(db=db),
46
+ edge_adder=PatchPlanEdgeAdder(db=db),
47
+ edge_deleter=PatchPlanEdgeDeleter(db=db),
48
+ edge_updater=PatchPlanEdgeUpdater(db=db),
49
+ )
50
+
51
+
52
+ @patch_app.command(name="plan")
53
+ async def plan_patch_cmd(
54
+ ctx: typer.Context,
55
+ patch_path: str = typer.Argument(
56
+ help="Path to the file containing the PatchQuery instance to run. Use Python-style dot paths, such as infrahub.cli.patch.queries.base"
57
+ ),
58
+ patch_plans_dir: Path = typer.Option(Path("infrahub-patches"), help="Path to patch plans directory"), # noqa: B008
59
+ apply: bool = typer.Option(False, help="Apply the patch immediately after creating it"),
60
+ config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
61
+ ) -> None:
62
+ """Create a plan for a given patch and save it in the patch plans directory to be applied/reverted"""
63
+ logging.getLogger("infrahub").setLevel(logging.WARNING)
64
+ logging.getLogger("neo4j").setLevel(logging.ERROR)
65
+ logging.getLogger("prefect").setLevel(logging.ERROR)
66
+
67
+ patch_module = importlib.import_module(patch_path)
68
+ patch_query_class = None
69
+ patch_query_class_count = 0
70
+ for _, cls in inspect.getmembers(patch_module, inspect.isclass):
71
+ if issubclass(cls, PatchQuery) and cls is not PatchQuery:
72
+ patch_query_class = cls
73
+ patch_query_class_count += 1
74
+
75
+ patch_query_path = f"{PatchQuery.__module__}.{PatchQuery.__name__}"
76
+ if patch_query_class is None:
77
+ rprint(f"{ERROR_BADGE} No subclass of {patch_query_path} found in {patch_path}")
78
+ raise typer.Exit(1)
79
+ if patch_query_class_count > 1:
80
+ rprint(
81
+ f"{ERROR_BADGE} Multiple subclasses of {patch_query_path} found in {patch_path}. Please only define one per file."
82
+ )
83
+ raise typer.Exit(1)
84
+
85
+ config.load_and_exit(config_file_name=config_file)
86
+
87
+ context: CliContext = ctx.obj
88
+ dbdriver = await context.init_db(retry=1)
89
+
90
+ patch_query_instance = patch_query_class(db=dbdriver)
91
+ async with dbdriver.start_session() as db:
92
+ patch_runner = get_patch_runner(db=db)
93
+ patch_plan_dir = await patch_runner.prepare_plan(patch_query_instance, directory=Path(patch_plans_dir))
94
+ rprint(f"{SUCCESS_BADGE} Patch plan created at {patch_plan_dir}")
95
+ if apply:
96
+ await patch_runner.apply(patch_plan_directory=patch_plan_dir)
97
+ rprint(f"{SUCCESS_BADGE} Patch plan successfully applied")
98
+
99
+ await dbdriver.close()
100
+
101
+
102
+ @patch_app.command(name="apply")
103
+ async def apply_patch_cmd(
104
+ ctx: typer.Context,
105
+ patch_plan_dir: Path = typer.Argument(help="Path to the directory containing a patch plan"),
106
+ config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
107
+ ) -> None:
108
+ """Apply a given patch plan"""
109
+ logging.getLogger("infrahub").setLevel(logging.WARNING)
110
+ logging.getLogger("neo4j").setLevel(logging.ERROR)
111
+ logging.getLogger("prefect").setLevel(logging.ERROR)
112
+
113
+ config.load_and_exit(config_file_name=config_file)
114
+
115
+ context: CliContext = ctx.obj
116
+ dbdriver = await context.init_db(retry=1)
117
+
118
+ if not patch_plan_dir.exists() or not patch_plan_dir.is_dir():
119
+ rprint(f"{ERROR_BADGE} patch_plan_dir must be an existing directory")
120
+ raise typer.Exit(1)
121
+
122
+ async with dbdriver.start_session() as db:
123
+ patch_runner = get_patch_runner(db=db)
124
+ await patch_runner.apply(patch_plan_directory=patch_plan_dir)
125
+ rprint(f"{SUCCESS_BADGE} Patch plan successfully applied")
126
+
127
+ await dbdriver.close()
128
+
129
+
130
+ @patch_app.command(name="revert")
131
+ async def revert_patch_cmd(
132
+ ctx: typer.Context,
133
+ patch_plan_dir: Path = typer.Argument(help="Path to the directory containing a patch plan"),
134
+ config_file: str = typer.Argument("infrahub.toml", envvar="INFRAHUB_CONFIG"),
135
+ ) -> None:
136
+ """Revert a given patch plan"""
137
+ logging.getLogger("infrahub").setLevel(logging.WARNING)
138
+ logging.getLogger("neo4j").setLevel(logging.ERROR)
139
+ logging.getLogger("prefect").setLevel(logging.ERROR)
140
+ config.load_and_exit(config_file_name=config_file)
141
+
142
+ context: CliContext = ctx.obj
143
+ db = await context.init_db(retry=1)
144
+
145
+ if not patch_plan_dir.exists() or not patch_plan_dir.is_dir():
146
+ rprint(f"{ERROR_BADGE} patch_plan_dir must be an existing directory")
147
+ raise typer.Exit(1)
148
+
149
+ patch_runner = get_patch_runner(db=db)
150
+ await patch_runner.revert(patch_plan_directory=patch_plan_dir)
151
+ rprint(f"{SUCCESS_BADGE} Patch plan successfully reverted")
152
+
153
+ await db.close()
@@ -2,13 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
4
  from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
+ from infrahub_sdk.graphql import Query
7
8
  from prefect.events.schemas.automations import Automation # noqa: TC002
8
9
  from pydantic import BaseModel, ConfigDict, Field, computed_field
9
10
  from typing_extensions import Self
10
11
 
11
12
  from infrahub.core import registry
13
+ from infrahub.core.constants import RelationshipCardinality
14
+ from infrahub.core.schema import AttributeSchema, NodeSchema # noqa: TC001
12
15
  from infrahub.core.schema.schema_branch_computed import ( # noqa: TC001
13
16
  ComputedAttributeTarget,
14
17
  ComputedAttributeTriggerNode,
@@ -309,3 +312,80 @@ class ComputedAttrPythonQueryTriggerDefinition(TriggerBranchDefinition):
309
312
  )
310
313
 
311
314
  return definition
315
+
316
+
317
+ class ComputedAttrJinja2GraphQLResponse(BaseModel):
318
+ node_id: str
319
+ computed_attribute_value: str | None
320
+ variables: dict[str, Any] = Field(default_factory=dict)
321
+
322
+
323
+ class ComputedAttrJinja2GraphQL(BaseModel):
324
+ node_schema: NodeSchema = Field(..., description="The node kind where the computed attribute is defined")
325
+ attribute_schema: AttributeSchema = Field(..., description="The computed attribute")
326
+ variables: list[str] = Field(..., description="The list of variable names used within the computed attribute")
327
+
328
+ def render_graphql_query(self, query_filter: str, filter_id: str) -> str:
329
+ query_fields = self.query_fields
330
+ query_fields["id"] = None
331
+ query_fields[self.attribute_schema.name] = {"value": None}
332
+ query = Query(
333
+ name="ComputedAttributeFilter",
334
+ query={
335
+ self.node_schema.kind: {
336
+ "@filters": {query_filter: filter_id},
337
+ "edges": {"node": query_fields},
338
+ }
339
+ },
340
+ )
341
+
342
+ return query.render()
343
+
344
+ @property
345
+ def query_fields(self) -> dict[str, Any]:
346
+ output: dict[str, Any] = {}
347
+ for variable in self.variables:
348
+ field_name, remainder = variable.split("__", maxsplit=1)
349
+ if field_name in self.node_schema.attribute_names:
350
+ output[field_name] = {remainder: None}
351
+ elif field_name in self.node_schema.relationship_names:
352
+ related_attribute, related_value = remainder.split("__", maxsplit=1)
353
+ relationship = self.node_schema.get_relationship(name=field_name)
354
+ if relationship.cardinality == RelationshipCardinality.ONE:
355
+ if field_name not in output:
356
+ output[field_name] = {"node": {}}
357
+ output[field_name]["node"][related_attribute] = {related_value: None}
358
+ return output
359
+
360
+ def parse_response(self, response: dict[str, Any]) -> list[ComputedAttrJinja2GraphQLResponse]:
361
+ rendered_response: list[ComputedAttrJinja2GraphQLResponse] = []
362
+ if kind_payload := response.get(self.node_schema.kind):
363
+ edges = kind_payload.get("edges", [])
364
+ for node in edges:
365
+ if node_response := self.to_node_response(node_dict=node):
366
+ rendered_response.append(node_response)
367
+ return rendered_response
368
+
369
+ def to_node_response(self, node_dict: dict[str, Any]) -> ComputedAttrJinja2GraphQLResponse | None:
370
+ if node := node_dict.get("node"):
371
+ node_id = node.get("id")
372
+ else:
373
+ return None
374
+
375
+ computed_attribute = node.get(self.attribute_schema.name, {}).get("value")
376
+ response = ComputedAttrJinja2GraphQLResponse(node_id=node_id, computed_attribute_value=computed_attribute)
377
+ for variable in self.variables:
378
+ field_name, remainder = variable.split("__", maxsplit=1)
379
+ response.variables[variable] = None
380
+ if field_content := node.get(field_name):
381
+ if field_name in self.node_schema.attribute_names:
382
+ response.variables[variable] = field_content.get(remainder)
383
+ elif field_name in self.node_schema.relationship_names:
384
+ relationship = self.node_schema.get_relationship(name=field_name)
385
+ if relationship.cardinality == RelationshipCardinality.ONE:
386
+ related_attribute, related_value = remainder.split("__", maxsplit=1)
387
+ node_content = field_content.get("node") or {}
388
+ related_attribute_content = node_content.get(related_attribute) or {}
389
+ response.variables[variable] = related_attribute_content.get(related_value)
390
+
391
+ return response
@@ -2,10 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from infrahub_sdk.protocols import (
6
- CoreNode, # noqa: TC002
7
- CoreTransformPython,
8
- )
5
+ from infrahub_sdk.protocols import CoreTransformPython
9
6
  from infrahub_sdk.template import Jinja2Template
10
7
  from prefect import flow
11
8
  from prefect.client.orchestration import get_client
@@ -28,9 +25,7 @@ from infrahub.workflows.catalogue import (
28
25
  from infrahub.workflows.utils import add_tags, wait_for_schema_to_converge
29
26
 
30
27
  from .gather import gather_trigger_computed_attribute_jinja2, gather_trigger_computed_attribute_python
31
- from .models import (
32
- PythonTransformTarget,
33
- )
28
+ from .models import ComputedAttrJinja2GraphQL, ComputedAttrJinja2GraphQLResponse, PythonTransformTarget
34
29
 
35
30
  if TYPE_CHECKING:
36
31
  from infrahub.core.schema.computed_attribute import ComputedAttribute
@@ -118,6 +113,7 @@ async def process_transform(
118
113
  location=f"{transform.file_path.value}::{transform.class_name.value}",
119
114
  data=data,
120
115
  client=service.client,
116
+ convert_query_response=transform.convert_query_response.value,
121
117
  ) # type: ignore[misc]
122
118
 
123
119
  await service.client.execute_graphql(
@@ -167,49 +163,33 @@ async def trigger_update_python_computed_attributes(
167
163
  flow_run_name="Update value for computed attribute {attribute_name}",
168
164
  )
169
165
  async def update_computed_attribute_value_jinja2(
170
- branch_name: str, obj: CoreNode, attribute_name: str, template_value: str, service: InfrahubServices
166
+ branch_name: str,
167
+ obj: ComputedAttrJinja2GraphQLResponse,
168
+ node_kind: str,
169
+ attribute_name: str,
170
+ template: Jinja2Template,
171
+ service: InfrahubServices,
171
172
  ) -> None:
172
173
  log = get_run_logger()
173
174
 
174
- await add_tags(branches=[branch_name], nodes=[obj.id], db_change=True)
175
-
176
- jinja_template = Jinja2Template(template=template_value)
177
- variables = {}
178
- for variable in jinja_template.get_variables():
179
- components = variable.split("__")
180
- if len(components) == 2:
181
- property_name = components[0]
182
- property_value = components[1]
183
- attribute_property = getattr(obj, property_name)
184
- variables[variable] = getattr(attribute_property, property_value)
185
- elif len(components) == 3:
186
- relationship_name = components[0]
187
- property_name = components[1]
188
- property_value = components[2]
189
- relationship = getattr(obj, relationship_name)
190
- try:
191
- attribute_property = getattr(relationship.peer, property_name)
192
- variables[variable] = getattr(attribute_property, property_value)
193
- except ValueError:
194
- variables[variable] = ""
195
-
196
- value = await jinja_template.render(variables=variables)
197
- existing_value = getattr(obj, attribute_name).value
198
- if value == existing_value:
175
+ await add_tags(branches=[branch_name], nodes=[obj.node_id], db_change=True)
176
+
177
+ value = await template.render(variables=obj.variables)
178
+ if value == obj.computed_attribute_value:
199
179
  log.debug(f"Ignoring to update {obj} with existing value on {attribute_name}={value}")
200
180
  return
201
181
 
202
182
  await service.client.execute_graphql(
203
183
  query=UPDATE_ATTRIBUTE,
204
184
  variables={
205
- "id": obj.id,
206
- "kind": obj.get_kind(),
185
+ "id": obj.node_id,
186
+ "kind": node_kind,
207
187
  "attribute": attribute_name,
208
188
  "value": value,
209
189
  },
210
190
  branch_name=branch_name,
211
191
  )
212
- log.info(f"Updating computed attribute {obj.get_kind()}.{attribute_name}='{value}' ({obj.id})")
192
+ log.info(f"Updating computed attribute {node_kind}.{attribute_name}='{value}' ({obj.node_id})")
213
193
 
214
194
 
215
195
  @flow(
@@ -235,41 +215,43 @@ async def process_jinja2(
235
215
  branch_name if branch_name in registry.get_altered_schema_branches() else registry.default_branch
236
216
  )
237
217
  schema_branch = registry.schema.get_schema_branch(name=target_branch_schema)
238
- await service.client.schema.all(branch=branch_name, refresh=True, schema_hash=schema_branch.get_hash())
239
-
218
+ node_schema = schema_branch.get_node(name=computed_attribute_kind, duplicate=False)
240
219
  computed_macros = [
241
220
  attrib
242
221
  for attrib in schema_branch.computed_attributes.get_impacted_jinja2_targets(kind=node_kind, updates=updates)
243
222
  if attrib.kind == computed_attribute_kind and attrib.attribute.name == computed_attribute_name
244
223
  ]
245
224
  for computed_macro in computed_macros:
246
- found: list[CoreNode] = []
225
+ found: list[ComputedAttrJinja2GraphQLResponse] = []
226
+ template_string = "n/a"
227
+ if computed_macro.attribute.computed_attribute and computed_macro.attribute.computed_attribute.jinja2_template:
228
+ template_string = computed_macro.attribute.computed_attribute.jinja2_template
229
+
230
+ jinja_template = Jinja2Template(template=template_string)
231
+ variables = jinja_template.get_variables()
232
+
233
+ attribute_graphql = ComputedAttrJinja2GraphQL(
234
+ node_schema=node_schema, attribute_schema=computed_macro.attribute, variables=variables
235
+ )
236
+
247
237
  for id_filter in computed_macro.node_filters:
248
- filters = {id_filter: object_id}
249
- nodes: list[CoreNode] = await service.client.filters(
250
- kind=computed_macro.kind,
251
- branch=branch_name,
252
- prefetch_relationships=True,
253
- populate_store=True,
254
- **filters,
255
- )
256
- found.extend(nodes)
238
+ query = attribute_graphql.render_graphql_query(query_filter=id_filter, filter_id=object_id)
239
+ response = await service.client.execute_graphql(query=query, branch_name=branch_name)
240
+ output = attribute_graphql.parse_response(response=response)
241
+ found.extend(output)
257
242
 
258
243
  if not found:
259
244
  log.debug("No nodes found that requires updates")
260
245
 
261
- template_string = "n/a"
262
- if computed_macro.attribute.computed_attribute and computed_macro.attribute.computed_attribute.jinja2_template:
263
- template_string = computed_macro.attribute.computed_attribute.jinja2_template
264
-
265
246
  batch = await service.client.create_batch()
266
247
  for node in found:
267
248
  batch.add(
268
249
  task=update_computed_attribute_value_jinja2,
269
250
  branch_name=branch_name,
270
251
  obj=node,
252
+ node_kind=node_schema.kind,
271
253
  attribute_name=computed_macro.attribute.name,
272
- template_value=template_string,
254
+ template=jinja_template,
273
255
  service=service,
274
256
  )
275
257
 
infrahub/config.py CHANGED
@@ -23,7 +23,7 @@ from pydantic import (
23
23
  from pydantic_settings import BaseSettings, SettingsConfigDict
24
24
  from typing_extensions import Self
25
25
 
26
- from infrahub.database.constants import DatabaseType
26
+ from infrahub.constants.database import DatabaseType
27
27
  from infrahub.exceptions import InitializationError, ProcessingError
28
28
 
29
29
  if TYPE_CHECKING:
@@ -629,6 +629,7 @@ class AnalyticsSettings(BaseSettings):
629
629
  class ExperimentalFeaturesSettings(BaseSettings):
630
630
  model_config = SettingsConfigDict(env_prefix="INFRAHUB_EXPERIMENTAL_")
631
631
  graphql_enums: bool = False
632
+ value_db_index: bool = False
632
633
 
633
634
 
634
635
  class SecuritySettings(BaseSettings):
File without changes
@@ -150,6 +150,7 @@ class ContentType(InfrahubStringEnum):
150
150
  APPLICATION_JSON = "application/json"
151
151
  APPLICATION_YAML = "application/yaml"
152
152
  APPLICATION_XML = "application/xml"
153
+ APPLICATION_HCL = "application/hcl"
153
154
  TEXT_PLAIN = "text/plain"
154
155
  TEXT_MARKDOWN = "text/markdown"
155
156
  TEXT_CSV = "text/csv"
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from infrahub.database.constants import IndexType
3
+ from infrahub.constants.database import IndexType
4
4
  from infrahub.database.index import IndexItem
5
5
 
6
6
  node_indexes: list[IndexItem] = [
@@ -17,6 +17,8 @@ node_indexes: list[IndexItem] = [
17
17
  IndexItem(name="diff_node_uuid", label="DiffNode", properties=["uuid"], type=IndexType.TEXT),
18
18
  ]
19
19
 
20
+ attr_value_index = IndexItem(name="attr_value", label="AttributeValue", properties=["value"], type=IndexType.RANGE)
21
+
20
22
  rel_indexes: list[IndexItem] = [
21
23
  IndexItem(name="attr_from", label="HAS_ATTRIBUTE", properties=["from"], type=IndexType.RANGE),
22
24
  IndexItem(name="attr_branch", label="HAS_ATTRIBUTE", properties=["branch"], type=IndexType.RANGE),
infrahub/core/manager.py CHANGED
@@ -1229,20 +1229,31 @@ class NodeManager:
1229
1229
  if not prefetch_relationships and not fields:
1230
1230
  return
1231
1231
  cardinality_one_identifiers_by_kind: dict[str, dict[str, RelationshipDirection]] | None = None
1232
- all_identifiers: list[str] | None = None
1232
+ outbound_identifiers: set[str] | None = None
1233
+ inbound_identifiers: set[str] | None = None
1234
+ bidirectional_identifiers: set[str] | None = None
1233
1235
  if not prefetch_relationships:
1234
1236
  cardinality_one_identifiers_by_kind = _get_cardinality_one_identifiers_by_kind(
1235
1237
  nodes=nodes_by_id.values(), fields=fields or {}
1236
1238
  )
1237
- all_identifiers_set: set[str] = set()
1239
+ outbound_identifiers = set()
1240
+ inbound_identifiers = set()
1241
+ bidirectional_identifiers = set()
1238
1242
  for identifier_direction_map in cardinality_one_identifiers_by_kind.values():
1239
- all_identifiers_set.update(identifier_direction_map.keys())
1240
- all_identifiers = list(all_identifiers_set)
1243
+ for identifier, direction in identifier_direction_map.items():
1244
+ if direction is RelationshipDirection.OUTBOUND:
1245
+ outbound_identifiers.add(identifier)
1246
+ elif direction is RelationshipDirection.INBOUND:
1247
+ inbound_identifiers.add(identifier)
1248
+ elif direction is RelationshipDirection.BIDIR:
1249
+ bidirectional_identifiers.add(identifier)
1241
1250
 
1242
1251
  query = await NodeListGetRelationshipsQuery.init(
1243
1252
  db=db,
1244
1253
  ids=list(nodes_by_id.keys()),
1245
- relationship_identifiers=all_identifiers,
1254
+ outbound_identifiers=None if outbound_identifiers is None else list(outbound_identifiers),
1255
+ inbound_identifiers=None if inbound_identifiers is None else list(inbound_identifiers),
1256
+ bidirectional_identifiers=None if bidirectional_identifiers is None else list(bidirectional_identifiers),
1246
1257
  branch=branch,
1247
1258
  at=at,
1248
1259
  branch_agnostic=branch_agnostic,
@@ -2,10 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Sequence
4
4
 
5
+ from infrahub.constants.database import IndexType
5
6
  from infrahub.core.migrations.shared import MigrationResult
6
7
  from infrahub.core.query import Query # noqa: TC001
7
8
  from infrahub.database import DatabaseType
8
- from infrahub.database.constants import IndexType
9
9
  from infrahub.database.index import IndexItem
10
10
 
11
11
  from ..shared import GraphMigration
@@ -29,13 +29,12 @@ class Migration014(GraphMigration):
29
29
  if db.db_type != DatabaseType.NEO4J:
30
30
  return result
31
31
 
32
- async with db.start_transaction() as ts:
33
- try:
34
- ts.manager.index.init(nodes=[INDEX_TO_DELETE], rels=[])
35
- await ts.manager.index.drop()
36
- except Exception as exc:
37
- result.errors.append(str(exc))
38
- return result
32
+ try:
33
+ db.manager.index.init(nodes=[INDEX_TO_DELETE], rels=[])
34
+ await db.manager.index.drop()
35
+ except Exception as exc:
36
+ result.errors.append(str(exc))
37
+ return result
39
38
 
40
39
  return result
41
40
 
@@ -29,6 +29,7 @@ from infrahub.types import ATTRIBUTE_TYPES
29
29
 
30
30
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
31
31
  from ...graphql.models import OrderModel
32
+ from ...log import get_logger
32
33
  from ..query.relationship import RelationshipDeleteAllQuery
33
34
  from ..relationship import RelationshipManager
34
35
  from ..utils import update_relationships_to
@@ -53,6 +54,8 @@ SchemaProtocol = TypeVar("SchemaProtocol")
53
54
  # -
54
55
  # ---------------------------------------------------------------------------------------
55
56
 
57
+ log = get_logger()
58
+
56
59
 
57
60
  class Node(BaseNode, metaclass=BaseNodeMeta):
58
61
  @classmethod
@@ -348,7 +351,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
348
351
  fields.pop("updated_at")
349
352
  for field_name in fields.keys():
350
353
  if field_name not in self._schema.valid_input_names:
351
- errors.append(ValidationError({field_name: f"{field_name} is not a valid input for {self.get_kind()}"}))
354
+ log.error(f"{field_name} is not a valid input for {self.get_kind()}")
352
355
 
353
356
  # Backfill fields with the ones from the template if there's one
354
357
  await self.handle_object_template(fields=fields, db=db, errors=errors)
@@ -478,6 +478,7 @@ class CoreTransformJinja2(CoreTransformation):
478
478
  class CoreTransformPython(CoreTransformation):
479
479
  file_path: String
480
480
  class_name: String
481
+ convert_query_response: BooleanOptional
481
482
 
482
483
 
483
484
  class CoreUserValidator(CoreValidator):
@@ -367,9 +367,11 @@ class IPPrefixReconcileQuery(Query):
367
367
  possible_prefix = tmp_prefix.ljust(self.ip_value.max_prefixlen, "0")
368
368
  if possible_prefix not in possible_prefix_map:
369
369
  possible_prefix_map[possible_prefix] = max_prefix_len
370
- self.params["possible_prefix_and_length_list"] = [
371
- [possible_prefix, max_length] for possible_prefix, max_length in possible_prefix_map.items()
372
- ]
370
+ self.params["possible_prefix_and_length_list"] = []
371
+ self.params["possible_prefix_list"] = []
372
+ for possible_prefix, max_length in possible_prefix_map.items():
373
+ self.params["possible_prefix_and_length_list"].append([possible_prefix, max_length])
374
+ self.params["possible_prefix_list"].append(possible_prefix)
373
375
 
374
376
  namespace_query = """
375
377
  // ------------------
@@ -386,8 +388,7 @@ class IPPrefixReconcileQuery(Query):
386
388
  // ------------------
387
389
  // Get IP Prefix node by UUID
388
390
  // ------------------
389
- MATCH (ip_node {uuid: $node_uuid})
390
- WHERE "%(ip_kind)s" IN labels(ip_node)
391
+ MATCH (ip_node:%(ip_kind)s {uuid: $node_uuid})
391
392
  """ % {
392
393
  "ip_kind": InfrahubKind.IPADDRESS
393
394
  if isinstance(self.ip_value, IPAddressType)
@@ -487,6 +488,7 @@ class IPPrefixReconcileQuery(Query):
487
488
  -[hvr:HAS_VALUE]->(av:%(ip_prefix_attribute_kind)s)
488
489
  WHERE all(r IN relationships(parent_path) WHERE (%(branch_filter)s))
489
490
  AND av.version = $ip_version
491
+ AND av.binary_address IN $possible_prefix_list
490
492
  AND any(prefix_and_length IN $possible_prefix_and_length_list WHERE av.binary_address = prefix_and_length[0] AND av.prefixlen <= prefix_and_length[1])
491
493
  WITH
492
494
  maybe_new_parent,