infrahub-server 1.5.5__py3-none-any.whl → 1.6.0__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 (130) hide show
  1. infrahub/api/artifact.py +5 -3
  2. infrahub/auth.py +5 -6
  3. infrahub/cli/db.py +3 -3
  4. infrahub/cli/db_commands/clean_duplicate_schema_fields.py +2 -2
  5. infrahub/cli/dev.py +30 -0
  6. infrahub/config.py +62 -14
  7. infrahub/constants/database.py +5 -5
  8. infrahub/core/branch/models.py +24 -6
  9. infrahub/core/constants/__init__.py +1 -0
  10. infrahub/core/diff/model/diff.py +2 -2
  11. infrahub/core/graph/constraints.py +2 -2
  12. infrahub/core/manager.py +191 -60
  13. infrahub/core/merge.py +29 -2
  14. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +2 -3
  15. infrahub/core/migrations/shared.py +2 -2
  16. infrahub/core/models.py +5 -6
  17. infrahub/core/node/__init__.py +12 -6
  18. infrahub/core/node/create.py +36 -8
  19. infrahub/core/node/ipam.py +4 -4
  20. infrahub/core/node/node_property_attribute.py +2 -2
  21. infrahub/core/node/standard.py +1 -1
  22. infrahub/core/query/attribute.py +1 -1
  23. infrahub/core/query/branch.py +11 -0
  24. infrahub/core/query/node.py +9 -5
  25. infrahub/core/query/standard_node.py +3 -0
  26. infrahub/core/relationship/model.py +15 -10
  27. infrahub/core/schema/__init__.py +3 -3
  28. infrahub/core/schema/generic_schema.py +1 -1
  29. infrahub/core/schema/schema_branch.py +35 -16
  30. infrahub/core/task/user_task.py +2 -2
  31. infrahub/core/validators/determiner.py +3 -6
  32. infrahub/core/validators/enum.py +2 -2
  33. infrahub/database/__init__.py +1 -1
  34. infrahub/dependencies/interface.py +2 -2
  35. infrahub/events/constants.py +2 -2
  36. infrahub/git/base.py +42 -1
  37. infrahub/git/models.py +2 -1
  38. infrahub/git/repository.py +5 -1
  39. infrahub/git/tasks.py +28 -1
  40. infrahub/git/utils.py +9 -0
  41. infrahub/graphql/analyzer.py +4 -4
  42. infrahub/graphql/loaders/peers.py +6 -0
  43. infrahub/graphql/mutations/computed_attribute.py +1 -1
  44. infrahub/graphql/mutations/convert_object_type.py +1 -1
  45. infrahub/graphql/mutations/display_label.py +1 -1
  46. infrahub/graphql/mutations/hfid.py +1 -1
  47. infrahub/graphql/mutations/ipam.py +1 -1
  48. infrahub/graphql/mutations/profile.py +9 -1
  49. infrahub/graphql/mutations/relationship.py +2 -2
  50. infrahub/graphql/mutations/resource_manager.py +1 -1
  51. infrahub/graphql/queries/__init__.py +2 -1
  52. infrahub/graphql/queries/branch.py +58 -3
  53. infrahub/graphql/queries/ipam.py +9 -4
  54. infrahub/graphql/queries/resource_manager.py +7 -11
  55. infrahub/graphql/queries/search.py +5 -6
  56. infrahub/graphql/resolvers/ipam.py +20 -0
  57. infrahub/graphql/resolvers/many_relationship.py +12 -11
  58. infrahub/graphql/resolvers/resolver.py +6 -2
  59. infrahub/graphql/resolvers/single_relationship.py +1 -11
  60. infrahub/graphql/schema.py +2 -0
  61. infrahub/graphql/types/__init__.py +3 -1
  62. infrahub/graphql/types/branch.py +98 -2
  63. infrahub/lock.py +6 -6
  64. infrahub/log.py +1 -1
  65. infrahub/message_bus/messages/__init__.py +0 -12
  66. infrahub/patch/constants.py +2 -2
  67. infrahub/profiles/node_applier.py +9 -0
  68. infrahub/proposed_change/tasks.py +1 -1
  69. infrahub/task_manager/task.py +4 -4
  70. infrahub/telemetry/constants.py +2 -2
  71. infrahub/trigger/models.py +2 -2
  72. infrahub/trigger/setup.py +6 -9
  73. infrahub/utils.py +19 -1
  74. infrahub/validators/tasks.py +1 -1
  75. infrahub/workers/infrahub_async.py +39 -1
  76. infrahub_sdk/async_typer.py +2 -1
  77. infrahub_sdk/batch.py +2 -2
  78. infrahub_sdk/client.py +121 -10
  79. infrahub_sdk/config.py +2 -2
  80. infrahub_sdk/ctl/branch.py +176 -2
  81. infrahub_sdk/ctl/check.py +3 -3
  82. infrahub_sdk/ctl/cli.py +2 -2
  83. infrahub_sdk/ctl/cli_commands.py +10 -9
  84. infrahub_sdk/ctl/generator.py +2 -2
  85. infrahub_sdk/ctl/graphql.py +3 -4
  86. infrahub_sdk/ctl/importer.py +2 -3
  87. infrahub_sdk/ctl/repository.py +5 -6
  88. infrahub_sdk/ctl/task.py +2 -4
  89. infrahub_sdk/ctl/utils.py +4 -4
  90. infrahub_sdk/ctl/validate.py +1 -2
  91. infrahub_sdk/diff.py +80 -3
  92. infrahub_sdk/graphql/constants.py +14 -1
  93. infrahub_sdk/graphql/renderers.py +5 -1
  94. infrahub_sdk/node/attribute.py +10 -10
  95. infrahub_sdk/node/constants.py +2 -3
  96. infrahub_sdk/node/node.py +54 -11
  97. infrahub_sdk/node/related_node.py +1 -2
  98. infrahub_sdk/node/relationship.py +1 -2
  99. infrahub_sdk/object_store.py +4 -4
  100. infrahub_sdk/operation.py +2 -2
  101. infrahub_sdk/protocols_base.py +0 -1
  102. infrahub_sdk/protocols_generator/generator.py +1 -1
  103. infrahub_sdk/pytest_plugin/items/jinja2_transform.py +1 -1
  104. infrahub_sdk/pytest_plugin/models.py +1 -1
  105. infrahub_sdk/pytest_plugin/plugin.py +1 -1
  106. infrahub_sdk/query_groups.py +2 -2
  107. infrahub_sdk/schema/__init__.py +10 -14
  108. infrahub_sdk/schema/main.py +2 -2
  109. infrahub_sdk/schema/repository.py +2 -2
  110. infrahub_sdk/spec/object.py +2 -2
  111. infrahub_sdk/spec/range_expansion.py +1 -1
  112. infrahub_sdk/template/__init__.py +2 -1
  113. infrahub_sdk/transfer/importer/json.py +3 -3
  114. infrahub_sdk/types.py +2 -2
  115. infrahub_sdk/utils.py +2 -2
  116. {infrahub_server-1.5.5.dist-info → infrahub_server-1.6.0.dist-info}/METADATA +58 -59
  117. {infrahub_server-1.5.5.dist-info → infrahub_server-1.6.0.dist-info}/RECORD +240 -246
  118. {infrahub_server-1.5.5.dist-info → infrahub_server-1.6.0.dist-info}/WHEEL +1 -1
  119. infrahub_server-1.6.0.dist-info/entry_points.txt +12 -0
  120. infrahub_testcontainers/container.py +2 -2
  121. infrahub_testcontainers/docker-compose-cluster.test.yml +1 -1
  122. infrahub_testcontainers/docker-compose.test.yml +1 -1
  123. infrahub/core/schema/generated/__init__.py +0 -0
  124. infrahub/core/schema/generated/attribute_schema.py +0 -133
  125. infrahub/core/schema/generated/base_node_schema.py +0 -111
  126. infrahub/core/schema/generated/genericnode_schema.py +0 -30
  127. infrahub/core/schema/generated/node_schema.py +0 -40
  128. infrahub/core/schema/generated/relationship_schema.py +0 -141
  129. infrahub_server-1.5.5.dist-info/entry_points.txt +0 -13
  130. {infrahub_server-1.5.5.dist-info → infrahub_server-1.6.0.dist-info/licenses}/LICENSE.txt +0 -0
