infrahub-server 1.6.0b0__py3-none-any.whl → 1.6.1__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 (88) hide show
  1. infrahub/api/oauth2.py +33 -6
  2. infrahub/api/oidc.py +36 -6
  3. infrahub/auth.py +11 -0
  4. infrahub/auth_pkce.py +41 -0
  5. infrahub/config.py +9 -3
  6. infrahub/core/branch/models.py +3 -2
  7. infrahub/core/changelog/models.py +2 -2
  8. infrahub/core/constants/__init__.py +1 -0
  9. infrahub/core/graph/__init__.py +1 -1
  10. infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
  11. infrahub/core/manager.py +36 -31
  12. infrahub/core/migrations/graph/__init__.py +2 -0
  13. infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
  14. infrahub/core/models.py +5 -6
  15. infrahub/core/node/__init__.py +16 -13
  16. infrahub/core/node/create.py +36 -8
  17. infrahub/core/node/proposed_change.py +5 -3
  18. infrahub/core/node/standard.py +1 -1
  19. infrahub/core/protocols.py +1 -7
  20. infrahub/core/query/attribute.py +1 -1
  21. infrahub/core/query/node.py +9 -5
  22. infrahub/core/relationship/model.py +21 -4
  23. infrahub/core/schema/generic_schema.py +1 -1
  24. infrahub/core/schema/manager.py +8 -3
  25. infrahub/core/schema/schema_branch.py +35 -16
  26. infrahub/core/validators/attribute/choices.py +2 -2
  27. infrahub/core/validators/determiner.py +3 -6
  28. infrahub/database/__init__.py +1 -1
  29. infrahub/git/base.py +2 -3
  30. infrahub/git/models.py +13 -0
  31. infrahub/git/tasks.py +23 -19
  32. infrahub/git/utils.py +16 -9
  33. infrahub/graphql/app.py +6 -6
  34. infrahub/graphql/loaders/peers.py +6 -0
  35. infrahub/graphql/mutations/action.py +15 -7
  36. infrahub/graphql/mutations/hfid.py +1 -1
  37. infrahub/graphql/mutations/profile.py +8 -1
  38. infrahub/graphql/mutations/repository.py +3 -3
  39. infrahub/graphql/mutations/schema.py +4 -4
  40. infrahub/graphql/mutations/webhook.py +2 -2
  41. infrahub/graphql/queries/resource_manager.py +2 -3
  42. infrahub/graphql/queries/search.py +2 -3
  43. infrahub/graphql/resolvers/ipam.py +20 -0
  44. infrahub/graphql/resolvers/many_relationship.py +12 -11
  45. infrahub/graphql/resolvers/resolver.py +6 -2
  46. infrahub/graphql/resolvers/single_relationship.py +1 -11
  47. infrahub/log.py +1 -1
  48. infrahub/message_bus/messages/__init__.py +0 -12
  49. infrahub/profiles/node_applier.py +9 -0
  50. infrahub/proposed_change/branch_diff.py +1 -1
  51. infrahub/proposed_change/tasks.py +1 -1
  52. infrahub/repositories/create_repository.py +3 -3
  53. infrahub/task_manager/models.py +1 -1
  54. infrahub/task_manager/task.py +5 -5
  55. infrahub/trigger/setup.py +6 -9
  56. infrahub/utils.py +18 -0
  57. infrahub/validators/tasks.py +1 -1
  58. infrahub/workers/infrahub_async.py +7 -6
  59. infrahub_sdk/client.py +113 -1
  60. infrahub_sdk/ctl/AGENTS.md +67 -0
  61. infrahub_sdk/ctl/branch.py +175 -1
  62. infrahub_sdk/ctl/check.py +3 -3
  63. infrahub_sdk/ctl/cli_commands.py +9 -9
  64. infrahub_sdk/ctl/generator.py +2 -2
  65. infrahub_sdk/ctl/graphql.py +1 -2
  66. infrahub_sdk/ctl/importer.py +1 -2
  67. infrahub_sdk/ctl/repository.py +6 -49
  68. infrahub_sdk/ctl/task.py +2 -4
  69. infrahub_sdk/ctl/utils.py +2 -2
  70. infrahub_sdk/ctl/validate.py +1 -2
  71. infrahub_sdk/diff.py +80 -3
  72. infrahub_sdk/graphql/constants.py +14 -1
  73. infrahub_sdk/graphql/renderers.py +5 -1
  74. infrahub_sdk/node/attribute.py +0 -1
  75. infrahub_sdk/node/constants.py +3 -1
  76. infrahub_sdk/node/node.py +303 -3
  77. infrahub_sdk/node/related_node.py +1 -2
  78. infrahub_sdk/node/relationship.py +1 -2
  79. infrahub_sdk/protocols_base.py +0 -1
  80. infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
  81. infrahub_sdk/schema/__init__.py +0 -3
  82. infrahub_sdk/timestamp.py +7 -7
  83. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/METADATA +2 -3
  84. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/RECORD +88 -84
  85. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/WHEEL +1 -1
  86. infrahub_testcontainers/container.py +2 -2
  87. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/entry_points.txt +0 -0
  88. {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -170,7 +170,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
170
170
  return self._human_friendly_id is not None
171
171
 
172
172
  async def add_human_friendly_id(self, db: InfrahubDatabase) -> None:
173
- if not self._schema.human_friendly_id or self._human_friendly_id:
173
+ if self._human_friendly_id:
174
174
  return
175
175
 
176
176
  self._human_friendly_id = HumanFriendlyIdentifier(
@@ -179,11 +179,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
179
179
  await self._human_friendly_id.compute(db=db, node=self)
180
180
 
181
181
  async def get_display_label(self, db: InfrahubDatabase) -> str:
182
- if self._display_label:
183
- if isinstance(self._display_label._value, str):
184
- return self._display_label._value
185
- if self._display_label._value:
186
- return self._display_label._value.value
182
+ if self._display_label and (value := self._display_label.get_value(node=self, at=self._at)):
183
+ return value
187
184
 
188
185
  return await self.render_display_label(db=db)
189
186
 
@@ -191,7 +188,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
191
188
  return self._display_label is not None
192
189
 
193
190
  async def add_display_label(self, db: InfrahubDatabase) -> None:
194
- if not self._schema.display_label or self._display_label:
191
+ if self._display_label:
195
192
  return
196
193
 
197
194
  self._display_label = DisplayLabel(node_schema=self._schema, template=self._schema.display_label)
@@ -467,15 +464,21 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
467
464
  for attribute_name in template._attributes:
468
465
  if attribute_name in list(fields) + [OBJECT_TEMPLATE_NAME_ATTR]:
469
466
  continue
470
- attr_value = getattr(template, attribute_name).value
467
+ attr = getattr(template, attribute_name)
468
+ attr_value = attr.value
471
469
  if attr_value is not None:
472
- fields[attribute_name] = {"value": attr_value, "source": template.id}
470
+ # Preserve is_from_profile flag when copying from template
471
+ field_data = {"value": attr_value, "source": attr.source_id or template.id}
472
+ if attr.is_from_profile:
473
+ field_data["is_from_profile"] = True
474
+ fields[attribute_name] = field_data
473
475
 
474
476
  for relationship_name in template._relationships:
475
477
  relationship_schema = template._schema.get_relationship(name=relationship_name)
476
478
  if (
477
479
  relationship_name in list(fields)
478
- or relationship_schema.kind not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC]
480
+ or relationship_schema.kind
481
+ not in [RelationshipKind.ATTRIBUTE, RelationshipKind.GENERIC, RelationshipKind.PROFILE]
479
482
  or relationship_name == OBJECT_TEMPLATE_RELATIONSHIP_NAME
480
483
  ):
481
484
  continue
@@ -570,7 +573,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
570
573
  self,
571
574
  rel_schema.name,
572
575
  await generator_method(
573
- db=db, name=rel_schema.name, schema=rel_schema, data=fields.get(rel_schema.name, None)
576
+ db=db, name=rel_schema.name, schema=rel_schema, data=fields.get(rel_schema.name)
574
577
  ),
575
578
  )
