Skip to content

Commit dd16f8d

Browse files
authored
Merge pull request #412 from opsmill/pog-avoid-dynamic-class-creation
Refactor InfrahubNode to avoid dynamic class creation
2 parents f414027 + 55d3d84 commit dd16f8d

File tree

6 files changed

+161
-103
lines changed

6 files changed

+161
-103
lines changed

changelog/+040fc56b.housekeeping.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactor InfrahubNode to avoid the creation of a dynamic Python class for each object defined

infrahub_sdk/ctl/generator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,20 +74,20 @@ async def run(
7474
targets = await client.get(
7575
kind="CoreGroup", branch=branch, include=["members"], name__value=generator_config.targets
7676
)
77-
await targets.members.fetch()
77+
await targets._get_relationship_many(name="members").fetch()
7878

79-
if not targets.members.peers:
79+
if not targets._get_relationship_many(name="members").peers:
8080
console.print(
8181
f"[red]No members found within '{generator_config.targets}', not running generator '{generator_name}'"
8282
)
8383
return
8484

85-
for member in targets.members.peers:
85+
for member in targets._get_relationship_many(name="members").peers:
8686
check_parameter = {}
8787
if identifier:
8888
attribute = getattr(member.peer, identifier)
8989
check_parameter = {identifier: attribute.value}
90-
params = {"name": member.peer.name.value}
90+
params = {"name": member.peer._get_attribute(name="name").value}
9191
generator = generator_class(
9292
query=generator_config.query,
9393
client=client,

infrahub_sdk/node/node.py

Lines changed: 146 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from __future__ import annotations
22

3+
from collections.abc import Iterable
34
from copy import copy
45
from typing import TYPE_CHECKING, Any
56

67
from ..constants import InfrahubClientMode
7-
from ..exceptions import (
8-
FeatureNotSupportedError,
9-
NodeNotFoundError,
10-
)
8+
from ..exceptions import FeatureNotSupportedError, NodeNotFoundError, ResourceNotDefinedError, SchemaNotFoundError
119
from ..graphql import Mutation, Query
1210
from ..schema import GenericSchemaAPI, RelationshipCardinality, RelationshipKind
1311
from ..utils import compare_lists, generate_short_id, get_flat_value
@@ -30,48 +28,6 @@
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-
7531
class 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

491460
class 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

10221033
class 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

Comments
 (0)