infrahub-server 1.4.13__py3-none-any.whl → 1.5.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 (222) hide show
  1. infrahub/actions/tasks.py +208 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/diff/diff.py +1 -1
  4. infrahub/api/internal.py +2 -0
  5. infrahub/api/query.py +2 -0
  6. infrahub/api/schema.py +27 -3
  7. infrahub/auth.py +5 -5
  8. infrahub/cli/__init__.py +2 -0
  9. infrahub/cli/db.py +160 -157
  10. infrahub/cli/dev.py +118 -0
  11. infrahub/cli/upgrade.py +56 -9
  12. infrahub/computed_attribute/tasks.py +19 -7
  13. infrahub/config.py +7 -2
  14. infrahub/core/attribute.py +35 -24
  15. infrahub/core/branch/enums.py +1 -1
  16. infrahub/core/branch/models.py +9 -5
  17. infrahub/core/branch/needs_rebase_status.py +11 -0
  18. infrahub/core/branch/tasks.py +72 -10
  19. infrahub/core/changelog/models.py +2 -10
  20. infrahub/core/constants/__init__.py +4 -0
  21. infrahub/core/constants/infrahubkind.py +1 -0
  22. infrahub/core/convert_object_type/object_conversion.py +201 -0
  23. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  24. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  25. infrahub/core/diff/model/path.py +4 -0
  26. infrahub/core/diff/payload_builder.py +1 -1
  27. infrahub/core/diff/query/artifact.py +1 -0
  28. infrahub/core/diff/query/field_summary.py +1 -0
  29. infrahub/core/graph/__init__.py +1 -1
  30. infrahub/core/initialization.py +7 -4
  31. infrahub/core/manager.py +3 -81
  32. infrahub/core/migrations/__init__.py +3 -0
  33. infrahub/core/migrations/exceptions.py +4 -0
  34. infrahub/core/migrations/graph/__init__.py +11 -10
  35. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  36. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  37. infrahub/core/migrations/graph/m037_index_attr_vals.py +11 -30
  38. infrahub/core/migrations/graph/m039_ipam_reconcile.py +9 -7
  39. infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +147 -0
  40. infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +164 -0
  41. infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +864 -0
  42. infrahub/core/migrations/query/__init__.py +7 -8
  43. infrahub/core/migrations/query/attribute_add.py +8 -6
  44. infrahub/core/migrations/query/attribute_remove.py +134 -0
  45. infrahub/core/migrations/runner.py +54 -0
  46. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  47. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  48. infrahub/core/migrations/schema/node_attribute_add.py +26 -5
  49. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  50. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  51. infrahub/core/migrations/schema/node_remove.py +2 -1
  52. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  53. infrahub/core/migrations/shared.py +66 -19
  54. infrahub/core/models.py +2 -2
  55. infrahub/core/node/__init__.py +207 -54
  56. infrahub/core/node/create.py +53 -49
  57. infrahub/core/node/lock_utils.py +124 -0
  58. infrahub/core/node/node_property_attribute.py +230 -0
  59. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  60. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  61. infrahub/core/node/resource_manager/number_pool.py +2 -1
  62. infrahub/core/node/standard.py +1 -1
  63. infrahub/core/property.py +11 -0
  64. infrahub/core/protocols.py +8 -1
  65. infrahub/core/query/attribute.py +82 -15
  66. infrahub/core/query/ipam.py +16 -4
  67. infrahub/core/query/node.py +66 -188
  68. infrahub/core/query/relationship.py +44 -26
  69. infrahub/core/query/subquery.py +0 -8
  70. infrahub/core/relationship/model.py +69 -24
  71. infrahub/core/schema/__init__.py +56 -0
  72. infrahub/core/schema/attribute_schema.py +4 -2
  73. infrahub/core/schema/basenode_schema.py +42 -2
  74. infrahub/core/schema/definitions/core/__init__.py +2 -0
  75. infrahub/core/schema/definitions/core/check.py +1 -1
  76. infrahub/core/schema/definitions/core/generator.py +2 -0
  77. infrahub/core/schema/definitions/core/group.py +16 -2
  78. infrahub/core/schema/definitions/core/repository.py +7 -0
  79. infrahub/core/schema/definitions/core/transform.py +1 -1
  80. infrahub/core/schema/definitions/internal.py +12 -3
  81. infrahub/core/schema/generated/attribute_schema.py +2 -2
  82. infrahub/core/schema/generated/base_node_schema.py +6 -1
  83. infrahub/core/schema/manager.py +3 -0
  84. infrahub/core/schema/node_schema.py +1 -0
  85. infrahub/core/schema/relationship_schema.py +0 -1
  86. infrahub/core/schema/schema_branch.py +295 -10
  87. infrahub/core/schema/schema_branch_display.py +135 -0
  88. infrahub/core/schema/schema_branch_hfid.py +120 -0
  89. infrahub/core/validators/aggregated_checker.py +1 -1
  90. infrahub/database/graph.py +21 -0
  91. infrahub/display_labels/__init__.py +0 -0
  92. infrahub/display_labels/gather.py +48 -0
  93. infrahub/display_labels/models.py +240 -0
  94. infrahub/display_labels/tasks.py +192 -0
  95. infrahub/display_labels/triggers.py +22 -0
  96. infrahub/events/branch_action.py +27 -1
  97. infrahub/events/group_action.py +1 -1
  98. infrahub/events/node_action.py +1 -1
  99. infrahub/generators/constants.py +7 -0
  100. infrahub/generators/models.py +38 -12
  101. infrahub/generators/tasks.py +34 -16
  102. infrahub/git/base.py +38 -1
  103. infrahub/git/integrator.py +22 -14
  104. infrahub/graphql/api/dependencies.py +2 -4
  105. infrahub/graphql/api/endpoints.py +16 -6
  106. infrahub/graphql/app.py +2 -4
  107. infrahub/graphql/initialization.py +2 -3
  108. infrahub/graphql/manager.py +213 -137
  109. infrahub/graphql/middleware.py +12 -0
  110. infrahub/graphql/mutations/branch.py +16 -0
  111. infrahub/graphql/mutations/computed_attribute.py +110 -3
  112. infrahub/graphql/mutations/convert_object_type.py +44 -13
  113. infrahub/graphql/mutations/display_label.py +118 -0
  114. infrahub/graphql/mutations/generator.py +25 -7
  115. infrahub/graphql/mutations/hfid.py +125 -0
  116. infrahub/graphql/mutations/ipam.py +73 -41
  117. infrahub/graphql/mutations/main.py +61 -178
  118. infrahub/graphql/mutations/profile.py +195 -0
  119. infrahub/graphql/mutations/proposed_change.py +8 -1
  120. infrahub/graphql/mutations/relationship.py +2 -2
  121. infrahub/graphql/mutations/repository.py +22 -83
  122. infrahub/graphql/mutations/resource_manager.py +2 -2
  123. infrahub/graphql/mutations/webhook.py +1 -1
  124. infrahub/graphql/queries/resource_manager.py +1 -1
  125. infrahub/graphql/registry.py +173 -0
  126. infrahub/graphql/resolvers/resolver.py +2 -0
  127. infrahub/graphql/schema.py +8 -1
  128. infrahub/graphql/schema_sort.py +170 -0
  129. infrahub/graphql/types/branch.py +4 -1
  130. infrahub/graphql/types/enums.py +3 -0
  131. infrahub/groups/tasks.py +1 -1
  132. infrahub/hfid/__init__.py +0 -0
  133. infrahub/hfid/gather.py +48 -0
  134. infrahub/hfid/models.py +240 -0
  135. infrahub/hfid/tasks.py +191 -0
  136. infrahub/hfid/triggers.py +22 -0
  137. infrahub/lock.py +119 -42
  138. infrahub/locks/__init__.py +0 -0
  139. infrahub/locks/tasks.py +37 -0
  140. infrahub/patch/plan_writer.py +2 -2
  141. infrahub/permissions/constants.py +2 -0
  142. infrahub/profiles/__init__.py +0 -0
  143. infrahub/profiles/node_applier.py +101 -0
  144. infrahub/profiles/queries/__init__.py +0 -0
  145. infrahub/profiles/queries/get_profile_data.py +98 -0
  146. infrahub/profiles/tasks.py +63 -0
  147. infrahub/proposed_change/tasks.py +24 -5
  148. infrahub/repositories/__init__.py +0 -0
  149. infrahub/repositories/create_repository.py +113 -0
  150. infrahub/server.py +9 -1
  151. infrahub/services/__init__.py +8 -5
  152. infrahub/services/adapters/workflow/worker.py +5 -2
  153. infrahub/task_manager/event.py +5 -0
  154. infrahub/task_manager/models.py +7 -0
  155. infrahub/tasks/registry.py +6 -4
  156. infrahub/trigger/catalogue.py +4 -0
  157. infrahub/trigger/models.py +2 -0
  158. infrahub/trigger/setup.py +13 -4
  159. infrahub/trigger/tasks.py +6 -0
  160. infrahub/webhook/models.py +1 -1
  161. infrahub/workers/dependencies.py +3 -1
  162. infrahub/workers/infrahub_async.py +5 -1
  163. infrahub/workflows/catalogue.py +118 -3
  164. infrahub/workflows/initialization.py +21 -0
  165. infrahub/workflows/models.py +17 -2
  166. infrahub_sdk/branch.py +17 -8
  167. infrahub_sdk/checks.py +1 -1
  168. infrahub_sdk/client.py +376 -95
  169. infrahub_sdk/config.py +29 -2
  170. infrahub_sdk/convert_object_type.py +61 -0
  171. infrahub_sdk/ctl/branch.py +3 -0
  172. infrahub_sdk/ctl/check.py +2 -3
  173. infrahub_sdk/ctl/cli_commands.py +20 -12
  174. infrahub_sdk/ctl/config.py +8 -2
  175. infrahub_sdk/ctl/generator.py +6 -3
  176. infrahub_sdk/ctl/graphql.py +184 -0
  177. infrahub_sdk/ctl/repository.py +39 -1
  178. infrahub_sdk/ctl/schema.py +40 -10
  179. infrahub_sdk/ctl/task.py +110 -0
  180. infrahub_sdk/ctl/utils.py +4 -0
  181. infrahub_sdk/ctl/validate.py +5 -3
  182. infrahub_sdk/diff.py +4 -5
  183. infrahub_sdk/exceptions.py +2 -0
  184. infrahub_sdk/generator.py +7 -1
  185. infrahub_sdk/graphql/__init__.py +12 -0
  186. infrahub_sdk/graphql/constants.py +1 -0
  187. infrahub_sdk/graphql/plugin.py +85 -0
  188. infrahub_sdk/graphql/query.py +77 -0
  189. infrahub_sdk/{graphql.py → graphql/renderers.py} +88 -75
  190. infrahub_sdk/graphql/utils.py +40 -0
  191. infrahub_sdk/node/attribute.py +2 -0
  192. infrahub_sdk/node/node.py +28 -20
  193. infrahub_sdk/node/relationship.py +1 -3
  194. infrahub_sdk/playback.py +1 -2
  195. infrahub_sdk/protocols.py +54 -6
  196. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  197. infrahub_sdk/pytest_plugin/utils.py +40 -0
  198. infrahub_sdk/repository.py +1 -2
  199. infrahub_sdk/schema/__init__.py +70 -4
  200. infrahub_sdk/schema/main.py +1 -0
  201. infrahub_sdk/schema/repository.py +8 -0
  202. infrahub_sdk/spec/models.py +7 -0
  203. infrahub_sdk/spec/object.py +54 -6
  204. infrahub_sdk/spec/processors/__init__.py +0 -0
  205. infrahub_sdk/spec/processors/data_processor.py +10 -0
  206. infrahub_sdk/spec/processors/factory.py +34 -0
  207. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  208. infrahub_sdk/spec/range_expansion.py +118 -0
  209. infrahub_sdk/task/models.py +6 -4
  210. infrahub_sdk/timestamp.py +18 -6
  211. infrahub_sdk/transforms.py +1 -1
  212. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/METADATA +9 -10
  213. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/RECORD +221 -165
  214. infrahub_testcontainers/container.py +114 -2
  215. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  216. infrahub_testcontainers/docker-compose.test.yml +5 -0
  217. infrahub_testcontainers/models.py +2 -2
  218. infrahub_testcontainers/performance_test.py +4 -4
  219. infrahub/core/convert_object_type/conversion.py +0 -134
  220. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/LICENSE.txt +0 -0
  221. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/WHEEL +0 -0
  222. {infrahub_server-1.4.13.dist-info → infrahub_server-1.5.0.dist-info}/entry_points.txt +0 -0
