Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/+operation-name-payload.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Send the GraphQL operation name as `operationName` in the request payload so tracing and observability tools can identify each query.
8 changes: 6 additions & 2 deletions docs/docs/python-sdk/sdk_ref/infrahub_sdk/client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ Return a cloned version of the client using the same configuration
#### `execute_graphql`

```python
execute_graphql(self, query: str, variables: dict | None = None, branch_name: str | None = None, at: str | Timestamp | None = None, timeout: int | None = None, tracker: str | None = None) -> dict
execute_graphql(self, query: str, variables: dict | None = None, branch_name: str | None = None, at: str | Timestamp | None = None, timeout: int | None = None, tracker: str | None = None, operation_name: str | None = None) -> dict
```

Execute a GraphQL query (or mutation).
Expand All @@ -237,6 +237,8 @@ If retry_on_failure is True, the query will retry until the server becomes reach
- `branch_name`: Name of the branch on which the query will be executed. Defaults to None.
- `at`: Time when the query should be executed. Defaults to None.
- `timeout`: Timeout in second for the query. Defaults to None.
- `operation_name`: GraphQL operation name, sent as `operationName` in the request payload
so tracing/observability tools can identify the operation. Defaults to None.

**Raises:**

Expand Down Expand Up @@ -523,7 +525,7 @@ Return a cloned version of the client using the same configuration
#### `execute_graphql`

```python
execute_graphql(self, query: str, variables: dict | None = None, branch_name: str | None = None, at: str | Timestamp | None = None, timeout: int | None = None, tracker: str | None = None) -> dict
execute_graphql(self, query: str, variables: dict | None = None, branch_name: str | None = None, at: str | Timestamp | None = None, timeout: int | None = None, tracker: str | None = None, operation_name: str | None = None) -> dict
```

Execute a GraphQL query (or mutation).
Expand All @@ -536,6 +538,8 @@ If retry_on_failure is True, the query will retry until the server becomes reach
- `branch_name`: Name of the branch on which the query will be executed. Defaults to None.
- `at`: Time when the query should be executed. Defaults to None.
- `timeout`: Timeout in second for the query. Defaults to None.
- `operation_name`: GraphQL operation name, sent as `operationName` in the request payload
so tracing/observability tools can identify the operation. Defaults to None.

**Raises:**

Expand Down
8 changes: 6 additions & 2 deletions infrahub_sdk/branch.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ async def merge(self, branch_name: str) -> bool:

async def all(self) -> dict[str, BranchData]:
query = Query(name="GetAllBranch", query=QUERY_ALL_BRANCHES_DATA)
data = await self.client.execute_graphql(query=query.render(), tracker="query-branch-all")
data = await self.client.execute_graphql(
query=query.render(), tracker="query-branch-all", operation_name=query.name
)

return {branch["name"]: BranchData(**branch) for branch in data["Branch"]}

Expand All @@ -195,6 +197,7 @@ async def get(self, branch_name: str) -> BranchData:
query=query.render(),
variables={"branch_name": branch_name},
tracker="query-branch",
operation_name=query.name,
)

if not data["Branch"]:
Expand Down Expand Up @@ -225,7 +228,7 @@ def __init__(self, client: InfrahubClientSync) -> None:

def all(self) -> dict[str, BranchData]:
query = Query(name="GetAllBranch", query=QUERY_ALL_BRANCHES_DATA)
data = self.client.execute_graphql(query=query.render(), tracker="query-branch-all")
data = self.client.execute_graphql(query=query.render(), tracker="query-branch-all", operation_name=query.name)

return {branch["name"]: BranchData(**branch) for branch in data["Branch"]}

Expand All @@ -235,6 +238,7 @@ def get(self, branch_name: str) -> BranchData:
query=query.render(),
variables={"branch_name": branch_name},
tracker="query-branch",
operation_name=query.name,
)

if not data["Branch"]:
Expand Down
40 changes: 36 additions & 4 deletions infrahub_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ async def get_version(self) -> str:

async def get_user(self) -> dict:
"""Return user information"""
return await self.execute_graphql(query=QUERY_USER)
return await self.execute_graphql(query=QUERY_USER, operation_name="GET_PROFILE_DETAILS")

async def get_user_permissions(self) -> dict:
"""Return user permissions"""
Expand Down Expand Up @@ -636,6 +636,7 @@ async def count(
branch_name=branch,
at=at,
timeout=timeout,
operation_name=query_name,
)
return int(response.get(schema.kind, {}).get("count", 0))

