-
Notifications
You must be signed in to change notification settings - Fork 6
IHS-156 / #497 : support from_pool attributes on Python SDK queries #850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
76d96d8
531a1a6
b1f51af
d831b3a
ac1db9e
86a8a88
1c5c452
6b33e0d
eaf4d4d
c871d42
0ca55cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Fixed Python SDK query generation regarding from_pool generated attribute value |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is much easier to understand now |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |
|
|
||
| import ipaddress | ||
| from collections.abc import Callable | ||
| from typing import TYPE_CHECKING, Any, get_args | ||
| from typing import TYPE_CHECKING, Any, NamedTuple, get_args | ||
|
|
||
| from ..protocols_base import CoreNodeBase | ||
| from ..uuidt import UUIDT | ||
|
|
@@ -25,8 +25,12 @@ def __init__(self, name: str, schema: AttributeSchemaAPI, data: Any | dict) -> N | |
| """ | ||
| self.name = name | ||
| self._schema = schema | ||
| self._from_pool: dict[str, Any] | None = None | ||
|
|
||
| if not isinstance(data, dict) or "value" not in data: | ||
| if isinstance(data, dict) and "from_pool" in data: | ||
| self._from_pool = data.pop("from_pool") | ||
| data.setdefault("value", None) | ||
| elif not isinstance(data, dict) or "value" not in data: | ||
| data = {"value": data} | ||
polmichel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| self._properties_flag = PROPERTIES_FLAG | ||
|
|
@@ -76,38 +80,57 @@ def value(self, value: Any) -> None: | |
| self._value = value | ||
| self.value_has_been_mutated = True | ||
|
|
||
| def _generate_input_data(self) -> dict | None: | ||
| data: dict[str, Any] = {} | ||
| variables: dict[str, Any] = {} | ||
| def _initialize_graphql_payload(self) -> _GraphQLPayloadAttribute: | ||
| """Resolve the attribute value into a GraphQL mutation payload object.""" | ||
|
|
||
| if self.value is None: | ||
| if self._schema.optional and self.value_has_been_mutated: | ||
| data["value"] = None | ||
| return data | ||
|
|
||
| if isinstance(self.value, str): | ||
| if SAFE_VALUE.match(self.value): | ||
| data["value"] = self.value | ||
| else: | ||
| var_name = f"value_{UUIDT.new().hex}" | ||
| variables[var_name] = self.value | ||
| data["value"] = f"${var_name}" | ||
| elif isinstance(self.value, get_args(IP_TYPES)): | ||
| data["value"] = self.value.with_prefixlen | ||
| elif isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool(): | ||
| data["from_pool"] = {"id": self.value.id} | ||
| else: | ||
| data["value"] = self.value | ||
|
|
||
| for prop_name in self._properties_flag: | ||
| if getattr(self, prop_name) is not None: | ||
| data[prop_name] = getattr(self, prop_name) | ||
|
|
||
| for prop_name in self._properties_object: | ||
| if getattr(self, prop_name) is not None: | ||
| data[prop_name] = getattr(self, prop_name)._generate_input_data() | ||
| # Pool-based allocation (dict data or resource-pool node) | ||
| if self._from_pool is not None: | ||
| return _GraphQLPayloadAttribute( | ||
| payload_dict={"from_pool": self._from_pool}, variables={}, need_additional_properties=True | ||
| ) | ||
| if isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool(): | ||
| return _GraphQLPayloadAttribute( | ||
| payload_dict={"from_pool": {"id": self.value.id}}, variables={}, need_additional_properties=True | ||
| ) | ||
|
|
||
| return {"data": data, "variables": variables} | ||
| # Null value | ||
| if self.value is None: | ||
| data = {"value": None} if (self._schema.optional and self.value_has_been_mutated) else {} | ||
| return _GraphQLPayloadAttribute(payload_dict=data, variables={}, need_additional_properties=False) | ||
|
|
||
| # Unsafe strings need a variable binding to avoid injection | ||
| if isinstance(self.value, str) and not SAFE_VALUE.match(self.value): | ||
| var_name = f"value_{UUIDT.new().hex}" | ||
| return _GraphQLPayloadAttribute( | ||
| payload_dict={"value": f"${var_name}"}, | ||
| variables={var_name: self.value}, | ||
| need_additional_properties=True, | ||
| ) | ||
|
|
||
| # Safe strings, IP types, and everything else | ||
| value = self.value.with_prefixlen if isinstance(self.value, get_args(IP_TYPES)) else self.value | ||
| return _GraphQLPayloadAttribute(payload_dict={"value": value}, variables={}, need_additional_properties=True) | ||
polmichel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def _generate_input_data(self) -> _GraphQLPayloadAttribute: | ||
| """Build the input payload for a GraphQL mutation on this attribute. | ||
|
|
||
| Returns a ResolvedValue object, which contains all the data required. | ||
| """ | ||
| graphql_payload = self._initialize_graphql_payload() | ||
|
|
||
| properties_flag: dict[str, Any] = { | ||
| property_name: getattr(self, property_name) | ||
| for property_name in self._properties_flag | ||
| if getattr(self, property_name) is not None | ||
| } | ||
| properties_object: dict[str, dict] = { | ||
| property_name: getattr(self, property_name)._generate_input_data() | ||
| for property_name in self._properties_object | ||
| if getattr(self, property_name) is not None | ||
| } | ||
| graphql_payload.add_properties(properties_flag, properties_object) | ||
|
|
||
| return graphql_payload | ||
|
|
||
| def _generate_query_data(self, property: bool = False, include_metadata: bool = False) -> dict | None: | ||
| data: dict[str, Any] = {"value": None} | ||
|
|
@@ -128,7 +151,42 @@ def _generate_query_data(self, property: bool = False, include_metadata: bool = | |
| return data | ||
|
|
||
| def _generate_mutation_query(self) -> dict[str, Any]: | ||
| if isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool(): | ||
| if self.is_from_pool_attribute(): | ||
| # If it points to a pool, ask for the value of the pool allocated resource | ||
| return {self.name: {"value": None}} | ||
| return {} | ||
|
|
||
| def is_from_pool_attribute(self) -> bool: | ||
| """Check whether this attribute's value is sourced from a resource pool. | ||
|
|
||
| Returns: | ||
| True if the attribute value is a resource pool node or was explicitly | ||
| allocated from a pool. | ||
| """ | ||
| return (isinstance(self.value, CoreNodeBase) and self.value.is_resource_pool()) or self._from_pool is not None | ||
|
|
||
|
|
||
| class _GraphQLPayloadAttribute(NamedTuple): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say move it to the top of the file |
||
| """Result of resolving an attribute value for a GraphQL mutation. | ||
|
|
||
| Attributes: | ||
| payload_dict: Key/value entries to include in the mutation payload | ||
| (e.g. ``{"value": ...}`` or ``{"from_pool": ...}``). | ||
| variables: GraphQL variable bindings for unsafe string values. | ||
| need_additional_properties: When ``True``, the payload needs to append property flags/objects | ||
| """ | ||
|
|
||
| payload_dict: dict[str, Any] | ||
| variables: dict[str, Any] | ||
| need_additional_properties: bool | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd call this |
||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
| return {"data": self.payload_dict, "variables": self.variables} | ||
|
|
||
| def add_properties(self, properties_flag: dict[str, Any], properties_object: dict[str, dict]) -> None: | ||
| if self.need_additional_properties: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i personally prefer the early return to reduce indentation levels |
||
| for prop_name, prop in properties_flag.items(): | ||
| self.payload_dict[prop_name] = prop | ||
|
|
||
| for prop_name, prop in properties_object.items(): | ||
| self.payload_dict[prop_name] = prop | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return description is incorrectly split across two list items.
The second bullet ("allocated from a pool.") is a continuation of the first, not a separate return value. This renders oddly in MDX.
📝 Proposed fix
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the robot is right here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ajtmccartyGot it! I'll fix the formatting to merge those two bullets into a single line.Task: Fix documentation formatting for
is_from_pool_attributereturn descriptionTruefor consistency with documentation standards💡 For code changes across multiple files, use
@coderabbitaiplan in an issue comment