@@ -40,8 +40,8 @@ class BuiltinIPPrefix(Node):
40
40
  retrieved = await NodeManager.get_one(
41
41
  db=db, branch=self._branch, id=self.id, fields={"member_type": None, "prefix": None}
42
42
  )
43
- self.member_type = retrieved.member_type # type: ignore[union-attr]
44
- self.prefix = retrieved.prefix # type: ignore[union-attr]
43
+ self.member_type = retrieved.member_type # type: ignore[attr-defined,union-attr]
44
+ self.prefix = retrieved.prefix # type: ignore[attr-defined,union-attr]
45
45
  utilization_getter = PrefixUtilizationGetter(db=db, ip_prefixes=[self])
46
46
  utilization = await utilization_getter.get_use_percentage(
47
47
  ip_prefixes=[self], branch_names=[self._branch.name]
@@ -57,6 +57,6 @@ class BuiltinIPPrefix(Node):
57
57
  retrieved = await NodeManager.get_one(
58
58
  db=db, branch=self._branch, id=self.id, fields={"member_type": None, "prefix": None}
59
59
  )
60
- self.member_type = retrieved.member_type # type: ignore[union-attr]
61
- self.prefix = retrieved.prefix # type: ignore[union-attr]
60
+ self.member_type = retrieved.member_type # type: ignore[attr-defined,union-attr]
61
+ self.prefix = retrieved.prefix # type: ignore[attr-defined,union-attr]
62
62
  return get_prefix_space(self)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from abc import abstractmethod
4
4
  from enum import Enum
5
- from typing import TYPE_CHECKING, Any, Generic, TypeVar
5
+ from typing import TYPE_CHECKING, Any, TypeVar
6
6
 
7
7
  from infrahub_sdk.template import Jinja2Template
8
8
 
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
21
21
  T = TypeVar("T")
22
22
 
23
23
 
24
- class NodePropertyAttribute(Generic[T]):
24
+ class NodePropertyAttribute[T]:
25
25
  """A node property attribute is a construct that seats between a property and an attribute.
26
26
 
27
27
  View it as a property, set at the node level but stored in the database as an attribute. It usually is something computed from other components of
@@ -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()
@@ -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}"
@@ -3,8 +3,10 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
5
  from infrahub import config
6
+ from infrahub.core.branch.enums import BranchStatus
6
7
  from infrahub.core.constants import GLOBAL_BRANCH_NAME
7
8
  from infrahub.core.query import Query, QueryType
9
+ from infrahub.core.query.standard_node import StandardNodeGetListQuery
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  from infrahub.database import InfrahubDatabase
@@ -146,3 +148,12 @@ class RebaseBranchDeleteRelationshipQuery(Query):
146
148
  self.add_to_query(query=query)
147
149
 
148
150
  self.params["ids"] = [db.to_database_id(id) for id in self.ids]
151
+
152
+
153
+ class BranchNodeGetListQuery(StandardNodeGetListQuery):
154
+ def __init__(self, exclude_global: bool = False, **kwargs: Any) -> None:
155
+ self.raw_filter = f"n.status <> '{BranchStatus.DELETING.value}'"
156
+ if exclude_global:
157
+ self.raw_filter += f" AND n.name <> '{GLOBAL_BRANCH_NAME}'"
158
+
159
+ super().__init__(**kwargs)
@@ -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
@@ -132,6 +132,7 @@ class StandardNodeGetItemQuery(Query):
132
132
  class StandardNodeGetListQuery(Query):
133
133
  name = "standard_node_list"
134
134
  type = QueryType.READ
135
+ raw_filter: str | None = None
135
136
 
136
137
  def __init__(
137
138
  self, node_class: StandardNode, ids: list[str] | None = None, node_name: str | None = None, **kwargs: Any
@@ -150,6 +151,8 @@ class StandardNodeGetListQuery(Query):
150
151
  if self.node_name:
151
152
  filters.append("n.name = $name")
152
153
  self.params["name"] = self.node_name
154
+ if self.raw_filter:
155
+ filters.append(self.raw_filter)
153
156
 
154
157
  where = ""
155
158
  if filters:
@@ -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,12 +1072,7 @@ class RelationshipManager:
1061
1072
 
1062
1073
  return self._relationships.as_list()
1063
1074
 
1064
- async def update(
1065
- self,
1066
- data: list[str | Node] | dict[str, Any] | str | Node | None,
1067
- db: InfrahubDatabase,
1068
- process_delete: bool = True,
1069
- ) -> bool:
1075
+ async def update(self, data: list[str | Node] | dict[str, Any] | str | Node | None, db: InfrahubDatabase) -> bool:
1070
1076
  """Replace and Update the list of relationships with this one."""
1071
1077
  if not isinstance(data, list):
1072
1078
  list_data: Sequence[str | Node | dict[str, Any] | None] = [data]
@@ -1092,9 +1098,8 @@ class RelationshipManager:
1092
1098
 
1093
1099
  if item is None:
1094
1100
  if previous_relationships:
1095
- if process_delete:
1096
- for rel in previous_relationships.values():
1097
- await rel.delete(db=db)
1101
+ for rel in previous_relationships.values():
1102
+ await rel.delete(db=db)
1098
1103
  changed = True
1099
1104
  continue
1100
1105
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import uuid
4
4
  from enum import Enum
5
- from typing import Any, TypeAlias
5
+ from typing import Any
6
6
 
7
7
  from infrahub_sdk.utils import deep_merge_dict
8
8
  from pydantic import BaseModel, ConfigDict, Field
@@ -22,8 +22,8 @@ from .profile_schema import ProfileSchema
22
22
  from .relationship_schema import RelationshipSchema
23
23
  from .template_schema import TemplateSchema
24
24
 
25
- NonGenericSchemaTypes: TypeAlias = NodeSchema | ProfileSchema | TemplateSchema
26
- MainSchemaTypes: TypeAlias = NonGenericSchemaTypes | GenericSchema
25
+ NonGenericSchemaTypes = NodeSchema | ProfileSchema | TemplateSchema
26
+ MainSchemaTypes = NonGenericSchemaTypes | GenericSchema
27
27
 
28
28
 
29
29
  # -----------------------------------------------------
@@ -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"]
@@ -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:
@@ -6,6 +6,7 @@ from typing_extensions import Self
6
6
 
7
7
  from infrahub.core import registry
8
8
  from infrahub.core.constants import Severity, TaskConclusion
9
+ from infrahub.core.protocols import CoreGenericAccount
9
10
  from infrahub.log import get_logger
10
11
 
11
12
  from .task import Task
@@ -16,7 +17,6 @@ if TYPE_CHECKING:
16
17
 
17
18
  from structlog.stdlib import BoundLogger
18
19
 
19
- from infrahub.core.protocols import CoreGenericAccount
20
20
  from infrahub.database import InfrahubDatabase
21
21
  from infrahub.graphql.initialization import GraphqlContext
22
22
  from infrahub.services.protocols import InfrahubLogger
@@ -67,7 +67,7 @@ class UserTask:
67
67
  if self._account:
68
68
  return False
69
69
 
70
- account: CoreGenericAccount | None = await registry.manager.get_one(id=self.account_id, db=self.db)
70
+ account = await registry.manager.get_one(id=self.account_id, db=self.db, kind=CoreGenericAccount)
71
71
  if not account:
72
72
  raise ValueError(f"Unable to find the account associated with {self.account_id}")
73
73
  self._account = account
@@ -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
@@ -1,7 +1,7 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
 
3
3
 
4
- class ConstraintIdentifier(str, Enum):
4
+ class ConstraintIdentifier(StrEnum):
5
5
  ATTRIBUTE_PARAMETERS_REGEX_UPDATE = "attribute.parameters.regex.update"
6
6
  ATTRIBUTE_PARAMETERS_MIN_LENGTH_UPDATE = "attribute.parameters.min_length.update"
7
7
  ATTRIBUTE_PARAMETERS_MAX_LENGTH_UPDATE = "attribute.parameters.max_length.update"
@@ -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:
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from dataclasses import dataclass
3
- from typing import Generic, TypeVar
3
+ from typing import TypeVar
4
4
 
5
5
  from infrahub.core.branch import Branch
6
6
  from infrahub.database import InfrahubDatabase
@@ -14,7 +14,7 @@ class DependencyBuilderContext:
14
14
  branch: Branch
15
15
 
16
16
 
17
- class DependencyBuilder(ABC, Generic[T]):
17
+ class DependencyBuilder[T](ABC):
18
18
  @classmethod
19
19
  @abstractmethod
20
20
  def build(cls, context: DependencyBuilderContext) -> T: ...
@@ -1,8 +1,8 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
 
3
3
  EVENT_NAMESPACE = "infrahub"
4
4
 
5
5
 
6
- class EventSortOrder(str, Enum):
6
+ class EventSortOrder(StrEnum):
7
7
  ASC = "asc"
8
8
  DESC = "desc"
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
@@ -16,10 +17,12 @@ from prefect.logging import get_run_logger
16
17
  from pydantic import BaseModel, ConfigDict, Field
17
18
  from pydantic import ValidationError as PydanticValidationError
18
19
 
20
+ from infrahub import config
19
21
  from infrahub.core.branch import Branch
20
22
  from infrahub.core.constants import InfrahubKind, RepositoryOperationalStatus, RepositorySyncStatus
21
23
  from infrahub.core.registry import registry
22
24
  from infrahub.exceptions import (
25
+ BranchNotFoundError,
23
26
  CommitNotFoundError,
24
27
  FileOutOfRepositoryError,
25
28
  RepositoryConnectionError,
@@ -31,6 +34,7 @@ from infrahub.exceptions import (
31
34
  )
32
35
  from infrahub.git.constants import BRANCHES_DIRECTORY_NAME, COMMITS_DIRECTORY_NAME, TEMPORARY_DIRECTORY_NAME
33
36
  from infrahub.git.directory import get_repositories_directory, initialize_repositories_directory
37
+ from infrahub.git.utils import branch_name_in_import_sync_branches
34
38
  from infrahub.git.worktree import Worktree
35
39
  from infrahub.log import get_logger
36
40
  from infrahub.workers.dependencies import get_client
@@ -733,6 +737,43 @@ class InfrahubRepositoryBase(BaseModel, ABC):
733
737
 
734
738
  return True
735
739
 
740
+ async def get_filtered_remote_branches(self) -> dict[str, BranchInRemote]:
741
+ branches = self.get_branches_from_remote()
742
+
743
+ if not config.SETTINGS.git.import_sync_branch_names:
744
+ return branches
745
+
746
+ filtered_branches = {}
747
+ skipped_branch_names = []
748
+
749
+ for short_name, branch_data in branches.items():
750
+ branch = None
751
+
752
+ with contextlib.suppress(BranchNotFoundError):
753
+ branch = registry.get_branch_from_registry(branch=short_name)
754
+
755
+ branch_exists_import_sync_condition = branch and (
756
+ branch.name not in {registry.default_branch, self.default_branch}
757
+ and not branch.sync_with_git
758
+ and not branch_name_in_import_sync_branches(branch_short_name=short_name)
759
+ )
760
+ branch_does_not_exist_import_sync_condition = not branch and not branch_name_in_import_sync_branches(
761
+ branch_short_name=short_name
762
+ )
763
+
764
+ if branch_exists_import_sync_condition or branch_does_not_exist_import_sync_condition:
765
+ skipped_branch_names.append(short_name)
766
+ continue
767
+
768
+ filtered_branches[short_name] = branch_data
769
+
770
+ if skipped_branch_names:
771
+ log.debug(
772
+ f"Skipped the following branches {skipped_branch_names} "
773
+ f"because no match was found in import_sync_branch_names {config.SETTINGS.git.import_sync_branch_names}"
774
+ )
775
+ return filtered_branches
776
+
736
777
  async def compare_local_remote(self) -> tuple[list[str], list[str]]:
737
778
  """
738
779
  Returns:
@@ -745,7 +786,7 @@ class InfrahubRepositoryBase(BaseModel, ABC):
745
786
  # TODO move this section into a dedicated function to compare and bring in sync the remote repo with the local one.
746
787
  # It can be useful just after a clone etc ...
747
788
  local_branches = self.get_branches_from_local()
748
- remote_branches = self.get_branches_from_remote()
789
+ remote_branches = await self.get_filtered_remote_branches()
749
790
 
750
791
  new_branches = set(remote_branches.keys()) - set(local_branches.keys())
751
792
  existing_branches = set(local_branches.keys()) - new_branches
infrahub/git/models.py CHANGED
@@ -92,7 +92,8 @@ class GitRepositoryMerge(BaseModel):
92
92
  source_branch: str = Field(..., description="The source branch")
93
93
  destination_branch: str = Field(..., description="The destination branch")
94
94
  destination_branch_id: str = Field(..., description="The ID of the destination branch")
95
- default_branch: str = Field(..., description="The default branch in Git")
95
+ default_branch: str | None = Field(default=None, description="The default branch in Git")
96
+ repository_kind: str = Field(..., description="The kind of the repository.")
96
97
 
97
98
 
98
99
  class GitRepositoryImportObjects(BaseModel):
@@ -11,6 +11,7 @@ from prefect import task
11
11
  from prefect.cache_policies import NONE
12
12
  from pydantic import Field
13
13
 
14
+ from infrahub import config
14
15
  from infrahub.core.constants import InfrahubKind, RepositoryInternalStatus
15
16
  from infrahub.exceptions import RepositoryError
16
17
  from infrahub.git.integrator import InfrahubRepositoryIntegrator
@@ -170,7 +171,10 @@ class InfrahubRepository(InfrahubRepositoryIntegrator):
170
171
  commit = self.get_commit_value(branch_name=source_branch, remote=False)
171
172
 
172
173
  try:
173
- repo.git.merge(commit)
174
+ if config.SETTINGS.git.use_explicit_merge_commit:
175
+ repo.git.merge(commit, "--no-ff", m="Merged by Infrahub")
176
+ else:
177
+ repo.git.merge(commit)
174
178
  except GitCommandError as exc:
175
179
  repo.git.merge("--abort")
176
180
  raise RepositoryError(identifier=self.name, message=exc.stderr) from exc
infrahub/git/tasks.py CHANGED
@@ -502,6 +502,7 @@ async def pull_read_only(model: GitRepositoryPullReadOnly) -> None:
502
502
  flow_run_name="Merge {model.source_branch} > {model.destination_branch} in git repository",
503
503
  )