@@ -1,24 +1,31 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any, Sequence
3
+ from typing import TYPE_CHECKING, Any, Sequence, TypeAlias
4
4
 
5
5
  from pydantic import BaseModel, ConfigDict, Field
6
+ from rich.console import Console
6
7
  from typing_extensions import Self
7
8
 
8
9
  from infrahub.core import registry
9
10
  from infrahub.core.path import SchemaPath # noqa: TC001
10
11
  from infrahub.core.query import Query # noqa: TC001
11
- from infrahub.core.schema import (
12
- AttributeSchema,
13
- GenericSchema,
14
- NodeSchema,
15
- RelationshipSchema,
16
- SchemaRoot,
17
- internal_schema,
18
- )
12
+ from infrahub.core.schema import AttributeSchema, MainSchemaTypes, RelationshipSchema, SchemaRoot, internal_schema
19
13
  from infrahub.core.timestamp import Timestamp
20
14
 
21
- from .query import MigrationQuery # noqa: TC001
15
+ from .query import MigrationBaseQuery # noqa: TC001
16
+
17
+ MIGRATION_LOG_TIME_FORMAT = "[%Y-%m-%d %H:%M:%S]"
18
+ _migration_console: Console | None = None
19
+
20
+
21
+ def get_migration_console() -> Console:
22
+ global _migration_console
23
+
24
+ if _migration_console is None:
25
+ _migration_console = Console(log_time_format=MIGRATION_LOG_TIME_FORMAT)
26
+
27
+ return _migration_console
28
+
22
29
 