576
579
  except ValidationError as exc:
@@ -600,7 +603,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
600
603
  self,
601
604
  attr_schema.name,
602
605
  await generator_method(
603
- db=db, name=attr_schema.name, schema=attr_schema, data=fields.get(attr_schema.name, None)
606
+ db=db, name=attr_schema.name, schema=attr_schema, data=fields.get(attr_schema.name)
604
607
  ),
605
608
  )
606
609
  if not self._existing:
@@ -1088,7 +1091,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
1088
1091
 
1089
1092
  if key in self._relationships:
1090
1093
  rel: RelationshipManager = getattr(self, key)
1091
- changed |= await rel.update(db=db, data=value)
1094
+ changed |= await rel.update(db=db, data=value, process_delete=process_pools)
1092
1095
 
1093
1096
  return changed
1094
1097
 
@@ -58,18 +58,34 @@ async def extract_peer_data(
58
58
  except ValueError:
59
59
  pass
60
60
 
61
- obj_peer_data[attr_name] = {"value": template_attr.value, "source": template_peer.id}
61
+ # If the template attribute comes from a profile, preserve the profile as the source
62
+ # Otherwise, use the template itself as the source
63
+ source_id = template_attr.source_id or template_peer.id
64
+ attr_data = {"value": template_attr.value, "source": source_id}
65
+ if template_attr.is_from_profile:
66
+ attr_data["is_from_profile"] = True
67
+ obj_peer_data[attr_name] = attr_data
62
68
 
63
69
  for rel in template_peer.get_schema().relationship_names:
64
70
  rel_manager: RelationshipManager = getattr(template_peer, rel)
65
-
66
- if rel_manager.schema.name not in obj_peer_schema.relationship_names:
71
+ if (
72
+ rel_manager.schema.kind
73
+ not in [
74
+ RelationshipKind.COMPONENT,
75
+ RelationshipKind.PARENT,
76
+ RelationshipKind.PROFILE,
77
+ RelationshipKind.ATTRIBUTE,
78
+ ]
79
+ or rel_manager.schema.name not in obj_peer_schema.relationship_names
80
+ ):
67
81
  continue
68
82
 
69
83
  peers_map = await rel_manager.get_peers(db=db)
70
- if rel_manager.schema.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT] and list(
71
- peers_map.keys()
72
- ) == [current_template.id]:
84
+ if rel_manager.schema.kind in [
85
+ RelationshipKind.COMPONENT,
86
+ RelationshipKind.PARENT,
87
+ RelationshipKind.PROFILE,
88
+ ] and list(peers_map.keys()) == [current_template.id]:
73
89
  obj_peer_data[rel] = {"id": parent_obj.id}