Expand Down Expand Up @@ -868,6 +869,7 @@ async def process_page(page_offset: int, page_number: int) -> tuple[dict, Proces
at=at,
tracker=f"query-{str(schema.kind).lower()}-page{page_number}",
timeout=timeout,
operation_name=query.name,
)

process_result: ProcessRelationsNode = await self._process_nodes_and_relationships(
Expand Down Expand Up @@ -943,6 +945,7 @@ async def execute_graphql(
at: str | Timestamp | None = None,
timeout: int | None = None,
tracker: str | None = None,
operation_name: str | None = None,
) -> dict:
"""Execute a GraphQL query (or mutation).
If retry_on_failure is True, the query will retry until the server becomes reachable.
Expand All @@ -953,6 +956,8 @@ async def execute_graphql(
branch_name (str, optional): Name of the branch on which the query will be executed. Defaults to None.
at (str, optional): Time when the query should be executed. Defaults to None.
timeout (int, optional): Timeout in second for the query. Defaults to None.
operation_name (str, optional): GraphQL operation name, sent as `operationName` in the request payload
so tracing/observability tools can identify the operation. Defaults to None.

Raises:
GraphQLError: When the GraphQL response contains errors.
Expand All @@ -971,6 +976,8 @@ async def execute_graphql(
payload: dict[str, str | dict] = {"query": query}
if variables:
payload["variables"] = variables
if operation_name:
payload["operationName"] = operation_name

headers = copy.copy(self.headers or {})
if self.insert_tracker and tracker:
Expand Down Expand Up @@ -1027,6 +1034,7 @@ async def _execute_graphql_with_file(
branch_name: str | None = None,
timeout: int | None = None,
tracker: str | None = None,
operation_name: str | None = None,
) -> dict:
"""Execute a GraphQL mutation with a file upload using multipart/form-data.

Expand Down Expand Up @@ -1072,6 +1080,7 @@ async def _execute_graphql_with_file(
file_name=file_name or "upload",
headers=headers,
timeout=timeout,
operation_name=operation_name,
)

resp.raise_for_status()
Expand All @@ -1092,6 +1101,7 @@ async def _post_multipart(
file_name: str,
headers: dict | None = None,
timeout: int | None = None,
operation_name: str | None = None,
) -> httpx.Response:
"""Execute a HTTP POST with multipart/form-data for GraphQL file uploads.

Expand All @@ -1107,7 +1117,11 @@ async def _post_multipart(

# Build the multipart form data according to GraphQL Multipart Request Spec
files = MultipartBuilder.build_payload(
query=query, variables=variables, file_content=file_content, file_name=file_name
query=query,
variables=variables,
file_content=file_content,
file_name=file_name,
operation_name=operation_name,
)

return await self._request_multipart(
Expand Down Expand Up @@ -1440,6 +1454,7 @@ async def get_diff_summary(
timeout=timeout,
tracker=tracker,
variables=input_data,
operation_name="GetDiffTree",
)

node_diffs: list[NodeDiff] = []
Expand Down Expand Up @@ -1487,6 +1502,7 @@ async def get_diff_tree(
timeout=timeout,
tracker=tracker,
variables=input_data,
operation_name=query.name,
)

diff_tree = response["DiffTree"]
Expand Down Expand Up @@ -1817,7 +1833,7 @@ def get_version(self) -> str:

def get_user(self) -> dict:
"""Return user information"""
return self.execute_graphql(query=QUERY_USER)
return self.execute_graphql(query=QUERY_USER, operation_name="GET_PROFILE_DETAILS")

def get_user_permissions(self) -> dict:
"""Return user permissions"""
Expand Down Expand Up @@ -1877,6 +1893,7 @@ def execute_graphql(
at: str | Timestamp | None = None,
timeout: int | None = None,
tracker: str | None = None,
operation_name: str | None = None,
) -> dict:
"""Execute a GraphQL query (or mutation).
If retry_on_failure is True, the query will retry until the server becomes reachable.
Expand All @@ -1887,6 +1904,8 @@ def execute_graphql(
branch_name (str, optional): Name of the branch on which the query will be executed. Defaults to None.
at (str, optional): Time when the query should be executed. Defaults to None.
timeout (int, optional): Timeout in second for the query. Defaults to None.
operation_name (str, optional): GraphQL operation name, sent as `operationName` in the request payload
so tracing/observability tools can identify the operation. Defaults to None.

Raises:
GraphQLError: When the GraphQL response contains errors.
Expand All @@ -1905,6 +1924,8 @@ def execute_graphql(
payload: dict[str, str | dict] = {"query": query}
if variables:
payload["variables"] = variables
if operation_name:
payload["operationName"] = operation_name

headers = copy.copy(self.headers or {})
if self.insert_tracker and tracker:
Expand Down Expand Up @@ -1961,6 +1982,7 @@ def _execute_graphql_with_file(
branch_name: str | None = None,
timeout: int | None = None,
tracker: str | None = None,
operation_name: str | None = None,
) -> dict:
"""Execute a GraphQL mutation with a file upload using multipart/form-data.

Expand Down Expand Up @@ -2006,6 +2028,7 @@ def _execute_graphql_with_file(
file_name=file_name or "upload",
headers=headers,
timeout=timeout,
operation_name=operation_name,
)

resp.raise_for_status()
Expand All @@ -2026,6 +2049,7 @@ def _post_multipart(
file_name: str,
headers: dict | None = None,
timeout: int | None = None,
operation_name: str | None = None,
) -> httpx.Response:
"""Execute a HTTP POST with multipart/form-data for GraphQL file uploads.

Expand All @@ -2041,7 +2065,11 @@ def _post_multipart(

# Build the multipart form data according to GraphQL Multipart Request Spec
files = MultipartBuilder.build_payload(
query=query, variables=variables, file_content=file_content, file_name=file_name
query=query,
variables=variables,
file_content=file_content,
file_name=file_name,
operation_name=operation_name,
)

return self._request_multipart(url=url, headers=headers, timeout=timeout or self.default_timeout, files=files)
Expand Down Expand Up @@ -2112,6 +2140,7 @@ def count(
branch_name=branch,
at=at,
timeout=timeout,
operation_name=query_name,
)
return int(response.get(schema.kind, {}).get("count", 0))

Expand Down Expand Up @@ -2385,6 +2414,7 @@ def process_page(page_offset: int, page_number: int) -> tuple[dict, ProcessRelat
at=at,
timeout=timeout,
tracker=f"query-{str(schema.kind).lower()}-page{page_number}",
operation_name=query.name,
)

process_result: ProcessRelationsNodeSync = self._process_nodes_and_relationships(
Expand Down Expand Up @@ -2774,6 +2804,7 @@ def get_diff_summary(
timeout=timeout,
tracker=tracker,
variables=input_data,
operation_name="GetDiffTree",
)

node_diffs: list[NodeDiff] = []
Expand Down Expand Up @@ -2821,6 +2852,7 @@ def get_diff_tree(
timeout=timeout,
tracker=tracker,
variables=input_data,
operation_name=query.name,
)

diff_tree = response["DiffTree"]
Expand Down
13 changes: 9 additions & 4 deletions infrahub_sdk/graphql/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,22 @@ class MultipartBuilder:
"""

@staticmethod
def build_operations(query: str, variables: dict[str, Any]) -> str:
def build_operations(query: str, variables: dict[str, Any], operation_name: str | None = None) -> str:
"""Build the operations JSON string.

Args:
query: The GraphQL query string.
variables: The variables dict (file variable should be null).
operation_name: Optional GraphQL operation name, included as `operationName` when set.

Returns:
JSON string containing the query and variables.
JSON string containing the query, variables, and optional operation name.

"""
return ujson.dumps({"query": query, "variables": variables})
operations: dict[str, Any] = {"query": query, "variables": variables}
if operation_name:
operations["operationName"] = operation_name
return ujson.dumps(operations)

@staticmethod
def build_file_map(file_key: str = "0", variable_path: str = "variables.file") -> str:
Expand All @@ -61,6 +65,7 @@ def build_payload(
variables: dict[str, Any],
file_content: BinaryIO | None = None,
file_name: str = "upload",
operation_name: str | None = None,
) -> dict[str, Any]:
"""Build the complete multipart form data payload.

Expand Down Expand Up @@ -91,7 +96,7 @@ def build_payload(
# Ensure file variable is null (spec requirement)
variables = {**variables, "file": None}

operations = MultipartBuilder.build_operations(query=query, variables=variables)
operations = MultipartBuilder.build_operations(query=query, variables=variables, operation_name=operation_name)
file_map = MultipartBuilder.build_file_map()

files: dict[str, Any] = {"operations": (None, operations), "map": (None, file_map)}
Expand Down
Loading