infrahub-server 1.7.0rc0__py3-none-any.whl → 1.7.2__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/gather.py +2 -2
- infrahub/api/query.py +3 -2
- infrahub/api/schema.py +5 -0
- infrahub/api/transformation.py +3 -3
- infrahub/cli/db.py +6 -2
- infrahub/computed_attribute/gather.py +2 -0
- infrahub/config.py +2 -2
- infrahub/core/attribute.py +21 -2
- infrahub/core/branch/models.py +11 -117
- infrahub/core/branch/tasks.py +7 -3
- infrahub/core/diff/merger/merger.py +5 -1
- infrahub/core/diff/model/path.py +43 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/index.py +2 -0
- infrahub/core/initialization.py +2 -1
- infrahub/core/ipam/resource_allocator.py +229 -0
- infrahub/core/migrations/graph/__init__.py +10 -0
- 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 +3 -2
- 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 +3 -3
- 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/graph/m052_fix_global_branch_level.py +51 -0
- infrahub/core/migrations/graph/m053_fix_branch_level_zero.py +61 -0
- infrahub/core/migrations/graph/m054_cleanup_orphaned_nodes.py +87 -0
- infrahub/core/migrations/graph/m055_remove_webhook_validate_certificates_default.py +86 -0
- infrahub/core/migrations/runner.py +6 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +8 -11
- infrahub/core/migrations/schema/attribute_supports_profile.py +3 -8
- infrahub/core/migrations/schema/models.py +8 -0
- infrahub/core/migrations/schema/node_attribute_add.py +24 -29
- infrahub/core/migrations/schema/tasks.py +7 -1
- infrahub/core/migrations/shared.py +37 -30
- infrahub/core/node/__init__.py +2 -1
- infrahub/core/node/lock_utils.py +23 -2
- infrahub/core/node/resource_manager/ip_address_pool.py +5 -11
- infrahub/core/node/resource_manager/ip_prefix_pool.py +5 -21
- infrahub/core/node/resource_manager/number_pool.py +109 -39
- infrahub/core/query/__init__.py +7 -1
- infrahub/core/query/branch.py +18 -2
- infrahub/core/query/ipam.py +629 -40
- infrahub/core/query/node.py +128 -0
- infrahub/core/query/resource_manager.py +114 -1
- infrahub/core/relationship/model.py +9 -3
- infrahub/core/schema/attribute_parameters.py +28 -1
- infrahub/core/schema/attribute_schema.py +9 -2
- infrahub/core/schema/definitions/core/webhook.py +0 -1
- infrahub/core/schema/definitions/internal.py +7 -4
- infrahub/core/schema/manager.py +50 -38
- infrahub/core/validators/attribute/kind.py +5 -2
- infrahub/core/validators/determiner.py +4 -0
- infrahub/graphql/analyzer.py +3 -1
- infrahub/graphql/app.py +7 -10
- infrahub/graphql/execution.py +95 -0
- infrahub/graphql/manager.py +8 -2
- infrahub/graphql/mutations/proposed_change.py +15 -0
- infrahub/graphql/parser.py +10 -7
- infrahub/graphql/queries/ipam.py +20 -25
- infrahub/graphql/queries/search.py +29 -9
- infrahub/lock.py +7 -0
- infrahub/proposed_change/tasks.py +2 -0
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/services/adapters/http/httpx.py +27 -0
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +73 -4
- infrahub/trigger/setup.py +1 -1
- infrahub/trigger/system.py +36 -0
- infrahub/webhook/models.py +4 -2
- infrahub/webhook/tasks.py +2 -2
- infrahub/workflows/initialization.py +2 -2
- 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.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/METADATA +8 -8
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/RECORD +123 -114
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/entry_points.txt +0 -1
- infrahub_testcontainers/docker-compose-cluster.test.yml +16 -10
- infrahub_testcontainers/docker-compose.test.yml +11 -10
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/pools/address.py +0 -16
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/WHEEL +0 -0
- {infrahub_server-1.7.0rc0.dist-info → infrahub_server-1.7.2.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/core/query/node.py
CHANGED
|
@@ -2180,6 +2180,134 @@ WITH %(tracked_vars)s,
|
|
|
2180
2180
|
return [str(result.get("n.uuid")) for result in self.get_results()]
|
|
2181
2181
|
|
|
2182
2182
|
|
|
2183
|
+
@dataclass(frozen=True)
|
|
2184
|
+
class NodeGetListByAttributeValueQueryResult:
|
|
2185
|
+
"""Result from NodeGetListByAttributeValueQuery."""
|
|
2186
|
+
|
|
2187
|
+
uuid: str
|
|
2188
|
+
kind: str
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
class NodeGetListByAttributeValueQuery(Query):
|
|
2192
|
+
"""Query to find nodes by searching attribute values.
|
|
2193
|
+
|
|
2194
|
+
This query is optimized for search operations by starting from the AttributeValueIndexed
|
|
2195
|
+
nodes and using a TEXT index for efficient CONTAINS searches. This approach is more
|
|
2196
|
+
efficient than the standard NodeGetListQuery when searching for values across all nodes.
|
|
2197
|
+
"""
|
|
2198
|
+
|
|
2199
|
+
name = "node_get_list_by_attribute_value"
|
|
2200
|
+
type = QueryType.READ
|
|
2201
|
+
|
|
2202
|
+
def __init__(
|
|
2203
|
+
self,
|
|
2204
|
+
search_value: str,
|
|
2205
|
+
kinds: list[str] | None = None,
|
|
2206
|
+
partial_match: bool = True,
|
|
2207
|
+
**kwargs: Any,
|
|
2208
|
+
) -> None:
|
|
2209
|
+
self.search_value = search_value
|
|
2210
|
+
self.kinds = kinds
|
|
2211
|
+
self.partial_match = partial_match
|
|
2212
|
+
|
|
2213
|
+
super().__init__(**kwargs)
|
|
2214
|
+
|
|
2215
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: Any) -> None: # noqa: ARG002
|
|
2216
|
+
self.order_by = ["n.uuid"]
|
|
2217
|
+
self.return_labels = ["DISTINCT n.uuid as uuid", "n.kind as kind"]
|
|
2218
|
+
|
|
2219
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
2220
|
+
at=self.at, branch_agnostic=self.branch_agnostic
|
|
2221
|
+
)
|
|
2222
|
+
self.params.update(branch_params)
|
|
2223
|
+
|
|
2224
|
+
# Build search values for case-insensitive matching without using toLower/toString
|
|
2225
|
+
# which would disable index lookup. We search for four case variations:
|
|
2226
|
+
# 1. Original (as provided), 2. lowercase, 3. UPPERCASE, 4. Title Case (first char upper, rest lower)
|
|
2227
|
+
search_original = self.search_value
|
|
2228
|
+
search_lower = self.search_value.lower()
|
|
2229
|
+
search_upper = self.search_value.upper()
|
|
2230
|
+
search_title = self.search_value.capitalize()
|
|
2231
|
+
|
|
2232
|
+
# Build the search predicate based on partial_match
|
|
2233
|
+
# We avoid toLower/toString to allow TEXT index usage
|
|
2234
|
+
if self.partial_match:
|
|
2235
|
+
# Use CONTAINS with multiple case variations to leverage TEXT index
|
|
2236
|
+
search_predicate = (
|
|
2237
|
+
"(av.value CONTAINS $search_original OR av.value CONTAINS $search_lower "
|
|
2238
|
+
"OR av.value CONTAINS $search_upper OR av.value CONTAINS $search_title)"
|
|
2239
|
+
)
|
|
2240
|
+
else:
|
|
2241
|
+
# Exact match with case variations
|
|
2242
|
+
search_predicate = (
|
|
2243
|
+
"(av.value = $search_original OR av.value = $search_lower "
|
|
2244
|
+
"OR av.value = $search_upper OR av.value = $search_title)"
|
|
2245
|
+
)
|
|
2246
|
+
|
|
2247
|
+
self.params["search_original"] = search_original
|
|
2248
|
+
self.params["search_lower"] = search_lower
|
|
2249
|
+
self.params["search_upper"] = search_upper
|
|
2250
|
+
self.params["search_title"] = search_title
|
|
2251
|
+
|
|
2252
|
+
# Build kind filter if specified
|
|
2253
|
+
kind_filter = ""
|
|
2254
|
+
if self.kinds:
|
|
2255
|
+
kind_filter = "AND any(l IN labels(n) WHERE l in $kinds)"
|
|
2256
|
+
self.params["kinds"] = self.kinds
|
|
2257
|
+
|
|
2258
|
+
# The query starts from AttributeValueIndexed nodes to leverage the TEXT index
|
|
2259
|
+
# This approach is more efficient for search operations as it:
|
|
2260
|
+
# 1. Starts from AttributeValueIndexed nodes (smaller set when filtered)
|
|
2261
|
+
# 2. Traverses from matching values back to their owning nodes
|
|
2262
|
+
# 3. Filters nodes by branch and status
|
|
2263
|
+
query = """
|
|
2264
|
+
// --------------------------
|
|
2265
|
+
// start with all possible Node-Attribute-AttributeValue combinations
|
|
2266
|
+
// --------------------------
|
|
2267
|
+
MATCH (av:AttributeValueIndexed)<-[:HAS_VALUE]-(attr:Attribute)<-[:HAS_ATTRIBUTE]-(n)
|
|
2268
|
+
WHERE %(search_predicate)s %(kind_filter)s
|
|
2269
|
+
WITH DISTINCT n, attr, av
|
|
2270
|
+
// --------------------------
|
|
2271
|
+
// filter HAS_VALUE edges
|
|
2272
|
+
// --------------------------
|
|
2273
|
+
CALL (av, attr) {
|
|
2274
|
+
MATCH (av)<-[r:HAS_VALUE]-(attr)
|
|
2275
|
+
WHERE %(branch_filter)s
|
|
2276
|
+
RETURN r
|
|
2277
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
2278
|
+
LIMIT 1
|
|
2279
|
+
}
|
|
2280
|
+
WITH n, attr
|
|
2281
|
+
WHERE r.status = "active"
|
|
2282
|
+
// --------------------------
|
|
2283
|
+
// filter HAS_ATTRIBUTE edges
|
|
2284
|
+
// --------------------------
|
|
2285
|
+
CALL (n, attr) {
|
|
2286
|
+
MATCH (attr)<-[r:HAS_ATTRIBUTE]-(n)
|
|
2287
|
+
WHERE %(branch_filter)s
|
|
2288
|
+
RETURN r
|
|
2289
|
+
ORDER BY r.branch_level DESC, r.from DESC, r.status ASC
|
|
2290
|
+
LIMIT 1
|
|
2291
|
+
}
|
|
2292
|
+
WITH n, attr, r
|
|
2293
|
+
WHERE r.status = "active"
|
|
2294
|
+
""" % {
|
|
2295
|
+
"search_predicate": search_predicate,
|
|
2296
|
+
"kind_filter": kind_filter,
|
|
2297
|
+
"branch_filter": branch_filter,
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
self.add_to_query(query)
|
|
2301
|
+
|
|
2302
|
+
def get_data(self) -> Generator[NodeGetListByAttributeValueQueryResult, None, None]:
|
|
2303
|
+
"""Yield results as typed dataclass instances."""
|
|
2304
|
+
for result in self.get_results():
|
|
2305
|
+
yield NodeGetListByAttributeValueQueryResult(
|
|
2306
|
+
uuid=result.get_as_str("uuid"),
|
|
2307
|
+
kind=result.get_as_str("kind"),
|
|
2308
|
+
)
|
|
2309
|
+
|
|
2310
|
+
|
|
2183
2311
|
class NodeGetHierarchyQuery(Query):
|
|
2184
2312
|
name = "node_get_hierarchy"
|
|
2185
2313
|
type = QueryType.READ
|
|
@@ -70,6 +70,21 @@ class NumberPoolAllocatedResult:
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class NumberPoolFreeData:
|
|
75
|
+
value: int
|
|
76
|
+
is_free: bool
|
|
77
|
+
is_last: bool
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_db(cls, result: QueryResult) -> NumberPoolFreeData:
|
|
81
|
+
return cls(
|
|
82
|
+
value=result.get_as_type("value", return_type=int),
|
|
83
|
+
is_free=result.get_as_type("is_free", return_type=bool),
|
|
84
|
+
is_last=result.get_as_type("is_last", return_type=bool),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
73
88
|
class IPAddressPoolGetIdentifiers(Query):
|
|
74
89
|
name = "ipaddresspool_get_identifiers"
|
|
75
90
|
type = QueryType.READ
|
|
@@ -393,7 +408,9 @@ class NumberPoolGetUsed(Query):
|
|
|
393
408
|
n.uuid = res.identifier AND
|
|
394
409
|
attr.name = $attribute_name AND
|
|
395
410
|
all(r in [res, hv, ha] WHERE (%(branch_filter)s))
|
|
396
|
-
ORDER BY res.branch_level DESC, hv.branch_level DESC, ha.branch_level DESC,
|
|
411
|
+
ORDER BY res.branch_level DESC, hv.branch_level DESC, ha.branch_level DESC,
|
|
412
|
+
res.from DESC, hv.from DESC, ha.from DESC,
|
|
413
|
+
res.status ASC, hv.status ASC, ha.status ASC
|
|
397
414
|
RETURN (res.status = "active" AND hv.status = "active" AND ha.status = "active") AS is_active
|
|
398
415
|
LIMIT 1
|
|
399
416
|
}
|
|
@@ -419,6 +436,102 @@ class NumberPoolGetUsed(Query):
|
|
|
419
436
|
yield NumberPoolIdentifierData.from_db(result)
|
|
420
437
|
|
|
421
438
|
|
|
439
|
+
class NumberPoolGetFree(Query):
|
|
440
|
+
name = "number_pool_get_free"
|
|
441
|
+
type = QueryType.READ
|
|
442
|
+
|
|
443
|
+
def __init__(
|
|
444
|
+
self,
|
|
445
|
+
pool: CoreNumberPool,
|
|
446
|
+
min_value: int | None = None,
|
|
447
|
+
max_value: int | None = None,
|
|
448
|
+
**kwargs: dict[str, Any],
|
|
449
|
+
) -> None:
|
|
450
|
+
self.pool = pool
|
|
451
|
+
self.min_value = min_value
|
|
452
|
+
self.max_value = max_value
|
|
453
|
+
|
|
454
|
+
super().__init__(**kwargs) # type: ignore[arg-type]
|
|
455
|
+
|
|
456
|
+
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
457
|
+
self.params["pool_id"] = self.pool.get_id()
|
|
458
|
+
# Use min_value/max_value if provided, otherwise use pool's start_range/end_range
|
|
459
|
+
self.params["start_range"] = self.min_value if self.min_value is not None else self.pool.start_range.value
|
|
460
|
+
self.params["end_range"] = self.max_value if self.max_value is not None else self.pool.end_range.value
|
|
461
|
+
self.limit = 1 # Query only works at returning a single, free entry
|
|
462
|
+
|
|
463
|
+
branch_filter, branch_params = self.branch.get_query_filter_path(
|
|
464
|
+
at=self.at.to_string(), branch_agnostic=self.branch_agnostic
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
self.params.update(branch_params)
|
|
468
|
+
self.params["attribute_name"] = self.pool.node_attribute.value
|
|
469
|
+
|
|
470
|
+
query = """
|
|
471
|
+
MATCH (pool:%(number_pool)s { uuid: $pool_id })-[res:IS_RESERVED]->(av:AttributeValueIndexed)
|
|
472
|
+
WHERE toInteger(av.value) >= $start_range and toInteger(av.value) <= $end_range
|
|
473
|
+
CALL (pool, res, av) {
|
|
474
|
+
MATCH (pool)-[res]->(av)<-[hv:HAS_VALUE]-(attr:Attribute)<-[ha:HAS_ATTRIBUTE]-(n:%(node)s)
|
|
475
|
+
WHERE
|
|
476
|
+
n.uuid = res.identifier AND
|
|
477
|
+
attr.name = $attribute_name AND
|
|
478
|
+
all(r in [res, hv, ha] WHERE (%(branch_filter)s))
|
|
479
|
+
ORDER BY res.branch_level DESC, hv.branch_level DESC, ha.branch_level DESC,
|
|
480
|
+
res.from DESC, hv.from DESC, ha.from DESC,
|
|
481
|
+
res.status ASC, hv.status ASC, ha.status ASC
|
|
482
|
+
RETURN (res.status = "active" AND hv.status = "active" AND ha.status = "active") AS is_active
|
|
483
|
+
LIMIT 1
|
|
484
|
+
}
|
|
485
|
+
WITH av, res, is_active
|
|
486
|
+
WHERE is_active = True
|
|
487
|
+
WITH DISTINCT toInteger(av.value) AS used_value
|
|
488
|
+
ORDER BY used_value ASC
|
|
489
|
+
WITH [$start_range - 1] + collect(used_value) AS nums
|
|
490
|
+
UNWIND range(0, size(nums) - 1) AS idx
|
|
491
|
+
CALL (nums, idx) {
|
|
492
|
+
WITH nums[idx] AS curr, idx - 1 + $start_range AS expected
|
|
493
|
+
RETURN expected AS number, expected <> curr AS is_free, idx = size(nums) - 1 AS is_last
|
|
494
|
+
}
|
|
495
|
+
WITH number, is_free, is_last
|
|
496
|
+
WHERE is_free = true OR is_last = true
|
|
497
|
+
WITH number AS free_number, is_free, is_last
|
|
498
|
+
""" % {
|
|
499
|
+
"branch_filter": branch_filter,
|
|
500
|
+
"number_pool": InfrahubKind.NUMBERPOOL,
|
|
501
|
+
"node": self.pool.node.value,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
self.add_to_query(query)
|
|
505
|
+
self.return_labels = ["free_number as value", "is_free", "is_last"]
|
|
506
|
+
self.order_by = ["value"]
|
|
507
|
+
|
|
508
|
+
def get_free_data(self) -> NumberPoolFreeData | None:
|
|
509
|
+
if not self.results:
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
return NumberPoolFreeData.from_db(result=self.results[0])
|
|
513
|
+
|
|
514
|
+
def get_result_value(self) -> int | None:
|
|
515
|
+
"""Get the free number from query results, handling edge cases.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
The free number if found, None if pool is exhausted in queried range.
|
|
519
|
+
"""
|
|
520
|
+
result_data = self.get_free_data()
|
|
521
|
+
if result_data is None:
|
|
522
|
+
# No reservations in range - return start_range
|
|
523
|
+
if self.params["start_range"] <= self.params["end_range"]:
|
|
524
|
+
return self.params["start_range"]
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
if result_data.is_free:
|
|
528
|
+
return result_data.value
|
|
529
|
+
# is_last=True and is_free=False means all numbers up to value are used
|
|
530
|
+
if result_data.is_last and result_data.value < self.params["end_range"]:
|
|
531
|
+
return result_data.value + 1
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
|
|
422
535
|
class NumberPoolSetReserved(Query):
|
|
423
536
|
name = "numberpool_set_reserved"
|
|
424
537
|
type = QueryType.WRITE
|
|
@@ -652,7 +652,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin, MetadataInterface):
|
|
|
652
652
|
destination_id=peer.id,
|
|
653
653
|
status="active",
|
|
654
654
|
direction=self.schema.direction.value,
|
|
655
|
-
branch_level=
|
|
655
|
+
branch_level=branch.hierarchy_level,
|
|
656
656
|
branch_support=self.schema.branch.value if self.schema.branch else None,
|
|
657
657
|
hierarchical=self.schema.hierarchical,
|
|
658
658
|
is_protected=self.is_protected,
|
|
@@ -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")
|
|
@@ -182,6 +182,7 @@ class SchemaNode(BaseModel):
|
|
|
182
182
|
display_label: str | None = None
|
|
183
183
|
display_labels: list[str]
|
|
184
184
|
uniqueness_constraints: list[list[str]] | None = None
|
|
185
|
+
human_friendly_id: list[str] | None = None
|
|
185
186
|
|
|
186
187
|
def to_dict(self) -> dict[str, Any]:
|
|
187
188
|
return {
|
|
@@ -199,6 +200,7 @@ class SchemaNode(BaseModel):
|
|
|
199
200
|
"display_label": self.display_label,
|
|
200
201
|
"display_labels": self.display_labels,
|
|
201
202
|
"uniqueness_constraints": self.uniqueness_constraints,
|
|
203
|
+
"human_friendly_id": self.human_friendly_id,
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
def without_duplicates(self, other: SchemaNode) -> SchemaNode:
|
|
@@ -225,7 +227,6 @@ base_node_schema = SchemaNode(
|
|
|
225
227
|
namespace="Schema",
|
|
226
228
|
branch=BranchSupportType.AWARE.value,
|
|
227
229
|
include_in_menu=False,
|
|
228
|
-
default_filter="name__value",
|
|
229
230
|
display_labels=["label__value"],
|
|
230
231
|
attributes=[
|
|
231
232
|
SchemaAttribute(
|
|
@@ -239,7 +240,7 @@ base_node_schema = SchemaNode(
|
|
|
239
240
|
name="name",
|
|
240
241
|
kind="Text",
|
|
241
242
|
description="Node name, must be unique within a namespace and must start with an uppercase letter.",
|
|
242
|
-
unique=
|
|
243
|
+
unique=False,
|
|
243
244
|
regex=str(NODE_NAME_REGEX),
|
|
244
245
|
min_length=DEFAULT_NAME_MIN_LENGTH,
|
|
245
246
|
max_length=DEFAULT_NAME_MAX_LENGTH,
|
|
@@ -394,8 +395,9 @@ node_schema = SchemaNode(
|
|
|
394
395
|
namespace="Schema",
|
|
395
396
|
branch=BranchSupportType.AWARE.value,
|
|
396
397
|
include_in_menu=False,
|
|
397
|
-
default_filter="name__value",
|
|
398
398
|
display_labels=["label__value"],
|
|
399
|
+
human_friendly_id=["namespace__value", "name__value"],
|
|
400
|
+
uniqueness_constraints=[["namespace__value", "name__value"]],
|
|
399
401
|
attributes=base_node_schema.attributes
|
|
400
402
|
+ [
|
|
401
403
|
SchemaAttribute(
|
|
@@ -898,8 +900,9 @@ generic_schema = SchemaNode(
|
|
|
898
900
|
namespace="Schema",
|
|
899
901
|
branch=BranchSupportType.AWARE.value,
|
|
900
902
|
include_in_menu=False,
|
|
901
|
-
default_filter="name__value",
|
|
902
903
|
display_labels=["label__value"],
|
|
904
|
+
human_friendly_id=["namespace__value", "name__value"],
|
|
905
|
+
uniqueness_constraints=[["namespace__value", "name__value"]],
|
|
903
906
|
attributes=base_node_schema.attributes
|
|
904
907
|
+ [
|
|
905
908
|
SchemaAttribute(
|