74
90
  continue
75
91
 
@@ -80,7 +96,13 @@ async def extract_peer_data(
80
96
  continue
81
97
  rel_peer_ids.append({"id": peer_id})
82
98
 
83
- obj_peer_data[rel] = rel_peer_ids
99
+ # Only set the relationship data if there are actual peers to set
100
+ if rel_peer_ids:
101
+ obj_peer_data[rel] = rel_peer_ids
102
+
103
+ if rel_manager.schema.kind == RelationshipKind.PROFILE:
104
+ profiles = list(await rel_manager.get_peers(db=db))
105
+ obj_peer_data[rel] = profiles
84
106
 
85
107
  return obj_peer_data
86
108
 
@@ -125,6 +147,12 @@ async def handle_template_relationships(
125
147
  await constraint_runner.check(node=obj_peer, field_filters=list(obj_peer_data))
126
148
  await obj_peer.save(db=db)
127
149
 
150
+ template_profile_ids = await get_profile_ids(db=db, obj=template_relationship_peer)
151
+ if template_profile_ids:
152
+ node_profiles_applier = NodeProfilesApplier(db=db, branch=branch)
153
+ await node_profiles_applier.apply_profiles(node=obj_peer)
154
+ await obj_peer.save(db=db)
155
+
128
156
  await handle_template_relationships(
129
157
  db=db,
130
158
  branch=branch,
@@ -136,7 +164,7 @@ async def handle_template_relationships(
136
164
  )
137
165
 
138
166
 
139
- async def get_profile_ids(db: InfrahubDatabase, obj: Node) -> set[str]:
167
+ async def get_profile_ids(db: InfrahubDatabase, obj: Node | CoreObjectTemplate) -> set[str]:
140
168
  if not hasattr(obj, "profiles"):
141
169
  return set()
142
170
  profile_rels = await obj.profiles.get_relationships(db=db)
@@ -1,11 +1,13 @@
1
- from typing import cast
1
+ from typing import TYPE_CHECKING, cast
2
2
 
3
3
  from infrahub.core.constants.infrahubkind import THREADCOMMENT
4
4
  from infrahub.core.manager import NodeManager
5
5
  from infrahub.core.node import Node
6
- from infrahub.core.protocols import CoreProposedChange as CoreProposedChangeProtocol
7
6
  from infrahub.database import InfrahubDatabase
8
7
 
8
+ if TYPE_CHECKING:
9
+ from infrahub.core.protocols import CoreProposedChange as CoreProposedChangeProtocol
10
+
9
11
 
10
12
  class CoreProposedChange(Node):
11
13
  async def to_graphql(
@@ -29,7 +31,7 @@ class CoreProposedChange(Node):
29
31
  if fields:
30
32
  if "total_comments" in fields:
31
33
  total_comments = 0
32
- proposed_change = cast(CoreProposedChangeProtocol, self)
34
+ proposed_change = cast("CoreProposedChangeProtocol", self)
33
35
  change_comments = await proposed_change.comments.get_relationships(db=db)
34
36
  total_comments += len(change_comments)
35
37
 
@@ -72,7 +72,7 @@ class StandardNode(BaseModel):
72
72
  response: dict[str, Any] = {"id": self.uuid}
73
73
 
74
74
  for field_name in fields.keys():
75
- if field_name in ["id"]:
75
+ if field_name == "id":
76
76
  continue
77
77
  if field_name == "__typename":
78
78
  response[field_name] = self.get_type()
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from infrahub.core.protocols_base import CoreNode
8
8
 
@@ -16,22 +16,16 @@ if TYPE_CHECKING:
16
16
  DateTime,
17
17
  DateTimeOptional,
18
18
  Dropdown,
19
- DropdownOptional,
20
19
  HashedPassword,
21
- HashedPasswordOptional,
22
20
  Integer,
23
21
  IntegerOptional,
24
22
  IPHost,
25
- IPHostOptional,
26
23
  IPNetwork,
27
- IPNetworkOptional,
28
24
  JSONAttribute,
29
25
  JSONAttributeOptional,
30
- ListAttribute,
31
26
  ListAttributeOptional,
32
27
  String,
33
28
  StringOptional,
34
- URLOptional,
35
29
  )
36
30
  from infrahub.core.relationship import RelationshipManager
37
31
 
@@ -373,7 +373,7 @@ async def default_attribute_query_filter(
373
373
  if property_name not in [v.value for v in NodeProperty]:
374
374
  raise ValueError(f"filter {filter_name}: {filter_value}, {property_name} is not a valid property")
375
375
 
376
- if property_attr not in ["id"]:
376
+ if property_attr != "id":
377
377
  raise ValueError(f"filter {filter_name}: {filter_value}, {property_attr} is supported")
378
378
 
379
379
  clean_filter_name = f"{property_name}_{property_attr}"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from collections import defaultdict
4
5
  from copy import copy
5
6
  from dataclasses import dataclass
@@ -12,6 +13,7 @@ from infrahub.core import registry
12
13
  from infrahub.core.constants import (
13
14
  GLOBAL_BRANCH_NAME,
14
15
  PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
16
+ PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER,
15
17
  AttributeDBNodeType,
16
18
  RelationshipDirection,
17
19
  RelationshipHierarchyDirection,
@@ -623,7 +625,8 @@ class NodeListGetAttributeQuery(Query):
623
625
 
624
626
  async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
625
627
  self.params["ids"] = self.ids
626
- self.params["profile_relationship_name"] = PROFILE_NODE_RELATIONSHIP_IDENTIFIER
628
+ self.params["profile_node_relationship_name"] = PROFILE_NODE_RELATIONSHIP_IDENTIFIER
629
+ self.params["profile_template_relationship_name"] = PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER
627
630
 
628
631
  branch_filter, branch_params = self.branch.get_query_filter_path(
629
632
  at=self.at, branch_agnostic=self.branch_agnostic
@@ -632,7 +635,10 @@ class NodeListGetAttributeQuery(Query):
632
635
 
633
636
  query = """
634
637
  MATCH (n:Node) WHERE n.uuid IN $ids
635
- WITH n, exists((n)-[:IS_RELATED]-(:Relationship {name: $profile_relationship_name})) AS might_use_profile
638
+ WITH n, (
639
+ exists((n)-[:IS_RELATED]-(:Relationship {name: $profile_node_relationship_name})) OR
640
+ exists((n)-[:IS_RELATED]-(:Relationship {name: $profile_template_relationship_name}))
641
+ ) AS might_use_profile
636
642
  MATCH (n)-[:HAS_ATTRIBUTE]-(a:Attribute)
637
643
  """
638
644
  if self.fields:
@@ -1134,10 +1140,8 @@ class NodeGetListQuery(Query):
1134
1140
  self._variables_to_track.append(variable)
1135
1141
 
1136
1142
  def _untrack_variable(self, variable: str) -> None:
1137
- try:
1143
+ with contextlib.suppress(ValueError):
1138
1144
  self._variables_to_track.remove(variable)
1139
- except ValueError:
1140
- ...
1141
1145
 
1142
1146
  def _get_tracked_variables(self) -> list[str]:
1143
1147
  return self._variables_to_track
@@ -912,6 +912,8 @@ class RelationshipManager:
912
912
  db: InfrahubDatabase,
913
913
  peer_type: type[PeerType],
914
914
  branch_agnostic: bool = ...,
915
+ include_source: bool = ...,
916
+ include_owner: bool = ...,
915
917
  ) -> Mapping[str, PeerType]: ...
916
918
 
917
919
  @overload
@@ -920,6 +922,8 @@ class RelationshipManager:
920
922
  db: InfrahubDatabase,
921
923
  peer_type: None = None,
922
924
  branch_agnostic: bool = ...,
925
+ include_source: bool = ...,
926
+ include_owner: bool = ...,
923
927
  ) -> Mapping[str, Node]: ...
924
928
 
925
929
  async def get_peers(
@@ -927,11 +931,18 @@ class RelationshipManager:
927
931
  db: InfrahubDatabase,
928
932
  peer_type: type[PeerType] | None = None, # noqa: ARG002
929
933
  branch_agnostic: bool = False,
934
+ include_source: bool = False,
935
+ include_owner: bool = False,
930
936
  ) -> Mapping[str, Node | PeerType]:
931
937
  rels = await self.get_relationships(db=db, branch_agnostic=branch_agnostic)
932
938
  peer_ids = [rel.peer_id for rel in rels if rel.peer_id]
933
939
  nodes = await registry.manager.get_many(
934
- db=db, ids=peer_ids, branch=self.branch, branch_agnostic=branch_agnostic
940
+ db=db,
941
+ ids=peer_ids,
942
+ branch=self.branch,
943
+ branch_agnostic=branch_agnostic,
944
+ include_source=include_source,
945
+ include_owner=include_owner,
935
946
  )
936
947
  return nodes
937
948
 
@@ -1061,7 +1072,12 @@ class RelationshipManager:
1061
1072
 
1062
1073
  return self._relationships.as_list()
1063
1074
 
1064
- async def update(self, data: list[str | Node] | dict[str, Any] | str | Node | None, db: InfrahubDatabase) -> bool:
1075
+ async def update(
1076
+ self,
1077
+ data: list[str | Node] | dict[str, Any] | str | Node | None,
1078
+ db: InfrahubDatabase,
1079
+ process_delete: bool = True,
1080
+ ) -> bool:
1065
1081
  """Replace and Update the list of relationships with this one."""
1066
1082
  if not isinstance(data, list):
1067
1083
  list_data: Sequence[str | Node | dict[str, Any] | None] = [data]
@@ -1087,8 +1103,9 @@ class RelationshipManager:
1087
1103
 
1088
1104
  if item is None:
1089
1105
  if previous_relationships:
1090
- for rel in previous_relationships.values():
1091
- await rel.delete(db=db)
1106
+ if process_delete:
1107
+ for rel in previous_relationships.values():
1108
+ await rel.delete(db=db)
1092
1109
  changed = True
1093
1110
  continue
1094
1111
 
@@ -51,4 +51,4 @@ class GenericSchema(GeneratedGenericSchema):
51
51
  def _get_field_names_for_diff(self) -> list[str]:
52
52
  """Exclude used_by from the diff for generic nodes"""
53
53
  fields = super()._get_field_names_for_diff()
54
- return [field for field in fields if field not in ["used_by"]]
54
+ return [field for field in fields if field != "used_by"]
@@ -774,10 +774,15 @@ class SchemaManager(NodeManager):
774
774
  """Return non active branches that were purged."""
775
775
 
776
776
  hashes_to_keep: set[str] = set()
777
+ branch_processed: set[str] = set()
777
778
  for active_branch in active_branches:
778
- if branch := self._branches.get(active_branch):
779
- nodes = branch.get_all(include_internal=True, duplicate=False)
780
- hashes_to_keep.update([node.get_hash() for node in nodes.values()])
779
+ branch_hash = self._branch_hash_by_name.get(active_branch)
780
+ if not branch_hash or branch_hash not in branch_processed:
781
+ if branch_hash:
782
+ branch_processed.add(branch_hash)
783
+ if branch := self._branches.get(active_branch):
784
+ nodes = branch.get_all(include_internal=True, duplicate=False)
785
+ hashes_to_keep.update([node.get_hash() for node in nodes.values()])
781
786
 
782
787
  removed_branches: list[str] = []
783
788
  for branch_name in list(self._branches.keys()):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import copy
4
5
  import hashlib
5
6
  import keyword
@@ -19,6 +20,7 @@ from infrahub.core.constants import (
19
20
  OBJECT_TEMPLATE_NAME_ATTR,
20
21
  OBJECT_TEMPLATE_RELATIONSHIP_NAME,
21
22
  PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
23
+ PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER,
22
24
  RESERVED_ATTR_GEN_NAMES,
23
25
  RESERVED_ATTR_REL_NAMES,
24
26
  RESTRICTED_NAMESPACES,
@@ -72,6 +74,16 @@ from .schema_branch_hfid import HFIDs
72
74
  log = get_logger()
73
75
 
74
76
 
77
+ profiles_rel_settings: dict[str, Any] = {
78
+ "name": "profiles",
79
+ "identifier": PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
80
+ "peer": InfrahubKind.PROFILE,
81
+ "kind": RelationshipKind.PROFILE,
82
+ "cardinality": RelationshipCardinality.MANY,
83
+ "branch": BranchSupportType.AWARE,
84
+ }
85
+
86
+
75
87
  class SchemaBranch:
76
88
  def __init__(
77
89
  self,
@@ -334,10 +346,8 @@ class SchemaBranch:
334
346
  )
335
347
 
336
348
  schema: MainSchemaTypes | None = None
337
- try:
349
+ with contextlib.suppress(KeyError):
338
350
  schema = self._cache[key]
339
- except KeyError:
340
- pass
341
351
 
342
352
  if not schema:
343
353
  raise ValueError(f"Schema {name!r} on branch {self.name} has incorrect hash: {key!r}")
@@ -1116,7 +1126,7 @@ class SchemaBranch:
1116
1126
  ) from None
1117
1127
 
1118
1128
  for rel in node.relationships:
1119
- if rel.peer in [InfrahubKind.GENERICGROUP]:
1129
+ if rel.peer == InfrahubKind.GENERICGROUP:
1120
1130
  continue
1121
1131
  if not self.has(rel.peer) or self.get(rel.peer, duplicate=False).state == HashableModelState.ABSENT:
1122
1132
  raise ValueError(
@@ -2163,10 +2173,8 @@ class SchemaBranch:
2163
2173
  or not node.generate_profile
2164
2174
  or node.state == HashableModelState.ABSENT
2165
2175
  ):
2166
- try:
2176
+ with contextlib.suppress(SchemaNotFoundError):
2167
2177
  self.delete(name=self._get_profile_kind(node_kind=node.kind))
2168
- except SchemaNotFoundError:
2169
- ...
2170
2178
  continue
2171
2179
 
2172
2180
  profile = self.generate_profile_from_node(node=node)
@@ -2212,15 +2220,6 @@ class SchemaBranch:
2212
2220
  ):
2213
2221
  continue
2214
2222
 
2215
- profiles_rel_settings: dict[str, Any] = {
2216
- "name": "profiles",
2217
- "identifier": PROFILE_NODE_RELATIONSHIP_IDENTIFIER,
2218
- "peer": InfrahubKind.PROFILE,
2219
- "kind": RelationshipKind.PROFILE,
2220
- "cardinality": RelationshipCardinality.MANY,
2221
- "branch": BranchSupportType.AWARE,
2222
- }
2223
-
2224
2223
  # Add relationship between node and profile
2225
2224
  if "profiles" not in node.relationship_names:
2226
2225
  node_schema = self.get(name=node_name, duplicate=True)
@@ -2285,6 +2284,18 @@ class SchemaBranch:
2285
2284
  )
2286
2285
  ],
2287
2286
  )
