infrahub-server 1.7.0b0__py3-none-any.whl → 1.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/api/exceptions.py +2 -2
- infrahub/api/schema.py +5 -0
- infrahub/cli/db.py +54 -24
- infrahub/core/account.py +12 -9
- infrahub/core/branch/models.py +11 -117
- infrahub/core/branch/tasks.py +7 -3
- infrahub/core/diff/branch_differ.py +1 -1
- infrahub/core/diff/conflict_transferer.py +1 -1
- infrahub/core/diff/data_check_synchronizer.py +1 -1
- infrahub/core/diff/enricher/cardinality_one.py +1 -1
- infrahub/core/diff/enricher/hierarchy.py +1 -1
- infrahub/core/diff/enricher/labels.py +1 -1
- infrahub/core/diff/merger/merger.py +6 -2
- infrahub/core/diff/repository/repository.py +3 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +1 -1
- infrahub/core/initialization.py +2 -1
- infrahub/core/ipam/reconciler.py +8 -6
- infrahub/core/ipam/utilization.py +8 -15
- infrahub/core/manager.py +1 -26
- infrahub/core/merge.py +1 -1
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +4 -4
- infrahub/core/migrations/graph/m014_remove_index_attr_value.py +3 -2
- infrahub/core/migrations/graph/m015_diff_format_update.py +3 -2
- infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +3 -2
- infrahub/core/migrations/graph/m017_add_core_profile.py +6 -4
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -3
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +3 -4
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m028_delete_diffs.py +3 -2
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +3 -2
- infrahub/core/migrations/graph/m031_check_number_attributes.py +4 -3
- infrahub/core/migrations/graph/m032_cleanup_orphaned_branch_relationships.py +3 -2
- infrahub/core/migrations/graph/m034_find_orphaned_schema_fields.py +3 -2
- infrahub/core/migrations/graph/m035_orphan_relationships.py +3 -3
- infrahub/core/migrations/graph/m036_drop_attr_value_index.py +3 -2
- infrahub/core/migrations/graph/m037_index_attr_vals.py +3 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +4 -5
- infrahub/core/migrations/graph/m039_ipam_reconcile.py +3 -2
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +4 -3
- infrahub/core/migrations/graph/m042_profile_attrs_in_db.py +5 -4
- infrahub/core/migrations/graph/m043_create_hfid_display_label_in_db.py +12 -5
- infrahub/core/migrations/graph/m044_backfill_hfid_display_label_in_db.py +15 -4
- infrahub/core/migrations/graph/m045_backfill_hfid_display_label_in_db_profile_template.py +10 -4
- infrahub/core/migrations/graph/m046_fill_agnostic_hfid_display_labels.py +6 -5
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +19 -5
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +6 -4
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +19 -4
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +3 -3
- infrahub/core/migrations/graph/m051_subtract_branched_from_microsecond.py +39 -0
- infrahub/core/migrations/query/__init__.py +2 -2
- infrahub/core/migrations/query/schema_attribute_update.py +1 -1
- infrahub/core/migrations/runner.py +6 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
- infrahub/core/migrations/schema/attribute_name_update.py +1 -1
- infrahub/core/migrations/schema/attribute_supports_profile.py +5 -10
- infrahub/core/migrations/schema/models.py +8 -0
- infrahub/core/migrations/schema/node_attribute_add.py +11 -14
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
- infrahub/core/migrations/schema/node_kind_update.py +1 -1
- infrahub/core/migrations/schema/tasks.py +7 -1
- infrahub/core/migrations/shared.py +37 -30
- infrahub/core/node/__init__.py +3 -2
- infrahub/core/node/base.py +9 -5
- infrahub/core/node/delete_validator.py +1 -1
- infrahub/core/order.py +30 -0
- infrahub/core/protocols.py +1 -0
- infrahub/core/protocols_base.py +4 -0
- infrahub/core/query/__init__.py +8 -5
- infrahub/core/query/attribute.py +3 -3
- infrahub/core/query/branch.py +1 -1
- infrahub/core/query/delete.py +1 -1
- infrahub/core/query/diff.py +3 -3
- infrahub/core/query/ipam.py +104 -43
- infrahub/core/query/node.py +454 -101
- infrahub/core/query/relationship.py +83 -26
- infrahub/core/query/resource_manager.py +107 -18
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/constraints/peer_kind.py +1 -1
- infrahub/core/relationship/constraints/peer_parent.py +1 -1
- infrahub/core/relationship/constraints/peer_relatives.py +1 -1
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/relationship/constraints/profiles_removal.py +1 -1
- infrahub/core/relationship/model.py +8 -2
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +9 -15
- infrahub/core/schema/basenode_schema.py +3 -0
- infrahub/core/schema/definitions/core/__init__.py +8 -2
- infrahub/core/schema/definitions/core/account.py +10 -10
- infrahub/core/schema/definitions/core/artifact.py +14 -8
- infrahub/core/schema/definitions/core/check.py +10 -4
- infrahub/core/schema/definitions/core/generator.py +26 -6
- infrahub/core/schema/definitions/core/graphql_query.py +1 -1
- infrahub/core/schema/definitions/core/group.py +9 -2
- infrahub/core/schema/definitions/core/ipam.py +80 -10
- infrahub/core/schema/definitions/core/menu.py +41 -7
- infrahub/core/schema/definitions/core/permission.py +16 -2
- infrahub/core/schema/definitions/core/profile.py +16 -2
- infrahub/core/schema/definitions/core/propose_change.py +24 -4
- infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
- infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
- infrahub/core/schema/definitions/core/repository.py +10 -0
- infrahub/core/schema/definitions/core/resource_pool.py +8 -1
- infrahub/core/schema/definitions/core/template.py +19 -2
- infrahub/core/schema/definitions/core/transform.py +11 -5
- infrahub/core/schema/definitions/core/webhook.py +27 -9
- infrahub/core/schema/manager.py +50 -38
- infrahub/core/schema/schema_branch.py +68 -2
- infrahub/core/utils.py +3 -3
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/attribute/choices.py +1 -1
- infrahub/core/validators/attribute/enum.py +1 -1
- infrahub/core/validators/attribute/kind.py +6 -3
- infrahub/core/validators/attribute/length.py +1 -1
- infrahub/core/validators/attribute/min_max.py +1 -1
- infrahub/core/validators/attribute/number_pool.py +1 -1
- infrahub/core/validators/attribute/optional.py +1 -1
- infrahub/core/validators/attribute/regex.py +1 -1
- infrahub/core/validators/node/attribute.py +1 -1
- infrahub/core/validators/node/relationship.py +1 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/database/__init__.py +1 -1
- infrahub/git/utils.py +1 -1
- infrahub/graphql/app.py +2 -2
- infrahub/graphql/field_extractor.py +1 -1
- infrahub/graphql/manager.py +17 -3
- infrahub/graphql/mutations/account.py +1 -1
- infrahub/graphql/order.py +14 -0
- infrahub/graphql/queries/diff/tree.py +5 -5
- infrahub/graphql/queries/resource_manager.py +25 -24
- infrahub/graphql/resolvers/ipam.py +3 -3
- infrahub/graphql/resolvers/resolver.py +44 -3
- infrahub/graphql/types/standard_node.py +8 -4
- infrahub/lock.py +7 -0
- infrahub/menu/repository.py +1 -1
- infrahub/patch/queries/base.py +1 -1
- infrahub/pools/number.py +1 -8
- infrahub/profiles/node_applier.py +1 -1
- infrahub/profiles/queries/get_profile_data.py +1 -1
- infrahub/proposed_change/action_checker.py +1 -1
- infrahub/services/__init__.py +1 -1
- infrahub/services/adapters/cache/nats.py +1 -1
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/webhook/gather.py +1 -1
- infrahub/webhook/tasks.py +22 -6
- infrahub_sdk/analyzer.py +2 -2
- infrahub_sdk/branch.py +12 -39
- infrahub_sdk/checks.py +4 -4
- infrahub_sdk/client.py +36 -0
- infrahub_sdk/ctl/cli_commands.py +2 -1
- infrahub_sdk/ctl/graphql.py +15 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/enums.py +6 -0
- infrahub_sdk/graphql/renderers.py +21 -0
- infrahub_sdk/graphql/utils.py +85 -0
- infrahub_sdk/node/attribute.py +12 -2
- infrahub_sdk/node/constants.py +11 -0
- infrahub_sdk/node/metadata.py +69 -0
- infrahub_sdk/node/node.py +65 -14
- infrahub_sdk/node/property.py +3 -0
- infrahub_sdk/node/related_node.py +24 -1
- infrahub_sdk/node/relationship.py +10 -1
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/schema/repository.py +1 -2
- infrahub_sdk/transforms.py +2 -2
- infrahub_sdk/types.py +18 -2
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/METADATA +6 -6
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/RECORD +176 -172
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/entry_points.txt +0 -1
- infrahub_testcontainers/models.py +3 -3
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/graphql/models.py +0 -36
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.7.0b0.dist-info → infrahub_server-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -14,7 +14,7 @@ from infrahub.core.changelog.models import (
|
|
|
14
14
|
)
|
|
15
15
|
from infrahub.core.constants import InfrahubKind, MetadataOptions, RelationshipDirection, RelationshipStatus
|
|
16
16
|
from infrahub.core.constants.database import DatabaseEdgeType
|
|
17
|
-
from infrahub.core.query import Query, QueryType
|
|
17
|
+
from infrahub.core.query import Query, QueryResult, QueryType
|
|
18
18
|
from infrahub.core.query.subquery import build_subquery_filter, build_subquery_order
|
|
19
19
|
from infrahub.core.timestamp import Timestamp
|
|
20
20
|
from infrahub.core.utils import extract_field_filters
|
|
@@ -159,7 +159,7 @@ class RelationshipQuery(Query):
|
|
|
159
159
|
branch: Branch | None = None,
|
|
160
160
|
at: Timestamp | str | None = None,
|
|
161
161
|
**kwargs,
|
|
162
|
-
):
|
|
162
|
+
) -> None:
|
|
163
163
|
if not source and not source_id:
|
|
164
164
|
raise ValueError("Either source or source_id must be provided.")
|
|
165
165
|
if not rel and not rel_id:
|
|
@@ -278,7 +278,7 @@ class RelationshipCreateQuery(RelationshipQuery):
|
|
|
278
278
|
destination: Node = None,
|
|
279
279
|
destination_id: UUID | None = None,
|
|
280
280
|
**kwargs,
|
|
281
|
-
):
|
|
281
|
+
) -> None:
|
|
282
282
|
if not destination and not destination_id:
|
|
283
283
|
raise ValueError("Either destination or destination_id must be provided.")
|
|
284
284
|
|
|
@@ -399,7 +399,7 @@ class RelationshipUpdatePropertyQuery(RelationshipQuery):
|
|
|
399
399
|
flag_properties_to_update: dict[str, bool],
|
|
400
400
|
node_properties_to_update: dict[str, str],
|
|
401
401
|
**kwargs,
|
|
402
|
-
):
|
|
402
|
+
) -> None:
|
|
403
403
|
if not flag_properties_to_update and not node_properties_to_update:
|
|
404
404
|
raise ValueError("Either flag_properties_to_update or node_properties_to_update must be set")
|
|
405
405
|
self.flag_properties_to_update = flag_properties_to_update
|
|
@@ -557,7 +557,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
|
|
|
557
557
|
insert_return = False
|
|
558
558
|
raise_error_if_empty = False
|
|
559
559
|
|
|
560
|
-
def __init__(self, source_branch: Branch, destination_branch: Branch, **kwargs):
|
|
560
|
+
def __init__(self, source_branch: Branch, destination_branch: Branch, **kwargs) -> None:
|
|
561
561
|
self.source_branch = source_branch
|
|
562
562
|
self.destination_branch = destination_branch
|
|
563
563
|
super().__init__(**kwargs)
|
|
@@ -690,7 +690,7 @@ class RelationshipGetPeerQuery(Query):
|
|
|
690
690
|
at: Timestamp | str | None = None,
|
|
691
691
|
include_metadata: MetadataOptions = MetadataOptions.NONE,
|
|
692
692
|
**kwargs,
|
|
693
|
-
):
|
|
693
|
+
) -> None:
|
|
694
694
|
if not source and not source_ids:
|
|
695
695
|
raise ValueError("Either source or source_ids must be provided.")
|
|
696
696
|
if not rel and not rel_type:
|
|
@@ -1124,6 +1124,25 @@ class RelationshipGetByIdentifierQuery(Query):
|
|
|
1124
1124
|
yield data
|
|
1125
1125
|
|
|
1126
1126
|
|
|
1127
|
+
@dataclass(frozen=True)
|
|
1128
|
+
class RelationshipCountPerNodeResult:
|
|
1129
|
+
"""Result from RelationshipCountPerNodeQuery containing peer count info."""
|
|
1130
|
+
|
|
1131
|
+
peer_uuid: str
|
|
1132
|
+
"""UUID of the peer node."""
|
|
1133
|
+
|
|
1134
|
+
count: int
|
|
1135
|
+
"""Number of relationship peers for this node."""
|
|
1136
|
+
|
|
1137
|
+
@classmethod
|
|
1138
|
+
def from_db(cls, result: QueryResult) -> RelationshipCountPerNodeResult:
|
|
1139
|
+
"""Convert raw QueryResult to typed dataclass."""
|
|
1140
|
+
return cls(
|
|
1141
|
+
peer_uuid=result.get_as_type("peer_node.uuid", str),
|
|
1142
|
+
count=result.get_as_type("nbr_peers", int),
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
|
|
1127
1146
|
class RelationshipCountPerNodeQuery(Query):
|
|
1128
1147
|
name = "relationship_count_per_node"
|
|
1129
1148
|
type: QueryType = QueryType.READ
|
|
@@ -1134,7 +1153,7 @@ class RelationshipCountPerNodeQuery(Query):
|
|
|
1134
1153
|
identifier: str,
|
|
1135
1154
|
direction: RelationshipDirection,
|
|
1136
1155
|
**kwargs,
|
|
1137
|
-
):
|
|
1156
|
+
) -> None:
|
|
1138
1157
|
self.node_ids = node_ids
|
|
1139
1158
|
self.identifier = identifier
|
|
1140
1159
|
self.direction = direction
|
|
@@ -1172,10 +1191,18 @@ class RelationshipCountPerNodeQuery(Query):
|
|
|
1172
1191
|
self.order_by = ["peer_node.uuid"]
|
|
1173
1192
|
self.return_labels = ["peer_node.uuid", "COUNT(peer_node.uuid) as nbr_peers"]
|
|
1174
1193
|
|
|
1194
|
+
def get_data(self) -> list[RelationshipCountPerNodeResult]:
|
|
1195
|
+
"""Return results as typed dataclass instances.
|
|
1196
|
+
|
|
1197
|
+
Returns:
|
|
1198
|
+
List of RelationshipCountPerNodeResult containing peer count info.
|
|
1199
|
+
"""
|
|
1200
|
+
return [RelationshipCountPerNodeResult.from_db(result) for result in self.get_results()]
|
|
1201
|
+
|
|
1175
1202
|
async def get_count_per_peer(self) -> dict[str, int]:
|
|
1176
1203
|
data: dict[str, int] = {}
|
|
1177
|
-
for
|
|
1178
|
-
data[
|
|
1204
|
+
for item in self.get_data():
|
|
1205
|
+
data[item.peer_uuid] = item.count
|
|
1179
1206
|
|
|
1180
1207
|
for node_id in self.node_ids:
|
|
1181
1208
|
if node_id not in data:
|
|
@@ -1184,6 +1211,33 @@ class RelationshipCountPerNodeQuery(Query):
|
|
|
1184
1211
|
return data
|
|
1185
1212
|
|
|
1186
1213
|
|
|
1214
|
+
@dataclass(frozen=True)
|
|
1215
|
+
class RelationshipDeleteAllQueryResult:
|
|
1216
|
+
"""Result from RelationshipDeleteAllQuery containing deleted relationship info."""
|
|
1217
|
+
|
|
1218
|
+
uuid: str
|
|
1219
|
+
"""UUID of the peer node whose relationship was deleted."""
|
|
1220
|
+
|
|
1221
|
+
kind: str
|
|
1222
|
+
"""Kind/type of the peer node."""
|
|
1223
|
+
|
|
1224
|
+
rel_identifier: str
|
|
1225
|
+
"""Relationship schema identifier name."""
|
|
1226
|
+
|
|
1227
|
+
rel_direction: str
|
|
1228
|
+
"""Direction of the relationship ("outbound" or "inbound")."""
|
|
1229
|
+
|
|
1230
|
+
@classmethod
|
|
1231
|
+
def from_db(cls, result: QueryResult) -> RelationshipDeleteAllQueryResult:
|
|
1232
|
+
"""Convert raw QueryResult to typed dataclass."""
|
|
1233
|
+
return cls(
|
|
1234
|
+
uuid=result.get_as_type("uuid", str),
|
|
1235
|
+
kind=result.get_as_type("kind", str),
|
|
1236
|
+
rel_identifier=result.get_as_type("rel_identifier", str),
|
|
1237
|
+
rel_direction=result.get_as_type("rel_direction", str),
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
|
|
1187
1241
|
class RelationshipDeleteAllQuery(Query):
|
|
1188
1242
|
"""
|
|
1189
1243
|
Delete all relationships linked to a given node on a given branch at a given time. For every IS_RELATED edge:
|
|
@@ -1198,7 +1252,7 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1198
1252
|
type = QueryType.WRITE
|
|
1199
1253
|
insert_return = False
|
|
1200
1254
|
|
|
1201
|
-
def __init__(self, node_id: str, **kwargs):
|
|
1255
|
+
def __init__(self, node_id: str, **kwargs) -> None:
|
|
1202
1256
|
self.node_id = node_id
|
|
1203
1257
|
super().__init__(**kwargs)
|
|
1204
1258
|
|
|
@@ -1313,51 +1367,54 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1313
1367
|
"peer_node_metadata_update": peer_node_metadata_update,
|
|
1314
1368
|
}
|
|
1315
1369
|
self.add_to_query(query)
|
|
1370
|
+
self.return_labels = ["uuid", "kind", "rel_identifier", "rel_direction"]
|
|
1371
|
+
|
|
1372
|
+
def get_data(self) -> list[RelationshipDeleteAllQueryResult]:
|
|
1373
|
+
"""Return results as typed dataclass instances.
|
|
1374
|
+
|
|
1375
|
+
Returns:
|
|
1376
|
+
List of RelationshipDeleteAllQueryResult containing deleted relationship info.
|
|
1377
|
+
"""
|
|
1378
|
+
return [RelationshipDeleteAllQueryResult.from_db(result) for result in self.get_results()]
|
|
1316
1379
|
|
|
1317
1380
|
def get_deleted_relationships_changelog(
|
|
1318
1381
|
self, node_schema: NodeSchema
|
|
1319
1382
|
) -> list[RelationshipCardinalityOneChangelog | RelationshipCardinalityManyChangelog]:
|
|
1320
|
-
rel_identifier_to_changelog_mapper = {}
|
|
1383
|
+
rel_identifier_to_changelog_mapper: dict[str, ChangelogRelationshipMapper] = {}
|
|
1321
1384
|
|
|
1322
|
-
for
|
|
1323
|
-
|
|
1324
|
-
if peer_uuid == self.node_id:
|
|
1385
|
+
for item in self.get_data():
|
|
1386
|
+
if item.uuid == self.node_id:
|
|
1325
1387
|
continue
|
|
1326
1388
|
|
|
1327
|
-
rel_identifier = result.data["rel_identifier"]
|
|
1328
|
-
kind = result.data["kind"]
|
|
1329
1389
|
deleted_rel_schemas = [
|
|
1330
|
-
rel_schema for rel_schema in node_schema.relationships if rel_schema.identifier == rel_identifier
|
|
1390
|
+
rel_schema for rel_schema in node_schema.relationships if rel_schema.identifier == item.rel_identifier
|
|
1331
1391
|
]
|
|
1332
1392
|
|
|
1333
1393
|
if len(deleted_rel_schemas) == 0:
|
|
1334
1394
|
continue # TODO Unidirectional relationship changelog should be handled, cf IFC-1319.
|
|
1335
1395
|
|
|
1336
1396
|
if len(deleted_rel_schemas) > 2:
|
|
1337
|
-
log.error(f"Duplicated relationship schema with identifier {rel_identifier}")
|
|
1397
|
+
log.error(f"Duplicated relationship schema with identifier {item.rel_identifier}")
|
|
1338
1398
|
continue
|
|
1339
1399
|
|
|
1340
1400
|
if len(deleted_rel_schemas) == 2:
|
|
1341
1401
|
# Hierarchical schema nodes have 2 relationships with `parent_child` identifiers,
|
|
1342
1402
|
# which are differentiated by their direction within the database.
|
|
1343
|
-
# assert rel_identifier != PARENT_CHILD_IDENTIFIER
|
|
1344
|
-
|
|
1345
|
-
rel_direction = result.data["rel_direction"]
|
|
1346
1403
|
deleted_rel_schema = (
|
|
1347
1404
|
deleted_rel_schemas[0]
|
|
1348
|
-
if deleted_rel_schemas[0].direction.value == rel_direction
|
|
1405
|
+
if deleted_rel_schemas[0].direction.value == item.rel_direction
|
|
1349
1406
|
else deleted_rel_schemas[1]
|
|
1350
1407
|
)
|
|
1351
1408
|
else:
|
|
1352
1409
|
deleted_rel_schema = deleted_rel_schemas[0]
|
|
1353
1410
|
|
|
1354
1411
|
try:
|
|
1355
|
-
changelog_mapper = rel_identifier_to_changelog_mapper[rel_identifier]
|
|
1412
|
+
changelog_mapper = rel_identifier_to_changelog_mapper[item.rel_identifier]
|
|
1356
1413
|
except KeyError:
|
|
1357
1414
|
changelog_mapper = ChangelogRelationshipMapper(schema=deleted_rel_schema)
|
|
1358
|
-
rel_identifier_to_changelog_mapper[rel_identifier] = changelog_mapper
|
|
1415
|
+
rel_identifier_to_changelog_mapper[item.rel_identifier] = changelog_mapper
|
|
1359
1416
|
|
|
1360
|
-
changelog_mapper.delete_relationship(peer_id=
|
|
1417
|
+
changelog_mapper.delete_relationship(peer_id=item.uuid, peer_kind=item.kind, rel_schema=deleted_rel_schema)
|
|
1361
1418
|
|
|
1362
1419
|
return [changelog_mapper.changelog for changelog_mapper in rel_identifier_to_changelog_mapper.values()]
|
|
1363
1420
|
|
|
@@ -1371,7 +1428,7 @@ class GetAllPeersIds(Query):
|
|
|
1371
1428
|
type: QueryType = QueryType.READ
|
|
1372
1429
|
insert_return = False
|
|
1373
1430
|
|
|
1374
|
-
def __init__(self, node_id: str, exclude_identifiers: list[str], **kwargs):
|
|
1431
|
+
def __init__(self, node_id: str, exclude_identifiers: list[str], **kwargs) -> None:
|
|
1375
1432
|
self.node_id = node_id
|
|
1376
1433
|
self.exclude_identifiers = exclude_identifiers
|
|
1377
1434
|
super().__init__(**kwargs)
|
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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):
|
|
@@ -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)
|
|
@@ -28,7 +28,7 @@ class RelationshipProfileRemovalConstraint(RelationshipManagerConstraintInterfac
|
|
|
28
28
|
In both cases, it will perform checks only if peers are being removed from the relationship being changed.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
|
-
def __init__(self, db: InfrahubDatabase, branch: Branch | None = None):
|
|
31
|
+
def __init__(self, db: InfrahubDatabase, branch: Branch | None = None) -> None:
|
|
32
32
|
self.db = db
|
|
33
33
|
self.branch = branch
|
|
34
34
|
self.schema_branch = registry.schema.get_schema_branch(branch.name if branch else registry.default_branch)
|
|
@@ -1181,6 +1181,7 @@ class RelationshipManager:
|
|
|
1181
1181
|
db: InfrahubDatabase,
|
|
1182
1182
|
process_delete: bool = True,
|
|
1183
1183
|
user_id: str = SYSTEM_USER_ID,
|
|
1184
|
+
at: Timestamp | None = None,
|
|
1184
1185
|
) -> bool:
|
|
1185
1186
|
"""Replace and Update the list of relationships with this one."""
|
|
1186
1187
|
if not isinstance(data, list):
|
|
@@ -1189,6 +1190,7 @@ class RelationshipManager:
|
|
|
1189
1190
|
list_data = data
|
|
1190
1191
|
|
|
1191
1192
|
await self._validate_hierarchy()
|
|
1193
|
+
update_at = Timestamp(at)
|
|
1192
1194
|
|
|
1193
1195
|
# Reset the list of relationship and save the previous one to see if we can reuse some
|
|
1194
1196
|
previous_relationships = {rel.peer_id: rel for rel in await self.get_relationships(db=db) if rel.peer_id}
|
|
@@ -1211,7 +1213,7 @@ class RelationshipManager:
|
|
|
1211
1213
|
if previous_relationships:
|
|
1212
1214
|
if process_delete:
|
|
1213
1215
|
for rel in previous_relationships.values():
|
|
1214
|
-
await rel.delete(db=db, user_id=user_id)
|
|
1216
|
+
await rel.delete(db=db, at=update_at, user_id=user_id)
|
|
1215
1217
|
changed = True
|
|
1216
1218
|
continue
|
|
1217
1219
|
|
|
@@ -1231,7 +1233,11 @@ class RelationshipManager:
|
|
|
1231
1233
|
# If the item is not present in the previous list of relationship, we create a new one.
|
|
1232
1234
|
self._relationships.append(
|
|
1233
1235
|
await self.rel_class(
|
|
1234
|
-
schema=self.schema,
|
|
1236
|
+
schema=self.schema,
|
|
1237
|
+
branch=self.branch,
|
|
1238
|
+
source_kind=self.node.get_kind(),
|
|
1239
|
+
at=update_at,
|
|
1240
|
+
node=self.node,
|
|
1235
1241
|
).new(db=db, data=item)
|
|
1236
1242
|
)
|
|
1237
1243
|
changed = True
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
-
from typing import Self
|
|
4
|
+
from typing import Any, Self
|
|
5
5
|
|
|
6
6
|
from pydantic import ConfigDict, Field, model_validator
|
|
7
7
|
|
|
@@ -24,6 +24,33 @@ def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParamete
|
|
|
24
24
|
class AttributeParameters(HashableModel):
|
|
25
25
|
model_config = ConfigDict(extra="forbid")
|
|
26
26
|
|
|
27
|
+
@classmethod
|
|
28
|
+
def convert_from(cls, source: AttributeParameters) -> Self:
|
|
29
|
+
"""Convert from another AttributeParameters subclass.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
source: The source AttributeParameters instance to convert from
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A new instance of the target class with compatible fields populated
|
|
36
|
+
"""
|
|
37
|
+
source_data = source.model_dump()
|
|
38
|
+
return cls.convert_from_dict(source_data=source_data)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def convert_from_dict(cls, source_data: dict[str, Any]) -> Self:
|
|
42
|
+
"""Convert from a dictionary to the target class.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
source_data: The source dictionary to convert from
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
A new instance of the target class with compatible fields populated
|
|
49
|
+
"""
|
|
50
|
+
target_fields = set(cls.model_fields.keys())
|
|
51
|
+
filtered_data = {k: v for k, v in source_data.items() if k in target_fields}
|
|
52
|
+
return cls(**filtered_data)
|
|
53
|
+
|
|
27
54
|
|
|
28
55
|
class TextAttributeParameters(AttributeParameters):
|
|
29
56
|
regex: str | None = Field(
|
|
@@ -114,13 +114,20 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
114
114
|
@field_validator("parameters", mode="before")
|
|
115
115
|
@classmethod
|
|
116
116
|
def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
|
|
117
|
-
"""Override parameters class if using base AttributeParameters class and should be using a subclass
|
|
117
|
+
"""Override parameters class if using base AttributeParameters class and should be using a subclass.
|
|
118
|
+
|
|
119
|
+
This validator handles parameter type conversion when an attribute's kind changes.
|
|
120
|
+
Fields from the source that don't exist in the target are silently dropped.
|
|
121
|
+
Fields with the same name in both classes are preserved.
|
|
122
|
+
"""
|
|
118
123
|
kind = info.data["kind"]
|
|
119
124
|
expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
|
|
120
125
|
if value is None:
|
|
121
126
|
return expected_parameters_class()
|
|
122
127
|
if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
|
|
123
|
-
return expected_parameters_class(
|
|
128
|
+
return expected_parameters_class.convert_from(value)
|
|
129
|
+
if isinstance(value, dict):
|
|
130
|
+
return expected_parameters_class.convert_from_dict(source_data=value)
|
|
124
131
|
return value
|
|
125
132
|
|
|
126
133
|
@model_validator(mode="after")
|
|
@@ -238,19 +245,6 @@ class TextAttributeSchema(AttributeSchema):
|
|
|
238
245
|
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
239
246
|
)
|
|
240
247
|
|
|
241
|
-
@model_validator(mode="after")
|
|
242
|
-
def reconcile_parameters(self) -> Self:
|
|
243
|
-
if self.regex != self.parameters.regex:
|
|
244
|
-
final_regex = self.parameters.regex if self.parameters.regex is not None else self.regex
|
|
245
|
-
self.regex = self.parameters.regex = final_regex
|
|
246
|
-
if self.min_length != self.parameters.min_length:
|
|
247
|
-
final_min_length = self.parameters.min_length if self.parameters.min_length is not None else self.min_length
|
|
248
|
-
self.min_length = self.parameters.min_length = final_min_length
|
|
249
|
-
if self.max_length != self.parameters.max_length:
|
|
250
|
-
final_max_length = self.parameters.max_length if self.parameters.max_length is not None else self.max_length
|
|
251
|
-
self.max_length = self.parameters.max_length = final_max_length
|
|
252
|
-
return self
|
|
253
|
-
|
|
254
248
|
def get_regex(self) -> str | None:
|
|
255
249
|
return self.parameters.regex
|
|
256
250
|
|
|
@@ -263,6 +263,9 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
263
263
|
) -> AttributeSchema | RelationshipSchema | None: ...
|
|
264
264
|
|
|
265
265
|
def get_field(self, name: str, raise_on_error: bool = True) -> AttributeSchema | RelationshipSchema | None:
|
|
266
|
+
if name in NODE_PROPERTY_ATTRIBUTES:
|
|
267
|
+
return self.get_attribute(name=name)
|
|
268
|
+
|
|
266
269
|
if field := self.get_attribute_or_none(name=name):
|
|
267
270
|
return field
|
|
268
271
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any, TypedDict
|
|
2
2
|
|
|
3
3
|
from infrahub.actions.schema import (
|
|
4
4
|
core_action,
|
|
@@ -93,7 +93,13 @@ from .template import core_object_component_template, core_object_template
|
|
|
93
93
|
from .transform import core_transform, core_transform_jinja2, core_transform_python
|
|
94
94
|
from .webhook import core_custom_webhook, core_standard_webhook, core_webhook
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
|
|
97
|
+
class CoreModelsMixedType(TypedDict):
|
|
98
|
+
generics: list[GenericSchema]
|
|
99
|
+
nodes: list[NodeSchema]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
core_models_mixed: CoreModelsMixedType = {
|
|
97
103
|
"generics": [
|
|
98
104
|
core_action,
|
|
99
105
|
core_trigger_rule,
|