11from __future__ import annotations
22
3+ from collections .abc import Iterable
34from copy import copy
45from typing import TYPE_CHECKING , Any
56
67from ..constants import InfrahubClientMode
7- from ..exceptions import (
8- FeatureNotSupportedError ,
9- NodeNotFoundError ,
10- )
8+ from ..exceptions import FeatureNotSupportedError , NodeNotFoundError , ResourceNotDefinedError , SchemaNotFoundError
119from ..graphql import Mutation , Query
1210from ..schema import GenericSchemaAPI , RelationshipCardinality , RelationshipKind
1311from ..utils import compare_lists , generate_short_id , get_flat_value
3028 from ..types import Order
3129
3230
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-
7531class InfrahubNodeBase :
7632 """Base class for InfrahubNode and InfrahubNodeSync"""
7733
@@ -86,6 +42,7 @@ def __init__(self, schema: MainSchemaTypesAPI, branch: str, data: dict | None =
8642 self ._data = data
8743 self ._branch = branch
8844 self ._existing : bool = True
45+ self ._attribute_data : dict [str , Attribute ] = {}
8946
9047 # Generate a unique ID only to be used inside the SDK
9148 # 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:
180137 def _init_attributes (self , data : dict | None = None ) -> None :
181138 for attr_schema in self ._schema .attributes :
182139 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
187142 )
188143
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+
189152 def _get_request_context (self , request_context : RequestContext | None = None ) -> dict [str , Any ] | None :
190153 if request_context :
191154 return request_context .model_dump (exclude_none = True )
@@ -487,6 +450,12 @@ def _relationship_mutation(self, action: str, relation_to_update: str, related_n
487450 }}
488451 """
489452
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+
490459
491460class InfrahubNode (InfrahubNodeBase ):
492461 """Represents a Infrahub node in an asynchronous context."""
@@ -506,11 +475,13 @@ def __init__(
506475 data: Optional data to initialize the node.
507476 """
508477 self ._client = client
509- self .__class__ = type (f"{ schema .kind } InfrahubNode" , (self .__class__ ,), {})
510478
511479 if isinstance (data , dict ) and isinstance (data .get ("node" ), dict ):
512480 data = data .get ("node" )
513481
482+ self ._relationship_cardinality_many_data : dict [str , RelationshipManager ] = {}
483+ self ._relationship_cardinality_one_data : dict [str , RelatedNode ] = {}
484+
514485 super ().__init__ (schema = schema , branch = branch or client .default_branch , data = data )
515486
516487 @classmethod
@@ -535,26 +506,45 @@ def _init_relationships(self, data: dict | None = None) -> None:
535506 rel_data = data .get (rel_schema .name , None ) if isinstance (data , dict ) else None
536507
537508 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
543511 )
544- setattr (self , rel_schema .name , rel_data )
545512 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 } " ,
557540 )
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 )
558548
559549 async def generate (self , nodes : list [str ] | None = None ) -> None :
560550 self ._validate_artifact_definition_support (ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
@@ -568,14 +558,14 @@ async def artifact_generate(self, name: str) -> None:
568558 self ._validate_artifact_support (ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
569559
570560 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 ])
573563
574564 async def artifact_fetch (self , name : str ) -> str | dict [str , Any ]:
575565 self ._validate_artifact_support (ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
576566
577567 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 )
579569 return content
580570
581571 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]]:
10181008 return [edge ["node" ] for edge in response [graphql_query_name ]["edges" ]]
10191009 return []
10201010
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+
10211032
10221033class InfrahubNodeSync (InfrahubNodeBase ):
10231034 """Represents a Infrahub node in a synchronous context."""
@@ -1036,12 +1047,14 @@ def __init__(
10361047 branch (Optional[str]): The branch where the node resides.
10371048 data (Optional[dict]): Optional data to initialize the node.
10381049 """
1039- self .__class__ = type (f"{ schema .kind } InfrahubNodeSync" , (self .__class__ ,), {})
10401050 self ._client = client
10411051
10421052 if isinstance (data , dict ) and isinstance (data .get ("node" ), dict ):
10431053 data = data .get ("node" )
10441054
1055+ self ._relationship_cardinality_many_data : dict [str , RelationshipManagerSync ] = {}
1056+ self ._relationship_cardinality_one_data : dict [str , RelatedNodeSync ] = {}
1057+
10451058 super ().__init__ (schema = schema , branch = branch or client .default_branch , data = data )
10461059
10471060 @classmethod
@@ -1066,27 +1079,47 @@ def _init_relationships(self, data: dict | None = None) -> None:
10661079 rel_data = data .get (rel_schema .name , None ) if isinstance (data , dict ) else None
10671080
10681081 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
10741084 )
1075- setattr ( self , rel_schema . name , rel_data )
1085+
10761086 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 ,
10881094 )
10891095
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+
10901123 def generate (self , nodes : list [str ] | None = None ) -> None :
10911124 self ._validate_artifact_definition_support (ARTIFACT_DEFINITION_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
10921125 nodes = nodes or []
@@ -1097,13 +1130,13 @@ def generate(self, nodes: list[str] | None = None) -> None:
10971130 def artifact_generate (self , name : str ) -> None :
10981131 self ._validate_artifact_support (ARTIFACT_GENERATE_FEATURE_NOT_SUPPORTED_MESSAGE )
10991132 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 ])
11021135
11031136 def artifact_fetch (self , name : str ) -> str | dict [str , Any ]:
11041137 self ._validate_artifact_support (ARTIFACT_FETCH_FEATURE_NOT_SUPPORTED_MESSAGE )
11051138 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 )
11071140 return content
11081141
11091142 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]]:
15451578 if response [graphql_query_name ].get ("count" , 0 ):
15461579 return [edge ["node" ] for edge in response [graphql_query_name ]["edges" ]]
15471580 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