2287
+ if f"Template{node.kind}" in self.all_names:
2288
+ template = self.get(name=f"Template{node.kind}", duplicate=False)
2289
+ profile.relationships.append(
2290
+ RelationshipSchema(
2291
+ name="related_templates",
2292
+ identifier=PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER,
2293
+ peer=template.kind,
2294
+ kind=RelationshipKind.PROFILE,
2295
+ cardinality=RelationshipCardinality.MANY,
2296
+ branch=BranchSupportType.AWARE,
2297
+ )
2298
+ )
2288
2299
 
2289
2300
  for node_attr in node.attributes:
2290
2301
  if not node_attr.support_profiles:
@@ -2415,6 +2426,14 @@ class SchemaBranch:
2415
2426
  template_schema.human_friendly_id = [parent_hfid] + template_schema.human_friendly_id
2416
2427
  template_schema.uniqueness_constraints[0].append(relationship.name)
2417
2428
 
2429
+ if getattr(node, "generate_profile", False):
2430
+ if "profiles" not in [r.name for r in template_schema.relationships]:
2431
+ settings = dict(profiles_rel_settings)
2432
+ settings["identifier"] = PROFILE_TEMPLATE_RELATIONSHIP_IDENTIFIER
2433
+ template_schema.relationships.append(RelationshipSchema(**settings))
2434
+
2435
+ self.set(name=template_schema.kind, schema=template_schema)
2436
+
2418
2437
  def generate_object_template_from_node(
2419
2438
  self, node: NodeSchema | GenericSchema, need_templates: set[NodeSchema | GenericSchema]
2420
2439
  ) -> TemplateSchema | GenericSchema:
@@ -4,13 +4,13 @@ from typing import TYPE_CHECKING, Any, cast
4
4
 
5
5
  from infrahub.core.constants import NULL_VALUE, PathType
6
6
  from infrahub.core.path import DataPath, GroupedDataPaths
7
- from infrahub.core.schema.generic_schema import GenericSchema
8
7
 
9
8
  from ..interface import ConstraintCheckerInterface
10
9
  from ..shared import AttributeSchemaValidatorQuery
11
10
 
12
11
  if TYPE_CHECKING:
13
12
  from infrahub.core.branch import Branch
13
+ from infrahub.core.schema.generic_schema import GenericSchema
14
14
  from infrahub.database import InfrahubDatabase
15
15
 
16
16
  from ..model import SchemaConstraintValidatorRequest
@@ -106,7 +106,7 @@ class AttributeChoicesChecker(ConstraintCheckerInterface):
106
106
  # skip inheriting schemas that override the attribute being checked
107
107
  excluded_kinds: list[str] = []
108
108
  if request.node_schema.is_generic_schema:
109
- request.node_schema = cast(GenericSchema, request.node_schema)
109
+ request.node_schema = cast("GenericSchema", request.node_schema)
110
110
  for inheriting_kind in request.node_schema.used_by:
111
111
  inheriting_schema = request.schema_branch.get_node(name=inheriting_kind, duplicate=False)
