infrahub-server 1.2.11__py3-none-any.whl → 1.3.0b1__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/actions/constants.py +86 -0
- infrahub/actions/gather.py +114 -0
- infrahub/actions/models.py +241 -0
- infrahub/actions/parsers.py +104 -0
- infrahub/actions/schema.py +382 -0
- infrahub/actions/tasks.py +126 -0
- infrahub/actions/triggers.py +21 -0
- infrahub/cli/db.py +1 -2
- infrahub/core/account.py +24 -47
- infrahub/core/attribute.py +13 -15
- infrahub/core/constants/__init__.py +5 -0
- infrahub/core/constants/infrahubkind.py +9 -0
- infrahub/core/convert_object_type/__init__.py +0 -0
- infrahub/core/convert_object_type/conversion.py +122 -0
- infrahub/core/convert_object_type/schema_mapping.py +56 -0
- infrahub/core/diff/query/all_conflicts.py +1 -5
- infrahub/core/diff/query/artifact.py +10 -20
- infrahub/core/diff/query/diff_get.py +3 -6
- infrahub/core/diff/query/field_summary.py +2 -4
- infrahub/core/diff/query/merge.py +70 -123
- infrahub/core/diff/query/save.py +20 -32
- infrahub/core/diff/query/summary_counts_enricher.py +34 -54
- infrahub/core/manager.py +14 -11
- infrahub/core/migrations/graph/m003_relationship_parent_optional.py +1 -2
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +2 -4
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +11 -22
- infrahub/core/migrations/graph/m020_duplicate_edges.py +3 -6
- infrahub/core/migrations/graph/m021_missing_hierarchy_merge.py +1 -2
- infrahub/core/migrations/graph/m024_missing_hierarchy_backfill.py +1 -2
- infrahub/core/migrations/query/attribute_add.py +1 -2
- infrahub/core/migrations/query/attribute_rename.py +5 -10
- infrahub/core/migrations/query/delete_element_in_schema.py +19 -17
- infrahub/core/migrations/query/node_duplicate.py +19 -21
- infrahub/core/migrations/query/relationship_duplicate.py +19 -17
- infrahub/core/migrations/schema/node_attribute_remove.py +4 -8
- infrahub/core/migrations/schema/node_remove.py +19 -19
- infrahub/core/models.py +29 -2
- infrahub/core/node/__init__.py +90 -18
- infrahub/core/node/create.py +211 -0
- infrahub/core/node/resource_manager/number_pool.py +31 -5
- infrahub/core/node/standard.py +6 -1
- infrahub/core/protocols.py +56 -0
- infrahub/core/protocols_base.py +3 -0
- infrahub/core/query/__init__.py +2 -2
- infrahub/core/query/diff.py +19 -32
- infrahub/core/query/ipam.py +10 -20
- infrahub/core/query/node.py +28 -46
- infrahub/core/query/relationship.py +53 -32
- infrahub/core/query/resource_manager.py +1 -2
- infrahub/core/query/subquery.py +2 -4
- infrahub/core/relationship/model.py +3 -0
- infrahub/core/schema/__init__.py +2 -1
- infrahub/core/schema/attribute_parameters.py +160 -0
- infrahub/core/schema/attribute_schema.py +111 -8
- infrahub/core/schema/basenode_schema.py +25 -1
- infrahub/core/schema/definitions/core/__init__.py +29 -1
- infrahub/core/schema/definitions/core/group.py +45 -0
- infrahub/core/schema/definitions/internal.py +27 -4
- infrahub/core/schema/generated/attribute_schema.py +16 -3
- infrahub/core/schema/manager.py +3 -0
- infrahub/core/schema/schema_branch.py +67 -7
- infrahub/core/validators/__init__.py +13 -1
- infrahub/core/validators/attribute/choices.py +1 -3
- infrahub/core/validators/attribute/enum.py +1 -3
- infrahub/core/validators/attribute/kind.py +1 -3
- infrahub/core/validators/attribute/length.py +13 -7
- infrahub/core/validators/attribute/min_max.py +118 -0
- infrahub/core/validators/attribute/number_pool.py +106 -0
- infrahub/core/validators/attribute/optional.py +1 -4
- infrahub/core/validators/attribute/regex.py +5 -6
- infrahub/core/validators/attribute/unique.py +1 -3
- infrahub/core/validators/determiner.py +18 -2
- infrahub/core/validators/enum.py +12 -0
- infrahub/core/validators/node/hierarchy.py +3 -6
- infrahub/core/validators/query.py +1 -3
- infrahub/core/validators/relationship/count.py +6 -12
- infrahub/core/validators/relationship/optional.py +2 -4
- infrahub/core/validators/relationship/peer.py +3 -8
- infrahub/core/validators/uniqueness/query.py +5 -9
- infrahub/database/__init__.py +11 -2
- infrahub/events/group_action.py +1 -0
- infrahub/git/base.py +5 -3
- infrahub/git/integrator.py +102 -3
- infrahub/graphql/analyzer.py +139 -18
- infrahub/graphql/manager.py +4 -0
- infrahub/graphql/mutations/action.py +164 -0
- infrahub/graphql/mutations/convert_object_type.py +62 -0
- infrahub/graphql/mutations/main.py +24 -175
- infrahub/graphql/mutations/proposed_change.py +20 -17
- infrahub/graphql/mutations/resource_manager.py +62 -6
- infrahub/graphql/queries/convert_object_type_mapping.py +36 -0
- infrahub/graphql/queries/resource_manager.py +7 -1
- infrahub/graphql/schema.py +6 -0
- infrahub/menu/menu.py +31 -0
- infrahub/message_bus/messages/__init__.py +0 -10
- infrahub/message_bus/operations/__init__.py +0 -8
- infrahub/patch/queries/consolidate_duplicated_nodes.py +3 -6
- infrahub/patch/queries/delete_duplicated_edges.py +5 -10
- infrahub/pools/number.py +5 -3
- infrahub/prefect_server/models.py +1 -19
- infrahub/proposed_change/models.py +68 -3
- infrahub/proposed_change/tasks.py +907 -30
- infrahub/task_manager/models.py +10 -6
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +18 -2
- infrahub/trigger/tasks.py +3 -1
- infrahub/types.py +6 -0
- infrahub/workflows/catalogue.py +76 -0
- infrahub_sdk/client.py +43 -10
- infrahub_sdk/node/__init__.py +39 -0
- infrahub_sdk/node/attribute.py +122 -0
- infrahub_sdk/node/constants.py +21 -0
- infrahub_sdk/{node.py → node/node.py} +50 -749
- infrahub_sdk/node/parsers.py +15 -0
- infrahub_sdk/node/property.py +24 -0
- infrahub_sdk/node/related_node.py +266 -0
- infrahub_sdk/node/relationship.py +302 -0
- infrahub_sdk/protocols.py +112 -0
- infrahub_sdk/protocols_base.py +34 -2
- infrahub_sdk/query_groups.py +13 -2
- infrahub_sdk/schema/main.py +1 -0
- infrahub_sdk/schema/repository.py +16 -0
- infrahub_sdk/spec/object.py +1 -1
- infrahub_sdk/store.py +1 -1
- infrahub_sdk/testing/schemas/car_person.py +1 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/METADATA +4 -4
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/RECORD +134 -122
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/WHEEL +1 -1
- infrahub_testcontainers/container.py +0 -1
- infrahub_testcontainers/docker-compose.test.yml +1 -1
- infrahub_testcontainers/helpers.py +8 -2
- infrahub/message_bus/messages/check_generator_run.py +0 -26
- infrahub/message_bus/messages/finalize_validator_execution.py +0 -15
- infrahub/message_bus/messages/proposed_change/base_with_diff.py +0 -16
- infrahub/message_bus/messages/proposed_change/request_proposedchange_refreshartifacts.py +0 -11
- infrahub/message_bus/messages/request_generatordefinition_check.py +0 -20
- infrahub/message_bus/messages/request_proposedchange_pipeline.py +0 -23
- infrahub/message_bus/operations/check/__init__.py +0 -3
- infrahub/message_bus/operations/check/generator.py +0 -156
- infrahub/message_bus/operations/finalize/__init__.py +0 -3
- infrahub/message_bus/operations/finalize/validator.py +0 -133
- infrahub/message_bus/operations/requests/__init__.py +0 -9
- infrahub/message_bus/operations/requests/generator_definition.py +0 -140
- infrahub/message_bus/operations/requests/proposed_change.py +0 -629
- /infrahub/{message_bus/messages/proposed_change → actions}/__init__.py +0 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.2.11.dist-info → infrahub_server-1.3.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -217,8 +217,7 @@ class RelationshipQuery(Query):
|
|
|
217
217
|
)
|
|
218
218
|
source_query_match = """
|
|
219
219
|
MATCH (s:Node { uuid: $source_id })
|
|
220
|
-
CALL {
|
|
221
|
-
WITH s
|
|
220
|
+
CALL (s) {
|
|
222
221
|
MATCH (s)-[r:IS_PART_OF]->(:Root)
|
|
223
222
|
WHERE %(source_filter)s
|
|
224
223
|
RETURN r.status = "active" AS s_is_active
|
|
@@ -246,8 +245,7 @@ class RelationshipQuery(Query):
|
|
|
246
245
|
)
|
|
247
246
|
destination_query_match = """
|
|
248
247
|
MATCH (d:Node { uuid: $destination_id })
|
|
249
|
-
CALL {
|
|
250
|
-
WITH d
|
|
248
|
+
CALL (d) {
|
|
251
249
|
MATCH (d)-[r:IS_PART_OF]->(:Root)
|
|
252
250
|
WHERE %(destination_filter)s
|
|
253
251
|
RETURN r.status = "active" AS d_is_active
|
|
@@ -524,8 +522,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
|
|
|
524
522
|
CREATE (s)%(r1)s(rl)
|
|
525
523
|
CREATE (rl)%(r2)s(d)
|
|
526
524
|
WITH rl
|
|
527
|
-
CALL {
|
|
528
|
-
WITH rl
|
|
525
|
+
CALL (rl) {
|
|
529
526
|
MATCH (rl)-[edge:IS_VISIBLE]->(visible)
|
|
530
527
|
WHERE %(rel_filter)s AND edge.status = "active"
|
|
531
528
|
WITH rl, edge, visible
|
|
@@ -536,8 +533,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
|
|
|
536
533
|
WHERE edge.branch = $branch
|
|
537
534
|
SET edge.to = $at
|
|
538
535
|
}
|
|
539
|
-
CALL {
|
|
540
|
-
WITH rl
|
|
536
|
+
CALL (rl) {
|
|
541
537
|
MATCH (rl)-[edge:IS_PROTECTED]->(protected)
|
|
542
538
|
WHERE %(rel_filter)s AND edge.status = "active"
|
|
543
539
|
WITH rl, edge, protected
|
|
@@ -548,8 +544,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
|
|
|
548
544
|
WHERE edge.branch = $branch
|
|
549
545
|
SET edge.to = $at
|
|
550
546
|
}
|
|
551
|
-
CALL {
|
|
552
|
-
WITH rl
|
|
547
|
+
CALL (rl) {
|
|
553
548
|
MATCH (rl)-[edge:HAS_OWNER]->(owner_node)
|
|
554
549
|
WHERE %(rel_filter)s AND edge.status = "active"
|
|
555
550
|
WITH rl, edge, owner_node
|
|
@@ -560,8 +555,7 @@ class RelationshipDeleteQuery(RelationshipQuery):
|
|
|
560
555
|
WHERE edge.branch = $branch
|
|
561
556
|
SET edge.to = $at
|
|
562
557
|
}
|
|
563
|
-
CALL {
|
|
564
|
-
WITH rl
|
|
558
|
+
CALL (rl) {
|
|
565
559
|
MATCH (rl)-[edge:HAS_SOURCE]->(source_node)
|
|
566
560
|
WHERE %(rel_filter)s AND edge.status = "active"
|
|
567
561
|
WITH rl, edge, source_node
|
|
@@ -656,8 +650,7 @@ class RelationshipGetPeerQuery(Query):
|
|
|
656
650
|
MATCH (source_node:Node)%(arrow_left_start)s[:IS_RELATED]%(arrow_left_end)s(rl:Relationship { name: $rel_identifier })
|
|
657
651
|
WHERE source_node.uuid IN $source_ids
|
|
658
652
|
WITH DISTINCT source_node, rl
|
|
659
|
-
CALL {
|
|
660
|
-
WITH rl, source_node
|
|
653
|
+
CALL (rl, source_node) {
|
|
661
654
|
MATCH path = (source_node)%(path)s(peer:Node)
|
|
662
655
|
WHERE
|
|
663
656
|
$source_kind IN LABELS(source_node) AND
|
|
@@ -726,22 +719,19 @@ class RelationshipGetPeerQuery(Query):
|
|
|
726
719
|
with_str = ", ".join(
|
|
727
720
|
[f"{subquery_result_name} as {label}" if label == "peer" else label for label in self.return_labels]
|
|
728
721
|
)
|
|
729
|
-
self.add_subquery(subquery=subquery, with_clause=with_str)
|
|
730
|
-
|
|
722
|
+
self.add_subquery(subquery=subquery, node_alias="peer", with_clause=with_str)
|
|
731
723
|
# ----------------------------------------------------------------------------
|
|
732
724
|
# QUERY Properties
|
|
733
725
|
# ----------------------------------------------------------------------------
|
|
734
726
|
query = """
|
|
735
|
-
CALL {
|
|
736
|
-
WITH rl
|
|
727
|
+
CALL (rl) {
|
|
737
728
|
MATCH (rl)-[r:IS_VISIBLE]-(is_visible)
|
|
738
729
|
WHERE %(branch_filter)s
|
|
739
730
|
RETURN r AS rel_is_visible, is_visible
|
|
740
731
|
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
741
732
|
LIMIT 1
|
|
742
733
|
}
|
|
743
|
-
CALL {
|
|
744
|
-
WITH rl
|
|
734
|
+
CALL (rl) {
|
|
745
735
|
MATCH (rl)-[r:IS_PROTECTED]-(is_protected)
|
|
746
736
|
WHERE %(branch_filter)s
|
|
747
737
|
RETURN r AS rel_is_protected, is_protected
|
|
@@ -758,8 +748,7 @@ class RelationshipGetPeerQuery(Query):
|
|
|
758
748
|
# We must query them one by one otherwise the second one won't return
|
|
759
749
|
for node_prop in ["source", "owner"]:
|
|
760
750
|
query = """
|
|
761
|
-
CALL {
|
|
762
|
-
WITH rl
|
|
751
|
+
CALL (rl) {
|
|
763
752
|
OPTIONAL MATCH (rl)-[r:HAS_%(node_prop_type)s]-(%(node_prop)s)
|
|
764
753
|
WHERE %(branch_filter)s
|
|
765
754
|
RETURN r AS rel_%(node_prop)s, %(node_prop)s
|
|
@@ -800,7 +789,7 @@ class RelationshipGetPeerQuery(Query):
|
|
|
800
789
|
self.order_by.append(subquery_result_name)
|
|
801
790
|
self.params.update(subquery_params)
|
|
802
791
|
|
|
803
|
-
self.add_subquery(subquery=subquery)
|
|
792
|
+
self.add_subquery(subquery=subquery, node_alias="peer")
|
|
804
793
|
|
|
805
794
|
order_cnt += 1
|
|
806
795
|
|
|
@@ -939,8 +928,7 @@ class RelationshipGetByIdentifierQuery(Query):
|
|
|
939
928
|
query = """
|
|
940
929
|
MATCH (rl:Relationship)
|
|
941
930
|
WHERE rl.name IN $identifiers
|
|
942
|
-
CALL {
|
|
943
|
-
WITH rl
|
|
931
|
+
CALL (rl) {
|
|
944
932
|
MATCH (src:Node)-[r1:IS_RELATED]-(rl:Relationship)-[r2:IS_RELATED]-(dst:Node)
|
|
945
933
|
WHERE (size($full_identifiers) = 0 OR [src.kind, rl.name, dst.kind] in $full_identifiers)
|
|
946
934
|
AND NOT src.namespace IN $excluded_namespaces
|
|
@@ -1003,8 +991,7 @@ class RelationshipCountPerNodeQuery(Query):
|
|
|
1003
991
|
query = """
|
|
1004
992
|
MATCH (peer_node:Node)%(path)s(rl:Relationship { name: $rel_identifier })
|
|
1005
993
|
WHERE peer_node.uuid IN $peer_ids AND %(branch_filter)s
|
|
1006
|
-
CALL {
|
|
1007
|
-
WITH rl
|
|
994
|
+
CALL (rl) {
|
|
1008
995
|
MATCH path = (peer_node:Node)%(path)s(rl)
|
|
1009
996
|
WHERE peer_node.uuid IN $peer_ids AND %(branch_filter)s
|
|
1010
997
|
RETURN peer_node as peer, r as r1
|
|
@@ -1082,8 +1069,7 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1082
1069
|
for arrow_left, arrow_right in (("<-", "-"), ("-", "->")):
|
|
1083
1070
|
for edge_type in edge_types:
|
|
1084
1071
|
sub_query = """
|
|
1085
|
-
CALL {
|
|
1086
|
-
WITH rl
|
|
1072
|
+
CALL (rl) {
|
|
1087
1073
|
MATCH (rl)%(arrow_left)s[active_edge:%(edge_type)s]%(arrow_right)s(n)
|
|
1088
1074
|
WHERE %(active_rel_filter)s AND active_edge.status ="active"
|
|
1089
1075
|
CREATE (rl)%(arrow_left)s[deleted_edge:%(edge_type)s $rel_prop]%(arrow_right)s(n)
|
|
@@ -1103,8 +1089,7 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1103
1089
|
|
|
1104
1090
|
# We only want to return uuid/kind of `Node` connected through `IS_RELATED` edges.
|
|
1105
1091
|
query += """
|
|
1106
|
-
CALL {
|
|
1107
|
-
WITH rl
|
|
1092
|
+
CALL (rl) {
|
|
1108
1093
|
MATCH (rl)-[active_edge:IS_RELATED]->(n)
|
|
1109
1094
|
WHERE %(active_rel_filter)s
|
|
1110
1095
|
WITH rl, active_edge, n
|
|
@@ -1123,7 +1108,6 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1123
1108
|
"outbound" as rel_direction
|
|
1124
1109
|
|
|
1125
1110
|
UNION
|
|
1126
|
-
|
|
1127
1111
|
WITH rl
|
|
1128
1112
|
MATCH (rl)<-[active_edge:IS_RELATED]-(n)
|
|
1129
1113
|
WHERE %(active_rel_filter)s
|
|
@@ -1193,3 +1177,40 @@ class RelationshipDeleteAllQuery(Query):
|
|
|
1193
1177
|
changelog_mapper.delete_relationship(peer_id=peer_uuid, peer_kind=kind, rel_schema=deleted_rel_schema)
|
|
1194
1178
|
|
|
1195
1179
|
return [changelog_mapper.changelog for changelog_mapper in rel_identifier_to_changelog_mapper.values()]
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
class GetAllPeersIds(Query):
|
|
1183
|
+
"""
|
|
1184
|
+
Return all peers ids connected to input node. Some peers can be excluded using `exclude_identifiers`.
|
|
1185
|
+
"""
|
|
1186
|
+
|
|
1187
|
+
name = "get_peers_ids"
|
|
1188
|
+
type: QueryType = QueryType.READ
|
|
1189
|
+
insert_return = False
|
|
1190
|
+
|
|
1191
|
+
def __init__(self, node_id: str, exclude_identifiers: list[str], **kwargs):
|
|
1192
|
+
self.node_id = node_id
|
|
1193
|
+
self.exclude_identifiers = exclude_identifiers
|
|
1194
|
+
super().__init__(**kwargs)
|
|
1195
|
+
|
|
1196
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs) -> None: # noqa: ARG002
|
|
1197
|
+
self.params["source_id"] = kwargs["node_id"]
|
|
1198
|
+
self.params["branch"] = self.branch.name
|
|
1199
|
+
self.params["exclude_identifiers"] = self.exclude_identifiers
|
|
1200
|
+
|
|
1201
|
+
active_rel_filter, rel_params = self.branch.get_query_filter_path(
|
|
1202
|
+
at=self.at, variable_name="e1", branch_agnostic=self.branch_agnostic
|
|
1203
|
+
)
|
|
1204
|
+
self.params.update(rel_params)
|
|
1205
|
+
|
|
1206
|
+
query = """
|
|
1207
|
+
MATCH (node:Node { uuid: $source_id })-[e1:IS_RELATED]-(rl:Relationship)-[e2:IS_RELATED]-(peer:Node)
|
|
1208
|
+
WHERE %(active_rel_filter)s AND peer.uuid <> node.uuid AND NOT (rl.name IN $exclude_identifiers)
|
|
1209
|
+
WITH DISTINCT(peer.uuid) as uuid
|
|
1210
|
+
RETURN uuid
|
|
1211
|
+
""" % {"active_rel_filter": active_rel_filter}
|
|
1212
|
+
|
|
1213
|
+
self.add_to_query(query)
|
|
1214
|
+
|
|
1215
|
+
def get_peers_uuids(self) -> list[str]:
|
|
1216
|
+
return [row.data["uuid"] for row in self.results] # type: ignore
|
|
@@ -220,8 +220,7 @@ class NumberPoolGetUsed(Query):
|
|
|
220
220
|
|
|
221
221
|
query = """
|
|
222
222
|
MATCH (pool:%(number_pool)s { uuid: $pool_id })
|
|
223
|
-
CALL {
|
|
224
|
-
WITH pool
|
|
223
|
+
CALL (pool) {
|
|
225
224
|
MATCH (pool)-[res:IS_RESERVED]->(av:AttributeValue)<-[hv:HAS_VALUE]-(attr:Attribute)
|
|
226
225
|
WHERE
|
|
227
226
|
attr.name = $attribute_name
|
infrahub/core/query/subquery.py
CHANGED
|
@@ -61,7 +61,7 @@ async def build_subquery_filter(
|
|
|
61
61
|
where_str = " AND ".join(field_where)
|
|
62
62
|
branch_level_str = "reduce(br_lvl = 0, r in relationships(path) | br_lvl + r.branch_level)"
|
|
63
63
|
froms_str = db.render_list_comprehension(items="relationships(path)", item_name="from")
|
|
64
|
-
to_return = f"{
|
|
64
|
+
to_return = f"{prefix}"
|
|
65
65
|
with_extra = ""
|
|
66
66
|
final_with_extra = ""
|
|
67
67
|
is_isnull = filter_name == "isnull"
|
|
@@ -82,7 +82,6 @@ async def build_subquery_filter(
|
|
|
82
82
|
elif field is not None and field.is_attribute:
|
|
83
83
|
is_active_filter = "(latest_node_details[2]).value = 'NULL'"
|
|
84
84
|
query = f"""
|
|
85
|
-
WITH {node_alias}
|
|
86
85
|
{match} path = {filter_str}
|
|
87
86
|
WHERE {where_str}
|
|
88
87
|
WITH
|
|
@@ -94,7 +93,7 @@ async def build_subquery_filter(
|
|
|
94
93
|
ORDER BY branch_level DESC, froms[-1] DESC, froms[-2] DESC
|
|
95
94
|
WITH head(collect([is_active, {node_alias}{with_extra}])) AS latest_node_details
|
|
96
95
|
WHERE {is_active_filter}
|
|
97
|
-
WITH latest_node_details[1] AS {
|
|
96
|
+
WITH latest_node_details[1] AS {prefix}{final_with_extra}
|
|
98
97
|
RETURN {to_return}
|
|
99
98
|
"""
|
|
100
99
|
return query, params, prefix
|
|
@@ -174,7 +173,6 @@ async def build_subquery_order(
|
|
|
174
173
|
to_return_str_parts.append(f"CASE WHEN is_active = TRUE THEN {expression} ELSE NULL END AS {alias}")
|
|
175
174
|
to_return_str = ", ".join(to_return_str_parts)
|
|
176
175
|
query = f"""
|
|
177
|
-
WITH {node_alias}
|
|
178
176
|
OPTIONAL MATCH path = {filter_str}
|
|
179
177
|
WHERE {where_str}
|
|
180
178
|
WITH {with_str_to_alias}
|
infrahub/core/schema/__init__.py
CHANGED
|
@@ -21,7 +21,8 @@ from .profile_schema import ProfileSchema
|
|
|
21
21
|
from .relationship_schema import RelationshipSchema
|
|
22
22
|
from .template_schema import TemplateSchema
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
NonGenericSchemaTypes: TypeAlias = NodeSchema | ProfileSchema | TemplateSchema
|
|
25
|
+
MainSchemaTypes: TypeAlias = NonGenericSchemaTypes | GenericSchema
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
# -----------------------------------------------------
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from pydantic import ConfigDict, Field, model_validator
|
|
7
|
+
|
|
8
|
+
from infrahub import config
|
|
9
|
+
from infrahub.core.constants.schema import UpdateSupport
|
|
10
|
+
from infrahub.core.models import HashableModel
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_attribute_parameters_class_for_kind(kind: str) -> type[AttributeParameters]:
|
|
14
|
+
param_classes: dict[str, type[AttributeParameters]] = {
|
|
15
|
+
"NumberPool": NumberPoolParameters,
|
|
16
|
+
"Text": TextAttributeParameters,
|
|
17
|
+
"TextArea": TextAttributeParameters,
|
|
18
|
+
"Number": NumberAttributeParameters,
|
|
19
|
+
}
|
|
20
|
+
return param_classes.get(kind, AttributeParameters)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AttributeParameters(HashableModel):
|
|
24
|
+
model_config = ConfigDict(extra="forbid")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TextAttributeParameters(AttributeParameters):
|
|
28
|
+
regex: str | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description="Regular expression that attribute value must match if defined",
|
|
31
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
32
|
+
)
|
|
33
|
+
min_length: int | None = Field(
|
|
34
|
+
default=None,
|
|
35
|
+
description="Set a minimum number of characters allowed.",
|
|
36
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
37
|
+
)
|
|
38
|
+
max_length: int | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description="Set a maximum number of characters allowed.",
|
|
41
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def validate_min_max(self) -> Self:
|
|
46
|
+
if (
|
|
47
|
+
config.SETTINGS.initialized
|
|
48
|
+
and config.SETTINGS.main.schema_strict_mode
|
|
49
|
+
and self.min_length is not None
|
|
50
|
+
and self.max_length is not None
|
|
51
|
+
):
|
|
52
|
+
if self.min_length > self.max_length:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"`max_length` can't be less than `min_length` when the schema is configured with strict mode"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NumberAttributeParameters(AttributeParameters):
|
|
61
|
+
min_value: int | None = Field(
|
|
62
|
+
default=None,
|
|
63
|
+
description="Set a minimum value allowed.",
|
|
64
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
65
|
+
)
|
|
66
|
+
max_value: int | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Set a maximum value allowed.",
|
|
69
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
70
|
+
)
|
|
71
|
+
excluded_values: str | None = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
description="List of values or range of values not allowed for the attribute, format is: '100,150-200,280,300-400'",
|
|
74
|
+
pattern=r"^(\d+(?:-\d+)?)(?:,\d+(?:-\d+)?)*$",
|
|
75
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@model_validator(mode="after")
|
|
79
|
+
def validate_ranges(self) -> Self:
|
|
80
|
+
ranges = self.get_excluded_ranges()
|
|
81
|
+
for i, (start_range_1, end_range_1) in enumerate(ranges):
|
|
82
|
+
if start_range_1 > end_range_1:
|
|
83
|
+
raise ValueError("`start_range` can't be less than `end_range`")
|
|
84
|
+
|
|
85
|
+
# Check for overlapping ranges
|
|
86
|
+
for start_range_2, end_range_2 in ranges[i + 1 :]:
|
|
87
|
+
if not (end_range_1 < start_range_2 or start_range_1 > end_range_2):
|
|
88
|
+
raise ValueError("Excluded ranges cannot overlap")
|
|
89
|
+
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="after")
|
|
93
|
+
def validate_min_max(self) -> Self:
|
|
94
|
+
if (
|
|
95
|
+
config.SETTINGS.initialized
|
|
96
|
+
and config.SETTINGS.main.schema_strict_mode
|
|
97
|
+
and self.min_value is not None
|
|
98
|
+
and self.max_value is not None
|
|
99
|
+
):
|
|
100
|
+
if self.min_value > self.max_value:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"`max_value` can't be less than `min_value` when the schema is configured with strict mode"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def get_excluded_single_values(self) -> list[int]:
|
|
108
|
+
if not self.excluded_values:
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
results = [int(value) for value in self.excluded_values.split(",") if "-" not in value]
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
def get_excluded_ranges(self) -> list[tuple[int, int]]:
|
|
115
|
+
if not self.excluded_values:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
ranges = []
|
|
119
|
+
for value in self.excluded_values.split(","):
|
|
120
|
+
if "-" in value:
|
|
121
|
+
start, end = map(int, value.split("-"))
|
|
122
|
+
ranges.append((start, end))
|
|
123
|
+
|
|
124
|
+
return ranges
|
|
125
|
+
|
|
126
|
+
def is_valid_value(self, value: int) -> bool:
|
|
127
|
+
if self.min_value is not None and value < self.min_value:
|
|
128
|
+
return False
|
|
129
|
+
if self.max_value is not None and value > self.max_value:
|
|
130
|
+
return False
|
|
131
|
+
if value in self.get_excluded_single_values():
|
|
132
|
+
return False
|
|
133
|
+
for start, end in self.get_excluded_ranges():
|
|
134
|
+
if start <= value <= end:
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class NumberPoolParameters(AttributeParameters):
|
|
140
|
+
end_range: int = Field(
|
|
141
|
+
default=sys.maxsize,
|
|
142
|
+
description="End range for numbers for the associated NumberPool",
|
|
143
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
144
|
+
)
|
|
145
|
+
start_range: int = Field(
|
|
146
|
+
default=1,
|
|
147
|
+
description="Start range for numbers for the associated NumberPool",
|
|
148
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
149
|
+
)
|
|
150
|
+
number_pool_id: str | None = Field(
|
|
151
|
+
default=None,
|
|
152
|
+
description="The ID of the numberpool associated with this attribute",
|
|
153
|
+
json_schema_extra={"update": UpdateSupport.NOT_SUPPORTED.value},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@model_validator(mode="after")
|
|
157
|
+
def validate_ranges(self) -> Self:
|
|
158
|
+
if self.start_range > self.end_range:
|
|
159
|
+
raise ValueError("`start_range` can't be less than `end_range`")
|
|
160
|
+
return self
|
|
@@ -2,15 +2,23 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import enum
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import TYPE_CHECKING, Any
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
6
6
|
|
|
7
|
-
from pydantic import field_validator, model_validator
|
|
7
|
+
from pydantic import Field, ValidationInfo, field_validator, model_validator
|
|
8
8
|
|
|
9
9
|
from infrahub import config
|
|
10
|
+
from infrahub.core.constants.schema import UpdateSupport
|
|
10
11
|
from infrahub.core.enums import generate_python_enum
|
|
11
12
|
from infrahub.core.query.attribute import default_attribute_query_filter
|
|
12
13
|
from infrahub.types import ATTRIBUTE_KIND_LABELS, ATTRIBUTE_TYPES
|
|
13
14
|
|
|
15
|
+
from .attribute_parameters import (
|
|
16
|
+
AttributeParameters,
|
|
17
|
+
NumberAttributeParameters,
|
|
18
|
+
NumberPoolParameters,
|
|
19
|
+
TextAttributeParameters,
|
|
20
|
+
get_attribute_parameters_class_for_kind,
|
|
21
|
+
)
|
|
14
22
|
from .generated.attribute_schema import GeneratedAttributeSchema
|
|
15
23
|
|
|
16
24
|
if TYPE_CHECKING:
|
|
@@ -21,6 +29,16 @@ if TYPE_CHECKING:
|
|
|
21
29
|
from infrahub.database import InfrahubDatabase
|
|
22
30
|
|
|
23
31
|
|
|
32
|
+
def get_attribute_schema_class_for_kind(kind: str) -> type[AttributeSchema]:
|
|
33
|
+
attribute_schema_class_by_kind: dict[str, type[AttributeSchema]] = {
|
|
34
|
+
"NumberPool": NumberPoolSchema,
|
|
35
|
+
"Text": TextAttributeSchema,
|
|
36
|
+
"TextArea": TextAttributeSchema,
|
|
37
|
+
"Number": NumberAttributeSchema,
|
|
38
|
+
}
|
|
39
|
+
return attribute_schema_class_by_kind.get(kind, AttributeSchema)
|
|
40
|
+
|
|
41
|
+
|
|
24
42
|
class AttributeSchema(GeneratedAttributeSchema):
|
|
25
43
|
_sort_by: list[str] = ["name"]
|
|
26
44
|
_enum_class: type[enum.Enum] | None = None
|
|
@@ -53,16 +71,46 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
53
71
|
|
|
54
72
|
@model_validator(mode="before")
|
|
55
73
|
@classmethod
|
|
56
|
-
def validate_dropdown_choices(cls, values:
|
|
74
|
+
def validate_dropdown_choices(cls, values: Any) -> Any:
|
|
57
75
|
"""Validate that choices are defined for a dropdown but not for other kinds."""
|
|
58
|
-
if values
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
if isinstance(values, dict):
|
|
77
|
+
kind = values.get("kind")
|
|
78
|
+
choices = values.get("choices")
|
|
79
|
+
elif isinstance(values, AttributeSchema):
|
|
80
|
+
kind = values.kind
|
|
81
|
+
choices = values.choices
|
|
82
|
+
else:
|
|
83
|
+
return values
|
|
84
|
+
if kind != "Dropdown" and choices:
|
|
85
|
+
raise ValueError(f"Can only specify 'choices' for kind=Dropdown: {kind}")
|
|
86
|
+
|
|
87
|
+
if kind == "Dropdown" and not choices:
|
|
62
88
|
raise ValueError("The property 'choices' is required for kind=Dropdown")
|
|
63
89
|
|
|
64
90
|
return values
|
|
65
91
|
|
|
92
|
+
@field_validator("parameters", mode="before")
|
|
93
|
+
@classmethod
|
|
94
|
+
def set_parameters_type(cls, value: Any, info: ValidationInfo) -> Any:
|
|
95
|
+
"""Override parameters class if using base AttributeParameters class and should be using a subclass"""
|
|
96
|
+
kind = info.data["kind"]
|
|
97
|
+
expected_parameters_class = get_attribute_parameters_class_for_kind(kind=kind)
|
|
98
|
+
if value is None:
|
|
99
|
+
return expected_parameters_class()
|
|
100
|
+
if not isinstance(value, expected_parameters_class) and isinstance(value, AttributeParameters):
|
|
101
|
+
return expected_parameters_class(**value.model_dump())
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
@model_validator(mode="after")
|
|
105
|
+
def validate_parameters(self) -> Self:
|
|
106
|
+
if isinstance(self.parameters, NumberPoolParameters) and not self.kind == "NumberPool":
|
|
107
|
+
raise ValueError(f"NumberPoolParameters can't be used as parameters for {self.kind}")
|
|
108
|
+
|
|
109
|
+
if isinstance(self.parameters, TextAttributeParameters) and self.kind not in ["Text", "TextArea"]:
|
|
110
|
+
raise ValueError(f"TextAttributeParameters can't be used as parameters for {self.kind}")
|
|
111
|
+
|
|
112
|
+
return self
|
|
113
|
+
|
|
66
114
|
def get_class(self) -> type[BaseAttribute]:
|
|
67
115
|
return ATTRIBUTE_TYPES[self.kind].get_infrahub_class()
|
|
68
116
|
|
|
@@ -106,7 +154,7 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
106
154
|
|
|
107
155
|
def to_node(self) -> dict[str, Any]:
|
|
108
156
|
fields_to_exclude = {"id", "state", "filters"}
|
|
109
|
-
fields_to_json = {"computed_attribute"}
|
|
157
|
+
fields_to_json = {"computed_attribute", "parameters"}
|
|
110
158
|
data = self.model_dump(exclude=fields_to_exclude | fields_to_json)
|
|
111
159
|
|
|
112
160
|
for field_name in fields_to_json:
|
|
@@ -117,6 +165,15 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
117
165
|
|
|
118
166
|
return data
|
|
119
167
|
|
|
168
|
+
def get_regex(self) -> str | None:
|
|
169
|
+
return self.regex
|
|
170
|
+
|
|
171
|
+
def get_min_length(self) -> int | None:
|
|
172
|
+
return self.min_length
|
|
173
|
+
|
|
174
|
+
def get_max_length(self) -> int | None:
|
|
175
|
+
return self.max_length
|
|
176
|
+
|
|
120
177
|
async def get_query_filter(
|
|
121
178
|
self,
|
|
122
179
|
name: str,
|
|
@@ -144,3 +201,49 @@ class AttributeSchema(GeneratedAttributeSchema):
|
|
|
144
201
|
partial_match=partial_match,
|
|
145
202
|
support_profiles=support_profiles,
|
|
146
203
|
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class NumberPoolSchema(AttributeSchema):
|
|
207
|
+
parameters: NumberPoolParameters = Field(
|
|
208
|
+
default_factory=NumberPoolParameters,
|
|
209
|
+
description="Extra parameters specific to NumberPool attributes",
|
|
210
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TextAttributeSchema(AttributeSchema):
|
|
215
|
+
parameters: TextAttributeParameters = Field(
|
|
216
|
+
default_factory=TextAttributeParameters,
|
|
217
|
+
description="Extra parameters specific to text attributes",
|
|
218
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@model_validator(mode="after")
|
|
222
|
+
def reconcile_parameters(self) -> Self:
|
|
223
|
+
if self.regex != self.parameters.regex:
|
|
224
|
+
final_regex = self.parameters.regex if self.parameters.regex is not None else self.regex
|
|
225
|
+
self.regex = self.parameters.regex = final_regex
|
|
226
|
+
if self.min_length != self.parameters.min_length:
|
|
227
|
+
final_min_length = self.parameters.min_length if self.parameters.min_length is not None else self.min_length
|
|
228
|
+
self.min_length = self.parameters.min_length = final_min_length
|
|
229
|
+
if self.max_length != self.parameters.max_length:
|
|
230
|
+
final_max_length = self.parameters.max_length if self.parameters.max_length is not None else self.max_length
|
|
231
|
+
self.max_length = self.parameters.max_length = final_max_length
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
def get_regex(self) -> str | None:
|
|
235
|
+
return self.parameters.regex
|
|
236
|
+
|
|
237
|
+
def get_min_length(self) -> int | None:
|
|
238
|
+
return self.parameters.min_length
|
|
239
|
+
|
|
240
|
+
def get_max_length(self) -> int | None:
|
|
241
|
+
return self.parameters.max_length
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class NumberAttributeSchema(AttributeSchema):
|
|
245
|
+
parameters: NumberAttributeParameters = Field(
|
|
246
|
+
default_factory=NumberAttributeParameters,
|
|
247
|
+
description="Extra parameters specific to number attributes",
|
|
248
|
+
json_schema_extra={"update": UpdateSupport.VALIDATE_CONSTRAINT.value},
|
|
249
|
+
)
|
|
@@ -13,7 +13,7 @@ from pydantic import field_validator
|
|
|
13
13
|
from infrahub.core.constants import RelationshipCardinality, RelationshipKind
|
|
14
14
|
from infrahub.core.models import HashableModel, HashableModelDiff
|
|
15
15
|
|
|
16
|
-
from .attribute_schema import AttributeSchema
|
|
16
|
+
from .attribute_schema import AttributeSchema, get_attribute_schema_class_for_kind
|
|
17
17
|
from .generated.base_node_schema import GeneratedBaseNodeSchema
|
|
18
18
|
from .relationship_schema import RelationshipSchema
|
|
19
19
|
|
|
@@ -74,6 +74,30 @@ class BaseNodeSchema(GeneratedBaseNodeSchema):
|
|
|
74
74
|
Be careful hash generated from hash() have a salt by default and they will not be the same across run"""
|
|
75
75
|
return hash(self.get_hash())
|
|
76
76
|
|
|
77
|
+
@field_validator("attributes", mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def set_attribute_type(cls, raw_attributes: Any) -> Any:
|
|
80
|
+
if not isinstance(raw_attributes, list):
|
|
81
|
+
return raw_attributes
|
|
82
|
+
attribute_schemas_with_types: list[Any] = []
|
|
83
|
+
for raw_attr in raw_attributes:
|
|
84
|
+
if not isinstance(raw_attr, (dict, AttributeSchema)):
|
|
85
|
+
attribute_schemas_with_types.append(raw_attr)
|
|
86
|
+
continue
|
|
87
|
+
if isinstance(raw_attr, dict):
|
|
88
|
+
kind = raw_attr.get("kind")
|
|
89
|
+
attribute_type_class = get_attribute_schema_class_for_kind(kind=kind)
|
|
90
|
+
attribute_schemas_with_types.append(attribute_type_class(**raw_attr))
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
expected_attr_schema_class = get_attribute_schema_class_for_kind(kind=raw_attr.kind)
|
|
94
|
+
if not isinstance(raw_attr, expected_attr_schema_class):
|
|
95
|
+
final_attr = expected_attr_schema_class(**raw_attr.model_dump())
|
|
96
|
+
else:
|
|
97
|
+
final_attr = raw_attr
|
|
98
|
+
attribute_schemas_with_types.append(final_attr)
|
|
99
|
+
return attribute_schemas_with_types
|
|
100
|
+
|
|
77
101
|
def to_dict(self) -> dict:
|
|
78
102
|
data = self.model_dump(
|
|
79
103
|
exclude_unset=True, exclude_none=True, exclude_defaults=True, exclude={"attributes", "relationships"}
|