23
30
  if TYPE_CHECKING:
24
31
  from infrahub.core.branch import Branch
@@ -41,10 +48,12 @@ class MigrationResult(BaseModel):
41
48
  class SchemaMigration(BaseModel):
42
49
  model_config = ConfigDict(arbitrary_types_allowed=True)
43
50
  name: str = Field(..., description="Name of the migration")
44
- queries: Sequence[type[MigrationQuery]] = Field(..., description="List of queries to execute for this migration")
51
+ queries: Sequence[type[MigrationBaseQuery]] = Field(
52
+ ..., description="List of queries to execute for this migration"
53
+ )
45
54
 
46
- new_node_schema: NodeSchema | GenericSchema | None = None
47
- previous_node_schema: NodeSchema | GenericSchema | None = None
55
+ new_node_schema: MainSchemaTypes | None = None
56
+ previous_node_schema: MainSchemaTypes | None = None
48
57
  schema_path: SchemaPath
49
58
 
50
59
  async def execute_pre_queries(
@@ -66,9 +75,14 @@ class SchemaMigration(BaseModel):
66
75
  return result
67
76
 
68
77
  async def execute_queries(
69
- self, db: InfrahubDatabase, result: MigrationResult, branch: Branch, at: Timestamp
78
+ self,
79
+ db: InfrahubDatabase,
80
+ result: MigrationResult,
81
+ branch: Branch,
82
+ at: Timestamp,
83
+ queries: Sequence[type[MigrationBaseQuery]],
70
84
  ) -> MigrationResult:
71
- for migration_query in self.queries:
85
+ for migration_query in queries:
72
86
  try:
73
87
  query = await migration_query.init(db=db, branch=branch, at=at, migration=self)
74
88
  await query.execute(db=db)
@@ -79,31 +93,40 @@ class SchemaMigration(BaseModel):
79
93
 
80
94
  return result
81
95
 
82
- async def execute(self, db: InfrahubDatabase, branch: Branch, at: Timestamp | str | None = None) -> MigrationResult:
96
+ async def execute(
97
+ self,
98
+ db: InfrahubDatabase,
99
+ branch: Branch,
100
+ at: Timestamp | str | None = None,
101
+ queries: Sequence[type[MigrationBaseQuery]] | None = None,
102
+ ) -> MigrationResult:
83
103
  async with db.start_transaction() as ts:
84
104
  result = MigrationResult()
85
105
  at = Timestamp(at)
86
106
 
87
107
  await self.execute_pre_queries(db=ts, result=result, branch=branch, at=at)
88
- await self.execute_queries(db=ts, result=result, branch=branch, at=at)
108
+ queries_to_execute = queries or self.queries
109
+ await self.execute_queries(db=ts, result=result, branch=branch, at=at, queries=queries_to_execute)
89
110
  await self.execute_post_queries(db=ts, result=result, branch=branch, at=at)
90
111
 
91
112
  return result
92
113
 
93
114
  @property
94
- def new_schema(self) -> NodeSchema | GenericSchema:
115
+ def new_schema(self) -> MainSchemaTypes:
95
116
  if self.new_node_schema:
96
117
  return self.new_node_schema
97
118
  raise ValueError("new_node_schema hasn't been initialized")
98
119
 
99
120
  @property
100
- def previous_schema(self) -> NodeSchema | GenericSchema:
121
+ def previous_schema(self) -> MainSchemaTypes:
101
122
  if self.previous_node_schema:
102
123
  return self.previous_node_schema
103
124
  raise ValueError("previous_node_schema hasn't been initialized")
104
125
 
105
126
 
106
127
  class AttributeSchemaMigration(SchemaMigration):
128
+ uuids: list[str] | None = None
129
+
107
130
  @property
108
131
  def new_attribute_schema(self) -> AttributeSchema:
109
132
  if not self.schema_path.field_name:
@@ -215,3 +238,27 @@ class ArbitraryMigration(BaseModel):
215
238
 
216
239
  async def execute(self, db: InfrahubDatabase) -> MigrationResult:
217
240
  raise NotImplementedError()
241
+
242
+
243
+ class MigrationRequiringRebase(BaseModel):
244
+ model_config = ConfigDict(arbitrary_types_allowed=True)
245
+ name: str = Field(..., description="Name of the migration")
246
+ minimum_version: int = Field(..., description="Minimum version of the graph to execute this migration")
247
+
248
+ @classmethod
249
+ def init(cls, **kwargs: dict[str, Any]) -> Self:
250
+ return cls(**kwargs) # type: ignore[arg-type]
251
+
252
+ async def validate_migration(self, db: InfrahubDatabase) -> MigrationResult:
253
+ raise NotImplementedError()
254
+
255
+ async def execute_against_branch(self, db: InfrahubDatabase, branch: Branch) -> MigrationResult:
256
+ """Method that will be run against non-default branches, it assumes that the branches have been rebased."""
257
+ raise NotImplementedError()
258
+
259
+ async def execute(self, db: InfrahubDatabase) -> MigrationResult:
260
+ """Method that will be run against the default branch."""
261
+ raise NotImplementedError()
262
+
263
+
264
+ MigrationTypes: TypeAlias = GraphMigration | InternalSchemaMigration | ArbitraryMigration | MigrationRequiringRebase
infrahub/core/models.py CHANGED
@@ -404,8 +404,8 @@ class HashableModelDiff(BaseModel):
404
404
  class HashableModel(BaseModel):
405
405
  model_config = ConfigDict(extra="forbid")
406
406
 
407
- id: str | None = None
408
- state: HashableModelState = HashableModelState.PRESENT
407
+ id: str | None = Field(default=None)
408
+ state: HashableModelState = Field(default=HashableModelState.PRESENT)
409
409
 
410
410
  _exclude_from_hash: list[str] = []
411
411
  _sort_by: list[str] = []
@@ -42,10 +42,12 @@ from infrahub.types import ATTRIBUTE_TYPES
42
42
  from ...graphql.constants import KIND_GRAPHQL_FIELD_NAME
43
43
  from ...graphql.models import OrderModel
44
44
  from ...log import get_logger
45
+ from ..attribute import BaseAttribute
45
46
  from ..query.relationship import RelationshipDeleteAllQuery
46
47
  from ..relationship import RelationshipManager
47
48
  from ..utils import update_relationships_to
48
49
  from .base import BaseNode, BaseNodeMeta, BaseNodeOptions
50
+ from .node_property_attribute import DisplayLabel, HumanFriendlyIdentifier
49
51
 
50
52
  if TYPE_CHECKING:
51
53
  from typing_extensions import Self
@@ -53,8 +55,6 @@ if TYPE_CHECKING:
53
55
  from infrahub.core.branch import Branch
54
56
  from infrahub.database import InfrahubDatabase
55
57
 
56
- from ..attribute import BaseAttribute
57
-
58
58
  SchemaProtocol = TypeVar("SchemaProtocol")
59
59
 
60
60
  # ---------------------------------------------------------------------------------------
@@ -80,6 +80,29 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
80
80
  _meta.default_filter = default_filter
81
81
  super().__init_subclass_with_meta__(_meta=_meta, **options)
82
82
 
83
+ def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
84
+ self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
85
+ self._branch: Branch = branch
86
+ self._at: Timestamp = at
87
+ self._existing: bool = False
88
+
89
+ self._updated_at: Timestamp | None = None
90
+ self.id: str = None
91
+ self.db_id: str = None
92
+
93
+ self._source: Node | None = None
94
+ self._owner: Node | None = None
95
+ self._is_protected: bool = None
96
+ self._computed_jinja2_attributes: list[str] = []
97
+
98
+ self._display_label: DisplayLabel | None = None
99
+ self._human_friendly_id: HumanFriendlyIdentifier | None = None
100
+
101
+ # Lists of attributes and relationships names
102
+ self._attributes: list[str] = []
103
+ self._relationships: list[str] = []
104
+ self._node_changelog: NodeChangelog | None = None
105
+
83
106
  def get_schema(self) -> NonGenericSchemaTypes:
84
107
  return self._schema
85
108
 
@@ -100,16 +123,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
100
123
  def get_updated_at(self) -> Timestamp | None:
101
124
  return self._updated_at
102
125
 
126
+ def get_attribute(self, name: str) -> BaseAttribute:
127
+ attribute = getattr(self, name)
128
+ if not isinstance(attribute, BaseAttribute):
129
+ raise ValueError(f"{name} is not an attribute of {self.get_kind()}")
130
+ return attribute
131
+
132
+ def get_relationship(self, name: str) -> RelationshipManager:
133
+ relationship = getattr(self, name)
134
+ if not isinstance(relationship, RelationshipManager):
135
+ raise ValueError(f"{name} is not a relationship of {self.get_kind()}")
136
+ return relationship
137
+
138
+ def uses_profiles(self) -> bool:
139
+ for attr_name in self.get_schema().attribute_names:
140
+ try:
141
+ node_attr = self.get_attribute(attr_name)
142
+ except ValueError:
143
+ continue
144
+ if node_attr and node_attr.is_from_profile:
145
+ return True
146
+ return False
147
+
103
148
  async def get_hfid(self, db: InfrahubDatabase, include_kind: bool = False) -> list[str] | None:
104
149
  """Return the Human friendly id of the node."""
105
150
  if not self._schema.human_friendly_id:
106
151
  return None
107
152
 
108
- hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
153
+ hfid_values: list[str] | None = None
154
+ if self._human_friendly_id:
155
+ hfid_values = self._human_friendly_id.get_value(node=self, at=self._at)
156
+ if not hfid_values:
157
+ hfid_values = [await self.get_path_value(db=db, path=item) for item in self._schema.human_friendly_id]
158
+
109
159
  hfid = [value for value in hfid_values if value is not None]
110
- if include_kind:
111
- return [self.get_kind()] + hfid
112
- return hfid
160
+ return [self.get_kind()] + hfid if include_kind else hfid
113
161
 
114
162
  async def get_hfid_as_string(self, db: InfrahubDatabase, include_kind: bool = False) -> str | None:
115
163
  """Return the Human friendly id of the node in string format separated with a dunder (__) ."""
@@ -118,6 +166,37 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
118
166
  return None
119
167
  return "__".join(hfid)
120
168
 
169
+ def has_human_friendly_id(self) -> bool:
170
+ return self._human_friendly_id is not None
171
+
172
+ async def add_human_friendly_id(self, db: InfrahubDatabase) -> None:
173
+ if not self._schema.human_friendly_id or self._human_friendly_id:
174
+ return
175
+
176
+ self._human_friendly_id = HumanFriendlyIdentifier(
177
+ node_schema=self._schema, template=self._schema.human_friendly_id
178
+ )
179
+ await self._human_friendly_id.compute(db=db, node=self)
180
+
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
187
+
188
+ return await self.render_display_label(db=db)
189
+
190
+ def has_display_label(self) -> bool:
191
+ return self._display_label is not None
192
+
193
+ async def add_display_label(self, db: InfrahubDatabase) -> None:
194
+ if not self._schema.display_label or self._display_label:
195
+ return
196
+
197
+ self._display_label = DisplayLabel(node_schema=self._schema, template=self._schema.display_label)
198
+ await self._display_label.compute(db=db, node=self)
199
+
121
200
  async def get_path_value(self, db: InfrahubDatabase, path: str) -> str:
122
201
  schema_path = self._schema.parse_schema_path(
123
202
  path=path, schema=db.schema.get_schema_branch(name=self._branch.name)
@@ -176,30 +255,8 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
176
255
  return self._branch
177
256
 
178
257
  def __repr__(self) -> str:
179
- if not self._existing:
180
- return f"{self.get_kind()}(ID: {str(self.id)})[NEW]"
181
-
182
- return f"{self.get_kind()}(ID: {str(self.id)})"
183
-
184
- def __init__(self, schema: NodeSchema | ProfileSchema | TemplateSchema, branch: Branch, at: Timestamp):
185
- self._schema: NodeSchema | ProfileSchema | TemplateSchema = schema
186
- self._branch: Branch = branch
187
- self._at: Timestamp = at
188
- self._existing: bool = False
189
-
190
- self._updated_at: Timestamp | None = None
191
- self.id: str = None
192
- self.db_id: str = None
193
-
194
- self._source: Node | None = None
195
- self._owner: Node | None = None
196
- self._is_protected: bool = None
197
- self._computed_jinja2_attributes: list[str] = []
198
-
199
- # Lists of attributes and relationships names
200
- self._attributes: list[str] = []
201
- self._relationships: list[str] = []
202
- self._node_changelog: NodeChangelog | None = None
258
+ v = f"{self.get_kind()}(ID: {str(self.id)})"
259
+ return v if self._existing else f"{v}[NEW]"
203
260
 
204
261
  @property
205
262
  def node_changelog(self) -> NodeChangelog:
@@ -257,7 +314,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
257
314
 
258
315
  return cls(**attrs)
259
316
 
260
- async def handle_pool(self, db: InfrahubDatabase, attribute: BaseAttribute, errors: list) -> None:
317
+ async def handle_pool(
318
+ self, db: InfrahubDatabase, attribute: BaseAttribute, errors: list, allocate_resources: bool = True
319
+ ) -> None:
261
320
  """Evaluate if a resource has been requested from a pool and apply the resource
262
321
 
263
322
  This method only works on number pools, currently Integer is the only type that has the from_pool
@@ -268,7 +327,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
268
327
  attribute.from_pool = {"id": attribute.schema.parameters.number_pool_id}
269
328
  attribute.is_default = False
270
329
 
271
- if not attribute.from_pool:
330
+ if not attribute.from_pool or not allocate_resources:
272
331
  return
273
332
 
274
333
  try:
@@ -428,7 +487,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
428
487
  elif relationship_peers := await relationship.get_peers(db=db):
429
488
  fields[relationship_name] = [{"id": peer_id} for peer_id in relationship_peers]
430
489
 
431
- async def _process_fields(self, fields: dict, db: InfrahubDatabase) -> None:
490
+ async def _process_fields(self, fields: dict, db: InfrahubDatabase, process_pools: bool = True) -> None:
432
491
  errors = []
433
492
 
434
493
  if "_source" in fields.keys():
@@ -482,7 +541,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
482
541
  # Generate Attribute and Relationship and assign them
483
542
  # -------------------------------------------
484
543
  errors.extend(await self._process_fields_relationships(fields=fields, db=db))
485
- errors.extend(await self._process_fields_attributes(fields=fields, db=db))
544
+ errors.extend(await self._process_fields_attributes(fields=fields, db=db, process_pools=process_pools))
486
545
 
487
546
  if errors:
488
547
  raise ValidationError(errors)
@@ -519,7 +578,9 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
519
578
 
520
579
  return errors
521
580
 
522
- async def _process_fields_attributes(self, fields: dict, db: InfrahubDatabase) -> list[ValidationError]:
581
+ async def _process_fields_attributes(
582
+ self, fields: dict, db: InfrahubDatabase, process_pools: bool
583
+ ) -> list[ValidationError]:
523
584
  errors: list[ValidationError] = []
524
585
 
525
586
  for attr_schema in self._schema.attributes:
@@ -544,9 +605,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
544
605
  )
545
606
  if not self._existing:
546
607
  attribute: BaseAttribute = getattr(self, attr_schema.name)
547
- await self.handle_pool(db=db, attribute=attribute, errors=errors)
608
+ await self.handle_pool(db=db, attribute=attribute, errors=errors, allocate_resources=process_pools)
548
609
 
549
- attribute.validate(value=attribute.value, name=attribute.name, schema=attribute.schema)
610
+ if process_pools or attribute.from_pool is None:
611
+ attribute.validate(value=attribute.value, name=attribute.name, schema=attribute.schema)
550
612
  except ValidationError as exc:
551
613
  errors.append(exc)
552
614
 
@@ -674,7 +736,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
674
736
  self.label.value = " ".join([word.title() for word in self.name.value.split("_")])
675
737
  self.label.is_default = False
676
738
 
677
- async def new(self, db: InfrahubDatabase, id: str | None = None, **kwargs: Any) -> Self:
739
+ async def new(self, db: InfrahubDatabase, id: str | None = None, process_pools: bool = True, **kwargs: Any) -> Self:
678
740
  if id and not is_valid_uuid(id):
679
741
  raise ValidationError({"id": f"{id} is not a valid UUID"})
680
742
  if id:
@@ -684,15 +746,40 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
684
746
 
685
747
  self.id = id or str(UUIDT())
686
748
 
687
- await self._process_fields(db=db, fields=kwargs)
749
+ await self._process_fields(db=db, fields=kwargs, process_pools=process_pools)
688
750
  await self._process_macros(db=db)
689
751
 
690
752
  return self
691
753
 
692
754
  async def resolve_relationships(self, db: InfrahubDatabase) -> None:
755
+ extra_filters: dict[str, set[str]] = {}
756
+
757
+ if not self._existing:
758
+ # If we are creating a new node, we need to resolve extra filters from HFID and Display Labels,
759
+ # if we don't do this the fields might be blank
760
+ schema_branch = db.schema.get_schema_branch(name=self.get_branch_based_on_support_type().name)
761
+ try:
762
+ hfid_identifier = schema_branch.hfids.get_node_definition(kind=self._schema.kind)
763
+ for rel_name, attrs in hfid_identifier.relationship_fields.items():
764
+ extra_filters.setdefault(rel_name, set()).update(attrs)
765
+ except KeyError:
766
+ # No HFID defined for this kind
767
+ ...
768
+ try:
769
+ display_label_identifier = schema_branch.display_labels.get_template_node(kind=self._schema.kind)
770
+ for rel_name, attrs in display_label_identifier.relationship_fields.items():
771
+ extra_filters.setdefault(rel_name, set()).update(attrs)
772
+ except KeyError:
773
+ # No Display Label defined for this kind
774
+ ...
775
+
693
776
  for name in self._relationships:
694
777
  relm: RelationshipManager = getattr(self, name)
695
- await relm.resolve(db=db)
778
+ query_filter = []
779
+ if name in extra_filters:
780
+ query_filter.extend(list(extra_filters[name]))
781
+
782
+ await relm.resolve(db=db, fields=query_filter)
696
783
 
697
784
  async def load(
698
785
  self,
@@ -712,12 +799,26 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
712
799
  )
713
800
  self._updated_at = Timestamp(updated_at)
714
801
 
802
+ if not self._schema.is_schema_node:
803
+ if hfid := kwargs.pop("human_friendly_id", None):
804
+ self._human_friendly_id = HumanFriendlyIdentifier(
805
+ node_schema=self._schema, template=self._schema.human_friendly_id, value=hfid
806
+ )
807
+ if display_label := kwargs.pop("display_label", None):
808
+ self._display_label = DisplayLabel(
809
+ node_schema=self._schema, template=self._schema.display_label, value=display_label
810
+ )
811
+
715
812
  await self._process_fields(db=db, fields=kwargs)
716
813
  return self
717
814
 
718
815
  async def _create(self, db: InfrahubDatabase, at: Timestamp | None = None) -> NodeChangelog:
719
816
  create_at = Timestamp(at)
720
817
 
818
+ if not self._schema.is_schema_node:
819
+ await self.add_human_friendly_id(db=db)
820
+ await self.add_display_label(db=db)
821
+
721
822
  query = await NodeCreateAllQuery.init(db=db, node=self, at=create_at)
722
823
  await query.execute(db=db)
723
824
 
@@ -729,6 +830,13 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
729
830
  new_ids = query.get_ids()
730
831
  node_changelog = NodeChangelog(node_id=self.get_id(), node_kind=self.get_kind(), display_label="")
731
832
 
833
+ if self._human_friendly_id:
834
+ node_changelog.create_attribute(
835
+ attribute=self._human_friendly_id.get_node_attribute(node=self, at=create_at)
836
+ )
837
+ if self._display_label:
838
+ node_changelog.create_attribute(attribute=self._display_label.get_node_attribute(node=self, at=create_at))
839
+
732
840
  # Go over the list of Attribute and assign the new IDs one by one
733
841
  for name in self._attributes:
734
842
  attr: BaseAttribute = getattr(self, name)
@@ -741,12 +849,10 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
741
849
  relm: RelationshipManager = getattr(self, name)
742
850
  for rel in relm._relationships:
743
851
  identifier = f"{rel.schema.identifier}::{rel.peer_id}"
744
-
745
852
  rel.id, rel.db_id = new_ids[identifier]
746
-
747
853
  node_changelog.create_relationship(relationship=rel)
748
854
 
749
- node_changelog.display_label = await self.render_display_label(db=db)
855
+ node_changelog.display_label = await self.get_display_label(db=db)
750
856
  return node_changelog
751
857
 
752
858
  async def _update(
@@ -782,19 +888,41 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
782
888
  if parent := await rel.get_parent(db=db):
783
889
  node_changelog.add_parent_from_relationship(parent=parent)
784
890
 
785
- node_changelog.display_label = await self.render_display_label(db=db)
891
+ # Update the HFID if one of its variables is being updated
892
+ if self._human_friendly_id and (
893
+ (fields and "human_friendly_id" in fields) or self._human_friendly_id.needs_update(fields=fields)
894
+ ):
895
+ await self._human_friendly_id.compute(db=db, node=self)
896
+ updated_attribute = await self._human_friendly_id.get_node_attribute(node=self, at=update_at).save(
897
+ at=update_at, db=db
898
+ )
899
+ if updated_attribute:
900
+ node_changelog.add_attribute(attribute=updated_attribute)
901
+
902
+ # Update the display label if one of its variables is being updated
903
+ if self._display_label and (
904
+ (fields and "display_label" in fields) or self._display_label.needs_update(fields=fields)
905
+ ):
906
+ await self._display_label.compute(db=db, node=self)
907
+ self._display_label.get_node_attribute(node=self, at=update_at).get_create_data(node_schema=self._schema)
908
+ updated_attribute = await self._display_label.get_node_attribute(node=self, at=update_at).save(
909
+ at=update_at, db=db
910
+ )
911
+ if updated_attribute:
912
+ node_changelog.add_attribute(attribute=updated_attribute)
913
+
914
+ node_changelog.display_label = await self.get_display_label(db=db)
786
915
  return node_changelog
787
916
 
788
917
  async def save(self, db: InfrahubDatabase, at: Timestamp | None = None, fields: list[str] | None = None) -> Self:
789
918
  """Create or Update the Node in the database."""
790
-
791
919
  save_at = Timestamp(at)
792
920
 
793
921
  if self._existing:
794
922
  self._node_changelog = await self._update(at=save_at, db=db, fields=fields)
795
- return self
923
+ else:
924
+ self._node_changelog = await self._create(at=save_at, db=db)
796
925
 
797
- self._node_changelog = await self._create(at=save_at, db=db)
798
926
  return self
799
927
 
800
928
  async def delete(self, db: InfrahubDatabase, at: Timestamp | None = None) -> None:
@@ -803,13 +931,24 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
803
931
  delete_at = Timestamp(at)
804
932
 
805
933
  node_changelog = NodeChangelog(
806
- node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.render_display_label(db=db)
934
+ node_id=self.get_id(), node_kind=self.get_kind(), display_label=await self.get_display_label(db=db)
807
935
  )
808
936
  # Go over the list of Attribute and update them one by one
809
937
  for name in self._attributes:
810
938
  attr: BaseAttribute = getattr(self, name)
811
- deleted_attribute = await attr.delete(at=delete_at, db=db)
812
- if deleted_attribute:
939
+ if deleted_attribute := await attr.delete(at=delete_at, db=db):
940
+ node_changelog.add_attribute(attribute=deleted_attribute)
941
+
942
+ if self._human_friendly_id:
943
+ if deleted_attribute := await self._human_friendly_id.get_node_attribute(node=self, at=delete_at).delete(
944
+ at=delete_at, db=db
945
+ ):
946
+ node_changelog.add_attribute(attribute=deleted_attribute)
947
+
948
+ if self._display_label:
949
+ if deleted_attribute := await self._display_label.get_node_attribute(node=self, at=delete_at).delete(
950
+ at=delete_at, db=db
951
+ ):
813
952
  node_changelog.add_attribute(attribute=deleted_attribute)
814
953
 
815
954
  branch = self.get_branch_based_on_support_type()
@@ -877,7 +1016,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
877
1016
  continue
878
1017
 
879
1018
  if field_name == "display_label":
880
- response[field_name] = await self.render_display_label(db=db)
1019
+ response[field_name] = await self.get_display_label(db=db)
881
1020
  continue
882
1021
 
883
1022
  if field_name == "hfid":
@@ -937,7 +1076,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
937
1076
 
938
1077
  return response
939
1078
 
940
- async def from_graphql(self, data: dict, db: InfrahubDatabase) -> bool:
1079
+ async def from_graphql(self, data: dict, db: InfrahubDatabase, process_pools: bool = True) -> bool:
941
1080
  """Update object from a GraphQL payload."""
942
1081
 
943
1082
  changed = False
@@ -945,7 +1084,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
945
1084
  for key, value in data.items():
946
1085
  if key in self._attributes and isinstance(value, dict):
947
1086
  attribute = getattr(self, key)
948
- changed |= await attribute.from_graphql(data=value, db=db)
1087
+ changed |= await attribute.from_graphql(data=value, db=db, process_pools=process_pools)
949
1088
 
950
1089
  if key in self._relationships:
951
1090
  rel: RelationshipManager = getattr(self, key)
@@ -980,6 +1119,20 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
980
1119
  return repr(self)
981
1120
  return display_label.strip()
982
1121
 
1122
+ async def set_human_friendly_id(self, value: list[str] | None) -> None:
1123
+ """Set the human friendly ID of this node if one is set. `save()` must be called to commit the change in the database."""
1124
+ if self._human_friendly_id is None:
1125
+ return
1126
+
1127
+ self._human_friendly_id.set_value(value=value, manually_assigned=True)
1128
+
1129
+ async def set_display_label(self, value: str | None) -> None:
1130
+ """Set the display label of this node if one is set. `save()` must be called to commit the change in the database."""
1131
+ if self._display_label is None:
1132
+ return
1133
+
1134
+ self._display_label.set_value(value=value, manually_assigned=True)
1135
+
983
1136
  def _get_parent_relationship_name(self) -> str | None:
984
1137
  """Return the name of the parent relationship is one is present"""
985
1138
  for relationship in self._schema.relationships:
@@ -989,7 +1142,7 @@ class Node(BaseNode, metaclass=BaseNodeMeta):
989
1142
  return None
990
1143
 
991
1144
  async def get_object_template(self, db: InfrahubDatabase) -> CoreObjectTemplate | None:
992
- object_template: RelationshipManager = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
1145
+ object_template: RelationshipManager | None = getattr(self, OBJECT_TEMPLATE_RELATIONSHIP_NAME, None)
993
1146
  return (
994
1147
  await object_template.get_peer(db=db, peer_type=CoreObjectTemplate) if object_template is not None else None
995
1148
  )