infrahub-server 1.6.2__py3-none-any.whl → 1.7.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 (253) hide show
  1. infrahub/actions/tasks.py +4 -2
  2. infrahub/api/exceptions.py +2 -2
  3. infrahub/api/schema.py +3 -1
  4. infrahub/artifacts/tasks.py +1 -0
  5. infrahub/auth.py +2 -2
  6. infrahub/cli/db.py +54 -28
  7. infrahub/computed_attribute/gather.py +3 -4
  8. infrahub/computed_attribute/tasks.py +23 -6
  9. infrahub/config.py +8 -0
  10. infrahub/constants/enums.py +12 -0
  11. infrahub/core/account.py +12 -9
  12. infrahub/core/attribute.py +106 -108
  13. infrahub/core/branch/models.py +44 -71
  14. infrahub/core/branch/tasks.py +5 -3
  15. infrahub/core/changelog/diff.py +1 -20
  16. infrahub/core/changelog/models.py +0 -7
  17. infrahub/core/constants/__init__.py +17 -0
  18. infrahub/core/constants/database.py +0 -1
  19. infrahub/core/constants/schema.py +0 -1
  20. infrahub/core/convert_object_type/repository_conversion.py +3 -4
  21. infrahub/core/diff/branch_differ.py +1 -1
  22. infrahub/core/diff/conflict_transferer.py +1 -1
  23. infrahub/core/diff/data_check_synchronizer.py +4 -3
  24. infrahub/core/diff/enricher/cardinality_one.py +2 -2
  25. infrahub/core/diff/enricher/hierarchy.py +1 -1
  26. infrahub/core/diff/enricher/labels.py +1 -1
  27. infrahub/core/diff/merger/merger.py +28 -2
  28. infrahub/core/diff/merger/serializer.py +3 -10
  29. infrahub/core/diff/model/diff.py +1 -1
  30. infrahub/core/diff/query/merge.py +376 -135
  31. infrahub/core/diff/repository/repository.py +3 -1
  32. infrahub/core/graph/__init__.py +1 -1
  33. infrahub/core/graph/constraints.py +3 -3
  34. infrahub/core/graph/schema.py +2 -12
  35. infrahub/core/ipam/reconciler.py +8 -6
  36. infrahub/core/ipam/utilization.py +8 -15
  37. infrahub/core/manager.py +133 -152
  38. infrahub/core/merge.py +1 -1
  39. infrahub/core/metadata/__init__.py +0 -0
  40. infrahub/core/metadata/interface.py +37 -0
  41. infrahub/core/metadata/model.py +31 -0
  42. infrahub/core/metadata/query/__init__.py +0 -0
  43. infrahub/core/metadata/query/node_metadata.py +301 -0
  44. infrahub/core/migrations/graph/__init__.py +4 -0
  45. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
  46. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
  47. infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
  48. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
  49. infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
  50. infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
  51. infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
  52. infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
  53. infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
  54. infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
  55. infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
  56. infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
  57. infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
  58. infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
  59. infrahub/core/migrations/query/__init__.py +2 -2
  60. infrahub/core/migrations/query/attribute_add.py +17 -6
  61. infrahub/core/migrations/query/attribute_remove.py +19 -5
  62. infrahub/core/migrations/query/attribute_rename.py +21 -5
  63. infrahub/core/migrations/query/node_duplicate.py +19 -4
  64. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  65. infrahub/core/migrations/schema/attribute_kind_update.py +26 -6
  66. infrahub/core/migrations/schema/attribute_name_update.py +1 -1
  67. infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
  68. infrahub/core/migrations/schema/models.py +3 -0
  69. infrahub/core/migrations/schema/node_attribute_add.py +5 -2
  70. infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
  71. infrahub/core/migrations/schema/node_kind_update.py +1 -1
  72. infrahub/core/migrations/schema/node_remove.py +24 -2
  73. infrahub/core/migrations/schema/tasks.py +4 -1
  74. infrahub/core/migrations/shared.py +13 -6
  75. infrahub/core/models.py +6 -6
  76. infrahub/core/node/__init__.py +157 -58
  77. infrahub/core/node/base.py +9 -5
  78. infrahub/core/node/create.py +7 -3
  79. infrahub/core/node/delete_validator.py +1 -1
  80. infrahub/core/node/standard.py +100 -14
  81. infrahub/core/order.py +30 -0
  82. infrahub/core/property.py +0 -1
  83. infrahub/core/protocols.py +1 -0
  84. infrahub/core/protocols_base.py +10 -2
  85. infrahub/core/query/__init__.py +11 -6
  86. infrahub/core/query/attribute.py +164 -49
  87. infrahub/core/query/branch.py +58 -70
  88. infrahub/core/query/delete.py +1 -1
  89. infrahub/core/query/diff.py +7 -7
  90. infrahub/core/query/ipam.py +104 -43
  91. infrahub/core/query/node.py +1072 -281
  92. infrahub/core/query/relationship.py +531 -325
  93. infrahub/core/query/resource_manager.py +107 -18
  94. infrahub/core/query/standard_node.py +25 -5
  95. infrahub/core/query/utils.py +2 -4
  96. infrahub/core/relationship/constraints/count.py +1 -1
  97. infrahub/core/relationship/constraints/peer_kind.py +1 -1
  98. infrahub/core/relationship/constraints/peer_parent.py +1 -1
  99. infrahub/core/relationship/constraints/peer_relatives.py +1 -1
  100. infrahub/core/relationship/constraints/profiles_kind.py +1 -1
  101. infrahub/core/relationship/constraints/profiles_removal.py +168 -0
  102. infrahub/core/relationship/model.py +293 -139
  103. infrahub/core/schema/attribute_parameters.py +28 -1
  104. infrahub/core/schema/attribute_schema.py +11 -17
  105. infrahub/core/schema/basenode_schema.py +3 -0
  106. infrahub/core/schema/definitions/core/__init__.py +8 -2
  107. infrahub/core/schema/definitions/core/account.py +10 -10
  108. infrahub/core/schema/definitions/core/artifact.py +14 -8
  109. infrahub/core/schema/definitions/core/check.py +10 -4
  110. infrahub/core/schema/definitions/core/generator.py +26 -6
  111. infrahub/core/schema/definitions/core/graphql_query.py +1 -1
  112. infrahub/core/schema/definitions/core/group.py +9 -2
  113. infrahub/core/schema/definitions/core/ipam.py +80 -10
  114. infrahub/core/schema/definitions/core/menu.py +41 -7
  115. infrahub/core/schema/definitions/core/permission.py +16 -2
  116. infrahub/core/schema/definitions/core/profile.py +16 -2
  117. infrahub/core/schema/definitions/core/propose_change.py +24 -4
  118. infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
  119. infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
  120. infrahub/core/schema/definitions/core/repository.py +10 -0
  121. infrahub/core/schema/definitions/core/resource_pool.py +8 -1
  122. infrahub/core/schema/definitions/core/template.py +19 -2
  123. infrahub/core/schema/definitions/core/transform.py +11 -5
  124. infrahub/core/schema/definitions/core/webhook.py +27 -9
  125. infrahub/core/schema/manager.py +63 -43
  126. infrahub/core/schema/relationship_schema.py +6 -2
  127. infrahub/core/schema/schema_branch.py +115 -11
  128. infrahub/core/task/task.py +4 -2
  129. infrahub/core/utils.py +3 -25
  130. infrahub/core/validators/aggregated_checker.py +1 -1
  131. infrahub/core/validators/attribute/choices.py +1 -1
  132. infrahub/core/validators/attribute/enum.py +1 -1
  133. infrahub/core/validators/attribute/kind.py +6 -3
  134. infrahub/core/validators/attribute/length.py +1 -1
  135. infrahub/core/validators/attribute/min_max.py +1 -1
  136. infrahub/core/validators/attribute/number_pool.py +1 -1
  137. infrahub/core/validators/attribute/optional.py +1 -1
  138. infrahub/core/validators/attribute/regex.py +1 -1
  139. infrahub/core/validators/determiner.py +3 -3
  140. infrahub/core/validators/node/attribute.py +1 -1
  141. infrahub/core/validators/node/relationship.py +1 -1
  142. infrahub/core/validators/relationship/peer.py +1 -1
  143. infrahub/database/__init__.py +4 -4
  144. infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
  145. infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
  146. infrahub/dependencies/registry.py +2 -0
  147. infrahub/display_labels/tasks.py +12 -3
  148. infrahub/git/integrator.py +18 -18
  149. infrahub/git/tasks.py +1 -1
  150. infrahub/git/utils.py +1 -1
  151. infrahub/graphql/app.py +2 -2
  152. infrahub/graphql/constants.py +3 -0
  153. infrahub/graphql/context.py +1 -1
  154. infrahub/graphql/field_extractor.py +1 -1
  155. infrahub/graphql/initialization.py +11 -0
  156. infrahub/graphql/loaders/account.py +134 -0
  157. infrahub/graphql/loaders/node.py +5 -12
  158. infrahub/graphql/loaders/peers.py +5 -7
  159. infrahub/graphql/manager.py +175 -21
  160. infrahub/graphql/metadata.py +91 -0
  161. infrahub/graphql/mutations/account.py +6 -6
  162. infrahub/graphql/mutations/attribute.py +0 -2
  163. infrahub/graphql/mutations/branch.py +9 -5
  164. infrahub/graphql/mutations/computed_attribute.py +1 -1
  165. infrahub/graphql/mutations/display_label.py +1 -1
  166. infrahub/graphql/mutations/hfid.py +1 -1
  167. infrahub/graphql/mutations/ipam.py +4 -6
  168. infrahub/graphql/mutations/main.py +9 -4
  169. infrahub/graphql/mutations/profile.py +16 -22
  170. infrahub/graphql/mutations/proposed_change.py +4 -4
  171. infrahub/graphql/mutations/relationship.py +40 -10
  172. infrahub/graphql/mutations/repository.py +14 -12
  173. infrahub/graphql/mutations/schema.py +2 -2
  174. infrahub/graphql/order.py +14 -0
  175. infrahub/graphql/queries/branch.py +62 -6
  176. infrahub/graphql/queries/diff/tree.py +5 -5
  177. infrahub/graphql/queries/resource_manager.py +25 -24
  178. infrahub/graphql/resolvers/account_metadata.py +84 -0
  179. infrahub/graphql/resolvers/ipam.py +6 -8
  180. infrahub/graphql/resolvers/many_relationship.py +77 -35
  181. infrahub/graphql/resolvers/resolver.py +59 -14
  182. infrahub/graphql/resolvers/single_relationship.py +87 -23
  183. infrahub/graphql/subscription/graphql_query.py +2 -0
  184. infrahub/graphql/types/__init__.py +0 -1
  185. infrahub/graphql/types/attribute.py +10 -5
  186. infrahub/graphql/types/branch.py +40 -53
  187. infrahub/graphql/types/enums.py +3 -0
  188. infrahub/graphql/types/metadata.py +28 -0
  189. infrahub/graphql/types/node.py +22 -2
  190. infrahub/graphql/types/relationship.py +10 -2
  191. infrahub/graphql/types/standard_node.py +12 -7
  192. infrahub/hfid/tasks.py +12 -3
  193. infrahub/lock.py +7 -0
  194. infrahub/menu/repository.py +1 -1
  195. infrahub/patch/queries/base.py +1 -1
  196. infrahub/pools/number.py +1 -8
  197. infrahub/profiles/gather.py +56 -0
  198. infrahub/profiles/mandatory_fields_checker.py +116 -0
  199. infrahub/profiles/models.py +66 -0
  200. infrahub/profiles/node_applier.py +154 -13
  201. infrahub/profiles/queries/get_profile_data.py +143 -31
  202. infrahub/profiles/tasks.py +79 -27
  203. infrahub/profiles/triggers.py +22 -0
  204. infrahub/proposed_change/action_checker.py +1 -1
  205. infrahub/proposed_change/tasks.py +4 -1
  206. infrahub/services/__init__.py +1 -1
  207. infrahub/services/adapters/cache/nats.py +1 -1
  208. infrahub/services/adapters/cache/redis.py +7 -0
  209. infrahub/tasks/artifact.py +1 -0
  210. infrahub/transformations/tasks.py +2 -2
  211. infrahub/trigger/catalogue.py +2 -0
  212. infrahub/trigger/models.py +1 -0
  213. infrahub/trigger/setup.py +3 -3
  214. infrahub/trigger/tasks.py +3 -0
  215. infrahub/validators/tasks.py +1 -0
  216. infrahub/webhook/gather.py +1 -1
  217. infrahub/webhook/models.py +1 -1
  218. infrahub/webhook/tasks.py +23 -7
  219. infrahub/workers/dependencies.py +9 -3
  220. infrahub/workers/infrahub_async.py +13 -4
  221. infrahub/workflows/catalogue.py +19 -0
  222. infrahub_sdk/analyzer.py +2 -2
  223. infrahub_sdk/branch.py +12 -39
  224. infrahub_sdk/checks.py +4 -4
  225. infrahub_sdk/client.py +36 -0
  226. infrahub_sdk/ctl/cli_commands.py +2 -1
  227. infrahub_sdk/ctl/graphql.py +15 -4
  228. infrahub_sdk/ctl/utils.py +2 -2
  229. infrahub_sdk/enums.py +6 -0
  230. infrahub_sdk/graphql/renderers.py +21 -0
  231. infrahub_sdk/graphql/utils.py +85 -0
  232. infrahub_sdk/node/attribute.py +12 -2
  233. infrahub_sdk/node/constants.py +12 -0
  234. infrahub_sdk/node/metadata.py +69 -0
  235. infrahub_sdk/node/node.py +65 -14
  236. infrahub_sdk/node/property.py +3 -0
  237. infrahub_sdk/node/related_node.py +37 -5
  238. infrahub_sdk/node/relationship.py +18 -1
  239. infrahub_sdk/operation.py +2 -2
  240. infrahub_sdk/schema/repository.py +1 -2
  241. infrahub_sdk/transforms.py +2 -2
  242. infrahub_sdk/types.py +18 -2
  243. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
  244. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +252 -231
  245. infrahub_testcontainers/container.py +3 -3
  246. infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
  247. infrahub_testcontainers/docker-compose.test.yml +13 -5
  248. infrahub_testcontainers/models.py +3 -3
  249. infrahub_testcontainers/performance_test.py +1 -1
  250. infrahub/graphql/models.py +0 -6
  251. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
  252. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
  253. {infrahub_server-1.6.2.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,24 +1,74 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from dataclasses import dataclass
3
4
  from typing import TYPE_CHECKING, Any, Generator
4
5
 
5
- from pydantic import BaseModel, ConfigDict
6
-
7
6
  from infrahub.core import registry
8
7
  from infrahub.core.constants import InfrahubKind, RelationshipStatus
9
- from infrahub.core.query import Query, QueryType
8
+ from infrahub.core.query import Query, QueryResult, QueryType
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from infrahub.core.protocols import CoreNumberPool
13
12
  from infrahub.database import InfrahubDatabase
14
13
 
15
14
 
16
- class NumberPoolIdentifierData(BaseModel):
17
- model_config = ConfigDict(frozen=True)
15
+ @dataclass(frozen=True)
16
+ class NumberPoolIdentifierData:
17
+ """Result containing a pool reservation value and its identifier."""
18
18
 
19
19
  value: int
20
20
  identifier: str
21
21
 
22
+ @classmethod
23
+ def from_db(cls, result: QueryResult) -> NumberPoolIdentifierData:
24
+ """Convert raw QueryResult to typed dataclass."""
25
+ return cls(
26
+ value=result.get_as_type("value", return_type=int),
27
+ identifier=result.get_as_type("identifier", return_type=str),
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class PoolIdentifierResult:
33
+ """Result from pool identifier queries containing allocation and identifier data."""
34
+
35
+ allocated_uuid: str
36
+ """UUID of the allocated resource (address or prefix)."""
37
+
38
+ identifier: str
39
+ """Identifier used for the reservation."""
40
+
41
+ @classmethod
42
+ def from_db(cls, result: QueryResult) -> PoolIdentifierResult:
43
+ """Convert raw QueryResult to typed dataclass."""
44
+ return cls(
45
+ allocated_uuid=result.get_as_type("allocated_uuid", str),
46
+ identifier=result.get_as_type("identifier", str),
47
+ )
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class NumberPoolAllocatedResult:
52
+ """Result from NumberPoolGetAllocated containing allocated number info."""
53
+
54
+ id: str
55
+ """UUID of the node with the allocated number."""
56
+
57
+ branch: str
58
+ """Branch where the allocation exists."""
59
+
60
+ value: int
61
+ """The allocated number value."""
62
+
63
+ @classmethod
64
+ def from_db(cls, result: QueryResult) -> NumberPoolAllocatedResult:
65
+ """Convert raw QueryResult to typed dataclass."""
66
+ return cls(
67
+ id=result.get_as_type("id", str),
68
+ branch=result.get_as_type("branch", str),
69
+ value=result.get_as_type("value", int),
70
+ )
71
+
22
72
 
23
73
  class IPAddressPoolGetIdentifiers(Query):
24
74
  name = "ipaddresspool_get_identifiers"
@@ -44,7 +94,15 @@ class IPAddressPoolGetIdentifiers(Query):
44
94
  WHERE allocated.uuid in $addresses
45
95
  """ % {"ipaddress_pool": InfrahubKind.IPADDRESSPOOL}
46
96
  self.add_to_query(query)
47
- self.return_labels = ["allocated", "reservation"]
97
+ self.return_labels = ["allocated.uuid AS allocated_uuid", "reservation.identifier AS identifier"]
98
+
99
+ def get_data(self) -> list[PoolIdentifierResult]:
100
+ """Return results as typed dataclass instances.
101
+
102
+ Returns:
103
+ List of PoolIdentifierResult containing allocation and identifier data.
104
+ """
105
+ return [PoolIdentifierResult.from_db(result) for result in self.get_results()]
48
106
 
49
107
 
50
108
  class IPAddressPoolGetReserved(Query):
@@ -159,6 +217,14 @@ class NumberPoolGetAllocated(Query):
159
217
  self.return_labels = ["n.uuid as id", "hv.branch as branch", "av.value as value"]
160
218
  self.order_by = ["av.value"]
161
219
 
220
+ def get_data(self) -> list[NumberPoolAllocatedResult]:
221
+ """Return results as typed dataclass instances.
222
+
223
+ Returns:
224
+ List of NumberPoolAllocatedResult containing allocated number info.
225
+ """
226
+ return [NumberPoolAllocatedResult.from_db(result) for result in self.get_results()]
227
+
162
228
 
163
229
  class NumberPoolGetReserved(Query):
164
230
  name = "numberpool_get_reserved"
@@ -205,17 +271,31 @@ class NumberPoolGetReserved(Query):
205
271
  self.return_labels = ["reservation.value AS value", "r.identifier AS identifier"]
206
272
 
207
273
  def get_reservation(self) -> int | None:
274
+ """Return the reserved value for a single identifier.
275
+
276
+ Returns:
277
+ The reserved integer value, or None if no reservation exists.
278
+ """
208
279
  result = self.get_result()
209
280
  if result:
210
281
  return result.get_as_optional_type("value", return_type=int)
211
282
  return None
212
283
 
284
+ def get_data(self) -> list[NumberPoolIdentifierData]:
285
+ """Return all reservations as typed dataclass instances.
286
+
287
+ Returns:
288
+ List of NumberPoolIdentifierData containing value and identifier.
289
+ """
290
+ return [NumberPoolIdentifierData.from_db(result) for result in self.get_results()]
291
+
213
292
  def get_reservations(self) -> Generator[NumberPoolIdentifierData]:
214
- for result in self.results:
215
- yield NumberPoolIdentifierData.model_construct(
216
- value=result.get_as_type("value", return_type=int),
217
- identifier=result.get_as_type("identifier", return_type=str),
218
- )
293
+ """Yield reservations as typed dataclass instances.
294
+
295
+ Yields:
296
+ NumberPoolIdentifierData for each reservation.
297
+ """
298
+ yield from self.get_data()
219
299
 
220
300
 
221
301
  class PoolChangeReserved(Query):
@@ -282,7 +362,6 @@ This will be especially important as we want to support upsert with NumberPool
282
362
  class NumberPoolGetUsed(Query):
283
363
  name = "number_pool_get_used"
284
364
  type = QueryType.READ
285
- return_model = NumberPoolIdentifierData
286
365
 
287
366
  def __init__(
288
367
  self,
@@ -331,11 +410,13 @@ class NumberPoolGetUsed(Query):
331
410
  self.order_by = ["value"]
332
411
 
333
412
  def iter_results(self) -> Generator[NumberPoolIdentifierData]:
334
- for result in self.results:
335
- yield self.return_model.model_construct(
336
- value=result.get_as_type("value", return_type=int),
337
- identifier=result.get_as_type("identifier", return_type=str),
338
- )
413
+ """Yield used pool values as typed dataclass instances.
414
+
415
+ Yields:
416
+ NumberPoolIdentifierData for each used value in the pool.
417
+ """
418
+ for result in self.get_results():
419
+ yield NumberPoolIdentifierData.from_db(result)
339
420
 
340
421
 
341
422
  class NumberPoolSetReserved(Query):
@@ -405,7 +486,15 @@ class PrefixPoolGetIdentifiers(Query):
405
486
  WHERE allocated.uuid in $prefixes
406
487
  """ % {"ipaddress_pool": InfrahubKind.IPPREFIXPOOL}
407
488
  self.add_to_query(query)
408
- self.return_labels = ["allocated", "reservation"]
489
+ self.return_labels = ["allocated.uuid AS allocated_uuid", "reservation.identifier AS identifier"]
490
+
491
+ def get_data(self) -> list[PoolIdentifierResult]:
492
+ """Return results as typed dataclass instances.
493
+
494
+ Returns:
495
+ List of PoolIdentifierResult containing allocation and identifier data.
496
+ """
497
+ return [PoolIdentifierResult.from_db(result) for result in self.get_results()]
409
498
 
410
499
 
411
500
  class PrefixPoolGetReserved(Query):
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING, Any
3
+ from typing import TYPE_CHECKING, Any, assert_never
4
4
 
5
+ from infrahub.constants.enums import OrderByField
5
6
  from infrahub.core.query import Query, QueryType
6
7
  from infrahub.exceptions import InitializationError
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from uuid import UUID
10
11
 
11
- from infrahub.core.node.standard import StandardNode
12
+ from infrahub.core.node.standard import StandardNode, StandardNodeOrdering
12
13
  from infrahub.database import InfrahubDatabase
13
14
 
14
15
 
@@ -135,11 +136,19 @@ class StandardNodeGetListQuery(Query):
135
136
  raw_filter: str | None = None
136
137
 
137
138
  def __init__(
138
- self, node_class: StandardNode, ids: list[str] | None = None, node_name: str | None = None, **kwargs: Any
139
+ self,
140
+ node_class: StandardNode,
141
+ node_ordering: StandardNodeOrdering,
142
+ ids: list[str] | None = None,
143
+ node_name: str | None = None,
144
+ partial_match: bool = False,
145
+ **kwargs: Any,
139
146
  ) -> None:
140
147
  self.ids = ids
141
148
  self.node_name = node_name
142
149
  self.node_class = node_class
150
+ self.partial_match = partial_match
151
+ self.node_ordering = node_ordering
143
152
 
144
153
  super().__init__(**kwargs)
145
154
 
@@ -148,9 +157,12 @@ class StandardNodeGetListQuery(Query):
148
157
  if self.ids:
149
158
  filters.append("n.uuid in $ids_value")
150
159
  self.params["ids_value"] = self.ids
151
- if self.node_name:
160
+ if self.node_name and not self.partial_match:
152
161
  filters.append("n.name = $name")
153
162
  self.params["name"] = self.node_name
163
+ if self.node_name and self.partial_match:
164
+ filters.append("toLower(toString(n.name)) CONTAINS toLower(toString($name))")
165
+ self.params["name"] = self.node_name
154
166
  if self.raw_filter:
155
167
  filters.append(self.raw_filter)
156
168
 
@@ -169,4 +181,12 @@ class StandardNodeGetListQuery(Query):
169
181
  self.add_to_query(query)
170
182
 
171
183
  self.return_labels = ["n"]
172
- self.order_by = [f"{db.get_id_function_name()}(n)"]
184
+ match self.node_ordering.order_by:
185
+ case OrderByField.ID:
186
+ self.order_by = [f"{db.get_id_function_name()}(n)"]
187
+ case OrderByField.CREATED_AT:
188
+ self.order_by = [f"n.created_at {self.node_ordering.direction.value}"]
189
+ case OrderByField.UPDATED_AT:
190
+ self.order_by = [f"n.updated_at {self.node_ordering.direction.value}"]
191
+ case _:
192
+ assert_never(self.node_ordering.order_by)
@@ -5,16 +5,14 @@ from typing import TYPE_CHECKING
5
5
  from infrahub.core.schema import NodeSchema, ProfileSchema, TemplateSchema
6
6
 
7
7
  if TYPE_CHECKING:
8
- from neo4j.graph import Node as Neo4jNode
9
-
10
8
  from infrahub.core.branch import Branch
11
9
  from infrahub.database import InfrahubDatabase
12
10
 
13
11
 
14
12
  def find_node_schema(
15
- db: InfrahubDatabase, node: Neo4jNode, branch: Branch | str, duplicate: bool = False
13
+ db: InfrahubDatabase, branch: Branch | str, labels: list[str], duplicate: bool = False
16
14
  ) -> NodeSchema | ProfileSchema | TemplateSchema | None:
17
- for label in node.labels:
15
+ for label in labels:
18
16
  if db.schema.has(name=label, branch=branch):
19
17
  schema = db.schema.get(name=label, branch=branch, duplicate=duplicate)
20
18
  if isinstance(schema, NodeSchema | ProfileSchema | TemplateSchema):
@@ -22,7 +22,7 @@ class NodeToValidate:
22
22
 
23
23
 
24
24
  class RelationshipCountConstraint(RelationshipManagerConstraintInterface):
25
- def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
25
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
26
26
  self.db = db
27
27
  self.branch = branch
28
28
 
@@ -23,7 +23,7 @@ class NodeToValidate:
23
23
 
24
24
 
25
25
  class RelationshipPeerKindConstraint(RelationshipManagerConstraintInterface):
26
- def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
26
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
27
27
  self.db = db
28
28
  self.branch = branch
29
29
 
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
16
16
 
17
17
 
18
18
  class RelationshipPeerParentConstraint(RelationshipManagerConstraintInterface):
19
- def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
19
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
20
20
  self.db = db
21
21
  self.branch = branch
22
22
 
@@ -25,7 +25,7 @@ class NodeToValidate:
25
25
 
26
26
 
27
27
  class RelationshipPeerRelativesConstraint(RelationshipManagerConstraintInterface):
28
- def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
28
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
29
29
  self.db = db
30
30
  self.branch = branch
31
31
 
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
 
19
19
 
20
20
  class RelationshipProfilesKindConstraint(RelationshipManagerConstraintInterface):
21
- def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
21
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
22
22
  self.db = db
23
23
  self.branch = branch
24
24
  self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.constants import MetadataOptions
7
+ from infrahub.core.manager import NodeManager
8
+ from infrahub.core.schema import NodeSchema, ProfileSchema
9
+ from infrahub.exceptions import ValidationError
10
+
11
+ from .interface import RelationshipManagerConstraintInterface
12
+
13
+ if TYPE_CHECKING:
14
+ from infrahub.core.branch import Branch
15
+ from infrahub.core.node import Node
16
+ from infrahub.core.relationship.model import Relationship, RelationshipManager
17
+ from infrahub.core.schema import MainSchemaTypes
18
+ from infrahub.database import InfrahubDatabase
19
+
20
+
21
+ class RelationshipProfileRemovalConstraint(RelationshipManagerConstraintInterface):
22
+ """Constraint that validates removing profiles from a node doesn't violate required relationships.
23
+
24
+ This runs in two cases:
25
+ 1. When a node's `profiles` relationship is changed.
26
+ 2. When a profile's `related_nodes` relationship is changed.
27
+
28
+ In both cases, it will perform checks only if peers are being removed from the relationship being changed.
29
+ """
30
+
31
+ def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
32
+ self.db = db
33
+ self.branch = branch
34
+ self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
35
+
36
+ def _get_required_attributes_names(self, schema: NodeSchema) -> set[str]:
37
+ attr_names: set[str] = set()
38
+ for attr_schema in schema.attributes:
39
+ if attr_schema.support_profiles and not attr_schema.optional:
40
+ attr_names.add(attr_schema.name)
41
+ return attr_names
42
+
43
+ def _get_required_relationship_names(self, schema: NodeSchema) -> set[str]:
44
+ rel_names: set[str] = set()
45
+ for rel_schema in schema.relationships:
46
+ if rel_schema.support_profiles and not rel_schema.optional:
47
+ rel_names.add(rel_schema.name)
48
+ return rel_names
49
+
50
+ async def _validate_profile_removal(
51
+ self, node: Node, profile_id: str, required_attr_names: set[str], required_rel_names: set[str]
52
+ ) -> None:
53
+ for attr_name in required_attr_names:
54
+ attr = node.get_attribute(name=attr_name)
55
+ if attr.is_from_profile:
56
+ source = await attr.get_source(db=self.db)
57
+ if source and source.id == profile_id:
58
+ node_display_label = await node.get_display_label(db=self.db)
59
+ node_reference = f"node '{node_display_label}' (ID: {node.get_id()})"
60
+ raise ValidationError(
61
+ f"Cannot remove profile '{profile_id}' because {node_reference} "
62
+ f"inherits required attribute '{attr_name}' from this profile."
63
+ )
64
+
65
+ for rel_name in required_rel_names:
66
+ rel_manager = node.get_relationship(name=rel_name)
67
+
68
+ relationships: list[Relationship] = await rel_manager.get_relationships(db=self.db)
69
+ for rel in relationships:
70
+ if rel.is_from_profile:
71
+ source = await rel.get_source(db=self.db)
72
+ if source and source.id == profile_id:
73
+ node_display_label = await node.get_display_label(db=self.db)
74
+ node_reference = f"node '{node_display_label}' (ID: {node.get_id()})"
75
+ raise ValidationError(
76
+ f"Cannot remove profile '{profile_id}' because {node_reference} "
77
+ f"inherits required relationship '{rel_name}' from this profile."
78
+ )
79
+
80
+ async def _check_node_profiles_removal(
81
+ self, relm: RelationshipManager, node_schema: NodeSchema, node: Node
82
+ ) -> None:
83
+ required_attr_names = self._get_required_attributes_names(schema=node_schema)
84
+ required_rel_names = self._get_required_relationship_names(schema=node_schema)
85
+ if not required_attr_names and not required_rel_names:
86
+ return
87
+
88
+ relm_update_details = await relm.fetch_relationship_ids(db=self.db, force_refresh=False)
89
+ if not relm_update_details.peer_ids_present_database_only:
90
+ return
91
+
92
+ if required_attr_names:
93
+ # Required to get source for attributes
94
+ node = await NodeManager.get_one(
95
+ db=self.db, branch=self.branch, id=node.get_id(), include_metadata=MetadataOptions.SOURCE
96
+ )
97
+
98
+ for profile_id in relm_update_details.peer_ids_present_database_only:
99
+ await self._validate_profile_removal(
100
+ node=node,
101
+ profile_id=profile_id,
102
+ required_attr_names=required_attr_names,
103
+ required_rel_names=required_rel_names,
104
+ )
105
+
106
+ async def _check_profile_related_nodes_removal(
107
+ self, relm: RelationshipManager, profile_schema: ProfileSchema, profile: Node
108
+ ) -> None:
109
+ relm_update_details = await relm.fetch_relationship_ids(db=self.db, force_refresh=False)
110
+ if not relm_update_details.peer_ids_present_database_only:
111
+ return
112
+
113
+ target_kind = profile_schema.get_relationship(name="related_nodes").peer
114
+ target_schema = self.schema_branch.get_node(name=target_kind, duplicate=False)
115
+
116
+ required_attr_names = self._get_required_attributes_names(schema=target_schema)
117
+ required_rel_names = self._get_required_relationship_names(schema=target_schema)
118
+ if not required_attr_names and not required_rel_names:
119
+ return
120
+
121
+ nodes = await NodeManager.get_many(
122
+ db=self.db,
123
+ branch=self.branch,
124
+ ids=relm_update_details.peer_ids_present_database_only,
125
+ include_metadata=MetadataOptions.SOURCE,
126
+ )
127
+ for node in nodes.values():
128
+ await self._validate_profile_removal(
129
+ node=node,
130
+ profile_id=profile.get_id(),
131
+ required_attr_names=required_attr_names,
132
+ required_rel_names=required_rel_names,
133
+ )
134
+
135
+ async def check(self, relm: RelationshipManager, node_schema: MainSchemaTypes, node: Node) -> None:
136
+ if relm.name == "profiles" and isinstance(node_schema, NodeSchema):
137
+ await self._check_node_profiles_removal(relm=relm, node_schema=node_schema, node=node)
138
+ return
139
+
140
+ if relm.name == "related_nodes" and isinstance(node_schema, ProfileSchema):
141
+ await self._check_profile_related_nodes_removal(relm=relm, profile_schema=node_schema, profile=node)
142
+ return
143
+
144
+ async def validate_profile_deletion(self, profile: Node, profile_schema: ProfileSchema) -> None:
145
+ related_nodes_rels = await profile.related_nodes.get_relationships(db=self.db) # type: ignore[attr-defined]
146
+ related_node_ids = [rel.peer_id for rel in related_nodes_rels if rel.peer_id]
147
+
148
+ if not related_node_ids:
149
+ return
150
+
151
+ target_kind = profile_schema.get_relationship(name="related_nodes").peer
152
+ target_schema = self.schema_branch.get_node(name=target_kind, duplicate=False)
153
+
154
+ required_attr_names = self._get_required_attributes_names(schema=target_schema)
155
+ required_rel_names = self._get_required_relationship_names(schema=target_schema)
156
+ if not required_attr_names and not required_rel_names:
157
+ return
158
+
159
+ nodes = await NodeManager.get_many(
160
+ db=self.db, branch=self.branch, ids=related_node_ids, include_metadata=MetadataOptions.SOURCE
161
+ )
162
+ for node in nodes.values():
163
+ await self._validate_profile_removal(
164
+ node=node,
165
+ profile_id=profile.get_id(),
166
+ required_attr_names=required_attr_names,
167
+ required_rel_names=required_rel_names,
168
+ )