112
112
  inheriting_schema_attribute = inheriting_schema.get_attribute(name=request.schema_path.field_name)
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  from typing import TYPE_CHECKING, Any
2
3
 
3
4
  from infrahub.core.constants import RelationshipKind, SchemaPathType
@@ -84,14 +85,10 @@ class ConstraintValidatorDeterminer:
84
85
  constraints: list[SchemaUpdateConstraintInfo] = []
85
86
  schemas = list(self.schema_branch.get_all(duplicate=False).values())
86
87
  # added here to check their uniqueness constraints
87
- try:
88
+ with contextlib.suppress(SchemaNotFoundError):
88
89
  schemas.append(self.schema_branch.get_node(name="SchemaAttribute", duplicate=False))
89
- except SchemaNotFoundError:
90
- pass
91
- try:
90
+ with contextlib.suppress(SchemaNotFoundError):
92
91
  schemas.append(self.schema_branch.get_node(name="SchemaRelationship", duplicate=False))
93
- except SchemaNotFoundError:
94
- pass
95
92
  for schema in schemas:
96
93
  constraints.extend(await self._get_property_constraints_for_one_schema(schema=schema))
97
94
  return constraints
@@ -356,7 +356,7 @@ class InfrahubDatabase:
356
356
  type
357
357
  and type == QueryType.READ