504
504
  async def merge_git_repository(model: GitRepositoryMerge) -> None:
505
+ log = get_run_logger()
505
506
  await add_tags(branches=[model.source_branch, model.destination_branch], nodes=[model.repository_id])
506
507
 
507
508
  client = get_client()
@@ -510,7 +511,11 @@ async def merge_git_repository(model: GitRepositoryMerge) -> None:
510
511
  id=model.repository_id, name=model.repository_name, client=client, default_branch_name=model.default_branch
511
512
  )
512
513
 
513
- if model.internal_status == RepositoryInternalStatus.STAGING.value:
514
+ if (
515
+ model.internal_status == RepositoryInternalStatus.STAGING.value
516
+ and model.repository_kind == InfrahubKind.REPOSITORY
517
+ ):
518
+ log.info(f"Merging {model.repository_kind}")
514
519
  repo_source = await client.get(
515
520
  kind=InfrahubKind.GENERICREPOSITORY, id=model.repository_id, branch=model.source_branch
516
521
  )
@@ -522,6 +527,28 @@ async def merge_git_repository(model: GitRepositoryMerge) -> None:
522
527
  repo_main.commit.value = commit
523
528
 
524
529
  await repo_main.save()
530
+ log.info(f"Finished merging {model.repository_kind}")
531
+
532
+ elif model.repository_kind == InfrahubKind.READONLYREPOSITORY:
533
+ repo_source = await client.get(
534
+ kind=InfrahubKind.READONLYREPOSITORY, id=model.repository_id, branch=model.source_branch
535
+ )
536
+ repo_destination = await client.get(
537
+ kind=InfrahubKind.READONLYREPOSITORY, id=model.repository_id, branch=model.destination_branch
538
+ )
539
+
540
+ if (
541
+ repo_destination.ref.value != repo_source.ref.value
542
+ or repo_destination.commit.value != repo_source.commit.value
543
+ ):
544
+ log.info(f"Merging {model.repository_kind}")
545
+
546
+ repo_destination.ref.value = repo_source.ref.value
547
+ repo_destination.commit.value = repo_source.commit.value
548
+ await repo_destination.save()
549
+
550
+ log.info(f"Finished merging {model.repository_kind}")
551
+
525
552
  else:
526
553
  async with lock.registry.get(name=model.repository_name, namespace="repository"):
527
554
  await repo.merge(source_branch=model.source_branch, dest_branch=model.destination_branch)
infrahub/git/utils.py CHANGED
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from collections import defaultdict
2
3
  from typing import TYPE_CHECKING, Any
3
4
 
@@ -12,6 +13,7 @@ from infrahub.core.manager import NodeManager
12
13
  from infrahub.database import InfrahubDatabase
13
14
  from infrahub.generators.models import ProposedChangeGeneratorDefinition
14
15
 
16
+ from .. import config
15
17
  from .models import RepositoryBranchInfo, RepositoryData
16
18
 
17
19
  if TYPE_CHECKING:
@@ -168,3 +170,10 @@ async def fetch_proposed_change_generator_definition_targets(
168
170
  return await _fetch_definition_targets(
169
171
  client=client, branch=branch, group_id=definition.group_id, parameters=definition.parameters
170
172
  )
173
+
174
+
175
+ def branch_name_in_import_sync_branches(branch_short_name: str) -> bool:
176
+ for branch_filter in config.SETTINGS.git.import_sync_branch_names:
177
+ if re.fullmatch(branch_filter, branch_short_name) or branch_filter == branch_short_name:
178
+ return True
179
+ return False