1
1
from __future__ import annotations
2
2
3
+ from collections .abc import Iterable
3
4
from copy import copy
4
5
from typing import TYPE_CHECKING , Any
5
6
6
7
from ..constants import InfrahubClientMode
7
- from ..exceptions import (
8
- FeatureNotSupportedError ,
9
- NodeNotFoundError ,
10
- )
8
+ from ..exceptions import FeatureNotSupportedError , NodeNotFoundError , ResourceNotDefinedError , SchemaNotFoundError
11
9
from ..graphql import Mutation , Query
12
10
from ..schema import GenericSchemaAPI , RelationshipCardinality , RelationshipKind
13
11
from ..utils import compare_lists , generate_short_id , get_flat_value
30
28
from ..types import Order
31
29
32
30
33
- def generate_relationship_property (node : InfrahubNode | InfrahubNodeSync , name : str ) -> property :
34
- """Generates a property that stores values under a private non-public name.
35
-
36
- Args:
37
- node (Union[InfrahubNode, InfrahubNodeSync]): The node instance.
38
- name (str): The name of the relationship property.
39
-
40
- Returns:
41
- A property object for managing the relationship.
42
-
43
- """
44
- internal_name = "_" + name .lower ()
45
- external_name = name
46
-
47
- def prop_getter (self : InfrahubNodeBase ) -> Any :
48
- return getattr (self , internal_name )
49
-
50
- def prop_setter (self : InfrahubNodeBase , value : Any ) -> None :
51
- if isinstance (value , RelatedNodeBase ) or value is None :
52
- setattr (self , internal_name , value )
53
- else :
54
- schema = [rel for rel in self ._schema .relationships if rel .name == external_name ][0 ]
55
- if isinstance (node , InfrahubNode ):
56
- setattr (
57
- self ,
58
- internal_name ,
59
- RelatedNode (
60
- name = external_name , branch = node ._branch , client = node ._client , schema = schema , data = value
61
- ),
62
- )
63
- else :
64
- setattr (
65
- self ,
66
- internal_name ,
67
- RelatedNodeSync (
68
- name = external_name , branch = node ._branch , client = node ._client , schema = schema , data = value
69
- ),
70
- )
71
-
72
- return property (prop_getter , prop_setter )
73
-
74
-
75
31
class InfrahubNodeBase :
76
32
"""Base class for InfrahubNode and InfrahubNodeSync"""
77
33
@@ -86,6 +42,7 @@ def __init__(self, schema: MainSchemaTypesAPI, branch: str, data: dict | None =
86
42
self ._data = data
87
43
self ._branch = branch
88
44
self ._existing : bool = True
45
+ self ._attribute_data : dict [str , Attribute ] = {}
89
46
90
47
# Generate a unique ID only to be used inside the SDK
91
48
# The format if this ID is purposely different from the ID used by the API
@@ -180,12 +137,18 @@ def hfid_str(self) -> str | None:
180
137
def _init_attributes (self , data : dict | None = None ) -> None :
181
138
for attr_schema in self ._schema .attributes :
182
139
attr_data = data .get (attr_schema .name , None ) if isinstance (data , dict ) else None
183
- setattr (
184
- self ,
185
- attr_schema .name ,
186
- Attribute (name = attr_schema .name , schema = attr_schema , data = attr_data ),
140
+ self ._attribute_data [attr_schema .name ] = Attribute (
141
+ name = attr_schema .name , schema = attr_schema , data = attr_data
187
142
)
188
143
144
+ def __setattr__ (self , name : str , value : Any ) -> None :
145
+ """Set values for attributes that exist or revert to normal behaviour"""
146
+ if "_attribute_data" in self .__dict__ and name in self ._attribute_data :
147
+ self ._attribute_data [name ].value = value
148
+ return
149
+
150
+ super ().__setattr__ (name , value )
151
+
189
152
def _get_request_context (self , request_context : RequestContext | None = None ) -> dict [str , Any ] | None :
190
153
if request_context :
191
154
return request_context .model_dump (exclude_none = True )
@@ -487,6 +450,12 @@ def _relationship_mutation(self, action: str, relation_to_update: str, related_n
487
450
}}
488
451
"""
489
452
453
+ def _get_attribute (self , name : str ) -> Attribute :
454
+ if name in self ._attribute_data :
455
+ return self ._attribute_data [name ]
456
+
457
+ raise ResourceNotDefinedError (message = f"The node doesn't have an attribute for { name } " )
458
+
490
459
491
460
class InfrahubNode (InfrahubNodeBase ):
492
461
"""Represents a Infrahub node in an asynchronous context."""
@@ -506,11 +475,13 @@ def __init__(
506
475
data: Optional data to initialize the node.
507
476
"""
508
477
self ._client = client
509
- self .__class__ = type (f"{ schema .kind } InfrahubNode" , (self .__class__ ,), {})
510
478
511
479
if isinstance (data , dict ) and isinstance (data .get ("node" ), dict ):
512
480
data = data .get ("node" )
513
481
482
+ self ._relationship_cardinality_many_data : dict [str , RelationshipManager ] = {}
483
+ self ._relationship_cardinality_one_data : dict [str , RelatedNode ] = {}
484
+
514
485
super ().__init__ (schema = schema , branch = branch or client .default_branch , data = data )
515
486
516
487
@classmethod
@@ -535,26 +506,45 @@ def _init_relationships(self, data: dict | None = None) -> None:
535
506
rel_data = data .get (rel_schema .name , None ) if isinstance (data , dict ) else None
536
507
537
508
if rel_schema .cardinality == "one" :
538
- setattr (self , f"_{ rel_schema .name } " , None )
539
- setattr (
540
- self .__class__ ,
541
- rel_schema .name ,
542
- generate_relationship_property (name = rel_schema .name , node = self ),
509
+ self ._relationship_cardinality_one_data [rel_schema .name ] = RelatedNode (
510
+ name = rel_schema .name , branch = self ._branch , client = self ._client , schema = rel_schema , data = rel_data
543
511
)
544
- setattr (self , rel_schema .name , rel_data )
545
512
else :
546
- setattr (
547
- self ,
548
- rel_schema .name ,
549
- RelationshipManager (
550
- name = rel_schema .name ,
551
- client = self ._client ,
552
- node = self ,
553
- branch = self ._branch ,
554
- schema = rel_schema ,
555
- data = rel_data ,
556
- ),
513
+ self ._relationship_cardinality_many_data [rel_schema .name ] = RelationshipManager (
514
+ name = rel_schema .name ,
515
+ client = self ._client ,
516
+ node = self ,
517
+ branch = self ._branch ,
518
+ schema = rel_schema ,
519
+ data = rel_data ,
520
+ )
521
+
522
+ def __getattr__ (self , name : str ) -> Attribute | RelationshipManager | RelatedNode :
523
+ if "_attribute_data" in self .__dict__ and name in self ._attribute_data :
524
+ return self ._attribute_data [name ]
525
+ if "_relationship_cardinality_many_data" in self .__dict__ and name in self ._relationship_cardinality_many_data :
526
+ return self ._relationship_cardinality_many_data [name ]
527
+ if "_relationship_cardinality_one_data" in self .__dict__ and name in self ._relationship_cardinality_one_data :
528
+ return self ._relationship_cardinality_one_data [name ]
529
+
530
+ raise AttributeError (f"'{ self .__class__ .__name__ } ' object has no attribute '{ name } '" )
531
+
532
+ def __setattr__ (self , name : str , value : Any ) -> None :
533
+ """Set values for relationship names that exist or revert to normal behaviour"""
534
+ if "_relationship_cardinality_one_data" in self .__dict__ and name in self ._relationship_cardinality_one_data :
535
+ rel_schemas = [rel_schema for rel_schema in self ._schema .relationships if rel_schema .name == name ]
536
+ if not rel_schemas :
537
+ raise SchemaNotFoundError (
538
+ identifier = self ._schema .kind ,
539
+ message = f"Unable to find relationship schema for '{ name } ' on { self ._schema .kind } " ,
557
540
)
541
+ rel_schema = rel_schemas [0 ]
542
+ self ._relationship_cardinality_one_data [name ] = RelatedNode (
543
+ name = rel_schema .name , branch = self ._branch , client = self ._client , schema = rel_schema , data = value
544
+ )
545
+ return
546
+
547
+ super ().__setattr__ (name , value )
558
548
559
549
async def generate (self , nodes : list [str ] | None = None ) -> None :
560
550
self ._validate_artifact_definition_support (ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
@@ -568,14 +558,14 @@ async def artifact_generate(self, name: str) -> None:
568
558
self ._validate_artifact_support (ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
569
559
570
560
artifact = await self ._client .get (kind = "CoreArtifact" , name__value = name , object__ids = [self .id ])
571
- await artifact .definition .fetch () # type: ignore[attr-defined]
572
- await artifact .definition .peer .generate ([artifact .id ]) # type: ignore[attr-defined]
561
+ await artifact ._get_relationship_one ( name = " definition" ) .fetch ()
562
+ await artifact ._get_relationship_one ( name = " definition" ) .peer .generate ([artifact .id ])
573
563
574
564
async def artifact_fetch (self , name : str ) -> str | dict [str , Any ]:
575
565
self ._validate_artifact_support (ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
576
566
577
567
artifact = await self ._client .get (kind = "CoreArtifact" , name__value = name , object__ids = [self .id ])
578
- content = await self ._client .object_store .get (identifier = artifact .storage_id .value ) # type: ignore[attr-defined]
568
+ content = await self ._client .object_store .get (identifier = artifact ._get_attribute ( name = " storage_id" ) .value )
579
569
return content
580
570
581
571
async def delete (self , timeout : int | None = None , request_context : RequestContext | None = None ) -> None :
@@ -1018,6 +1008,27 @@ async def get_pool_resources_utilization(self) -> list[dict[str, Any]]:
1018
1008
return [edge ["node" ] for edge in response [graphql_query_name ]["edges" ]]
1019
1009
return []
1020
1010
1011
+ def _get_relationship_many (self , name : str ) -> RelationshipManager :
1012
+ if name in self ._relationship_cardinality_many_data :
1013
+ return self ._relationship_cardinality_many_data [name ]
1014
+
1015
+ raise ResourceNotDefinedError (message = f"The node doesn't have a cardinality=many relationship for { name } " )
1016
+
1017
+ def _get_relationship_one (self , name : str ) -> RelatedNode :
1018
+ if name in self ._relationship_cardinality_one_data :
1019
+ return self ._relationship_cardinality_one_data [name ]
1020
+
1021
+ raise ResourceNotDefinedError (message = f"The node doesn't have a cardinality=one relationship for { name } " )
1022
+
1023
+ def __dir__ (self ) -> Iterable [str ]:
1024
+ base = list (super ().__dir__ ())
1025
+ return sorted (
1026
+ base
1027
+ + list (self ._attribute_data .keys ())
1028
+ + list (self ._relationship_cardinality_many_data .keys ())
1029
+ + list (self ._relationship_cardinality_one_data .keys ())
1030
+ )
1031
+
1021
1032
1022
1033
class InfrahubNodeSync (InfrahubNodeBase ):
1023
1034
"""Represents a Infrahub node in a synchronous context."""
@@ -1036,12 +1047,14 @@ def __init__(
1036
1047
branch (Optional[str]): The branch where the node resides.
1037
1048
data (Optional[dict]): Optional data to initialize the node.
1038
1049
"""
1039
- self .__class__ = type (f"{ schema .kind } InfrahubNodeSync" , (self .__class__ ,), {})
1040
1050
self ._client = client
1041
1051
1042
1052
if isinstance (data , dict ) and isinstance (data .get ("node" ), dict ):
1043
1053
data = data .get ("node" )
1044
1054
1055
+ self ._relationship_cardinality_many_data : dict [str , RelationshipManagerSync ] = {}
1056
+ self ._relationship_cardinality_one_data : dict [str , RelatedNodeSync ] = {}
1057
+
1045
1058
super ().__init__ (schema = schema , branch = branch or client .default_branch , data = data )
1046
1059
1047
1060
@classmethod
@@ -1066,27 +1079,47 @@ def _init_relationships(self, data: dict | None = None) -> None:
1066
1079
rel_data = data .get (rel_schema .name , None ) if isinstance (data , dict ) else None
1067
1080
1068
1081
if rel_schema .cardinality == "one" :
1069
- setattr (self , f"_{ rel_schema .name } " , None )
1070
- setattr (
1071
- self .__class__ ,
1072
- rel_schema .name ,
1073
- generate_relationship_property (name = rel_schema .name , node = self ),
1082
+ self ._relationship_cardinality_one_data [rel_schema .name ] = RelatedNodeSync (
1083
+ name = rel_schema .name , branch = self ._branch , client = self ._client , schema = rel_schema , data = rel_data
1074
1084
)
1075
- setattr ( self , rel_schema . name , rel_data )
1085
+
1076
1086
else :
1077
- setattr (
1078
- self ,
1079
- rel_schema .name ,
1080
- RelationshipManagerSync (
1081
- name = rel_schema .name ,
1082
- client = self ._client ,
1083
- node = self ,
1084
- branch = self ._branch ,
1085
- schema = rel_schema ,
1086
- data = rel_data ,
1087
- ),
1087
+ self ._relationship_cardinality_many_data [rel_schema .name ] = RelationshipManagerSync (
1088
+ name = rel_schema .name ,
1089
+ client = self ._client ,
1090
+ node = self ,
1091
+ branch = self ._branch ,
1092
+ schema = rel_schema ,
1093
+ data = rel_data ,
1088
1094
)
1089
1095
1096
+ def __getattr__ (self , name : str ) -> Attribute | RelationshipManagerSync | RelatedNodeSync :
1097
+ if "_attribute_data" in self .__dict__ and name in self ._attribute_data :
1098
+ return self ._attribute_data [name ]
1099
+ if "_relationship_cardinality_many_data" in self .__dict__ and name in self ._relationship_cardinality_many_data :
1100
+ return self ._relationship_cardinality_many_data [name ]
1101
+ if "_relationship_cardinality_one_data" in self .__dict__ and name in self ._relationship_cardinality_one_data :
1102
+ return self ._relationship_cardinality_one_data [name ]
1103
+
1104
+ raise AttributeError (f"'{ self .__class__ .__name__ } ' object has no attribute '{ name } '" )
1105
+
1106
+ def __setattr__ (self , name : str , value : Any ) -> None :
1107
+ """Set values for relationship names that exist or revert to normal behaviour"""
1108
+ if "_relationship_cardinality_one_data" in self .__dict__ and name in self ._relationship_cardinality_one_data :
1109
+ rel_schemas = [rel_schema for rel_schema in self ._schema .relationships if rel_schema .name == name ]
1110
+ if not rel_schemas :
1111
+ raise SchemaNotFoundError (
1112
+ identifier = self ._schema .kind ,
1113
+ message = f"Unable to find relationship schema for '{ name } ' on { self ._schema .kind } " ,
1114
+ )
1115
+ rel_schema = rel_schemas [0 ]
1116
+ self ._relationship_cardinality_one_data [name ] = RelatedNodeSync (
1117
+ name = rel_schema .name , branch = self ._branch , client = self ._client , schema = rel_schema , data = value
1118
+ )
1119
+ return
1120
+
1121
+ super ().__setattr__ (name , value )
1122
+
1090
1123
def generate (self , nodes : list [str ] | None = None ) -> None :
1091
1124
self ._validate_artifact_definition_support (ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
1092
1125
nodes = nodes or []
@@ -1097,13 +1130,13 @@ def generate(self, nodes: list[str] | None = None) -> None:
1097
1130
def artifact_generate (self , name : str ) -> None :
1098
1131
self ._validate_artifact_support (ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
1099
1132
artifact = self ._client .get (kind = "CoreArtifact" , name__value = name , object__ids = [self .id ])
1100
- artifact .definition .fetch () # type: ignore[attr-defined]
1101
- artifact .definition .peer .generate ([artifact .id ]) # type: ignore[attr-defined]
1133
+ artifact ._get_relationship_one ( name = " definition" ) .fetch ()
1134
+ artifact ._get_relationship_one ( name = " definition" ) .peer .generate ([artifact .id ])
1102
1135
1103
1136
def artifact_fetch (self , name : str ) -> str | dict [str , Any ]:
1104
1137
self ._validate_artifact_support (ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE )
1105
1138
artifact = self ._client .get (kind = "CoreArtifact" , name__value = name , object__ids = [self .id ])
1106
- content = self ._client .object_store .get (identifier = artifact .storage_id .value ) # type: ignore[attr-defined]
1139
+ content = self ._client .object_store .get (identifier = artifact ._get_attribute ( name = " storage_id" ) .value )
1107
1140
return content
1108
1141
1109
1142
def delete (self , timeout : int | None = None , request_context : RequestContext | None = None ) -> None :
@@ -1545,3 +1578,24 @@ def get_pool_resources_utilization(self) -> list[dict[str, Any]]:
1545
1578
if response [graphql_query_name ].get ("count" , 0 ):
1546
1579
return [edge ["node" ] for edge in response [graphql_query_name ]["edges" ]]
1547
1580
return []
1581
+
1582
+ def _get_relationship_many (self , name : str ) -> RelationshipManager | RelationshipManagerSync :
1583
+ if name in self ._relationship_cardinality_many_data :
1584
+ return self ._relationship_cardinality_many_data [name ]
1585
+
1586
+ raise ResourceNotDefinedError (message = f"The node doesn't have a cardinality=many relationship for { name } " )
1587
+
1588
+ def _get_relationship_one (self , name : str ) -> RelatedNode | RelatedNodeSync :
1589
+ if name in self ._relationship_cardinality_one_data :
1590
+ return self ._relationship_cardinality_one_data [name ]
1591
+
1592
+ raise ResourceNotDefinedError (message = f"The node doesn't have a cardinality=one relationship for { name } " )
1593
+
1594
+ def __dir__ (self ) -> Iterable [str ]:
1595
+ base = list (super ().__dir__ ())
1596
+ return sorted (
1597
+ base
1598
+ + list (self ._attribute_data .keys ())
1599
+ + list (self ._relationship_cardinality_many_data .keys ())
1600
+ + list (self ._relationship_cardinality_one_data .keys ())
1601
+ )
0 commit comments