358
358
  and runtime not in [Neo4jRuntime.DEFAULT, Neo4jRuntime.UNDEFINED]
359
- and not (self.is_transaction and runtime in [Neo4jRuntime.PARALLEL])
359
+ and not (self.is_transaction and runtime == Neo4jRuntime.PARALLEL)
360
360
  ):
361
361
  query = f"CYPHER runtime = {runtime.value}\n" + query
362
362
  else:
infrahub/git/base.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import shutil
4
5
  from abc import ABC, abstractmethod
5
6
  from pathlib import Path
@@ -748,10 +749,8 @@ class InfrahubRepositoryBase(BaseModel, ABC):
748
749
  for short_name, branch_data in branches.items():
749
750
  branch = None
750
751
 
751
- try:
752
+ with contextlib.suppress(BranchNotFoundError):
752
753
  branch = registry.get_branch_from_registry(branch=short_name)
753
- except BranchNotFoundError:
754
- ...
755
754
 
756
755
  branch_exists_import_sync_condition = branch and (
757
756
  branch.name not in {registry.default_branch, self.default_branch}
infrahub/git/models.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from pydantic import BaseModel, ConfigDict, Field
2
2
 
3
3
  from infrahub.context import InfrahubContext
4
+ from infrahub.core.node import Node
5
+ from infrahub.core.protocols import CoreReadOnlyRepository, CoreRepository
4
6
  from infrahub.message_bus.types import ProposedChangeBranchDiff
5
7
 
6
8
 
@@ -201,11 +203,22 @@ class RepositoryBranchInfo(BaseModel):
201
203
 
202
204
 
203
205
  class RepositoryData(BaseModel):
206
+ model_config = ConfigDict(arbitrary_types_allowed=True)
207
+
204
208
  repository_id: str = Field(..., description="Id of the repository")
205
209
  repository_name: str = Field(..., description="Name of the repository")
210
+ repository: CoreRepository | CoreReadOnlyRepository | Node = Field(
211
+ ..., description="InfrahubNode representing a Repository"
212
+ )
206
213
  branches: dict[str, str] = Field(
207
214
  ...,
208
215
  description="Dictionary with the name of the branch as the key and the active commit id as the value",
209
216
  )
210
217
 
211
218
  branch_info: dict[str, RepositoryBranchInfo] = Field(default_factory=dict)
219
+
220
+ def get_staging_branch(self) -> str | None:
221
+ for branch, info in self.branch_info.items():
222
+ if info.internal_status == "staging":
223
+ return branch
224
+ return None