Skip to content
Closed
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
2 changes: 2 additions & 0 deletions cluster-info.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `78be9c1` Merge pull request #21285 from gooddata/dho/cq-104-arrow-api
- `81eb97a` Merge pull request #21382 from gooddata/dho/cq-104-arrow-api-2
24 changes: 24 additions & 0 deletions cluster.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"id": "C010",
"title": "Add binary execution result endpoint for Apache Arrow format",
"services": [
"gooddata-afm-client"
],
"commits": [
{
"sha": "78be9c10f3b9ec597f6282014708415e3527a3df",
"author": "Dan Homola",
"author_email": "dan.homola@gooddata.com",
"message": "Merge pull request #21285 from gooddata/dho/cq-104-arrow-api"
},
{
"sha": "81eb97a771c9c18c47c508df3970ccab19e28108",
"author": "Dan Homola",
"author_email": "dan.homola@gooddata.com",
"message": "Merge pull request #21382 from gooddata/dho/cq-104-arrow-api-2"
}
],
"diff": "--- a/gooddata-afm-client.json\n+++ b/gooddata-afm-client.json\n+ \"/api/v1/actions/workspaces/{workspaceId}/execution/afm/execute/result/{resultId}/binary\": {\n+ \"get\": {\n+ \"description\": \"(BETA) Gets a single execution result as an Apache Arrow IPC File or Stream format.\",\n+ \"operationId\": \"retrieveResultBinary\",\n+ \"responses\": {\n+ \"200\": {\n+ \"content\": {\n+ \"application/vnd.apache.arrow.file\": { \"schema\": { \"format\": \"binary\", \"type\": \"string\" } },\n+ \"application/vnd.apache.arrow.stream\": { \"schema\": { \"format\": \"binary\", \"type\": \"string\" } }\n+ },\n+ \"description\": \"Execution result was found and returned.\"\n+ }\n+ },\n+ \"summary\": \"(BETA) Get a single execution result in Apache Arrow File or Stream format\",\n+ \"x-gdc-security-info\": { \"permissions\": [\"VIEW\"] }\n+ }\n+ }",
"jira_tickets": [],
"sdk_impact": "new_feature"
}
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,30 @@ def read_result(
)
return ExecutionResult(execution_result)

def read_result_binary(
self,
timeout: Union[int, float, tuple] | None = None,
) -> bytes:
"""
Reads the execution result in Apache Arrow binary format (IPC File or Stream).

Args:
timeout: request timeout in seconds. If a tuple is provided, it is used as
(connection timeout, read timeout).

Returns:
bytes: The execution result serialized as Apache Arrow IPC binary data.
"""
response = self._actions_api.retrieve_result_binary(
workspace_id=self._workspace_id,
result_id=self.result_id,
_check_return_type=False,
_preload_content=False,
_request_timeout=timeout,
**({"x_gdc_cancel_token": self.cancel_token} if self.cancel_token else {}),
)
return response.data

def cancel(self) -> None:
"""
Cancels the execution backing this execution result.
Expand Down Expand Up @@ -464,6 +488,22 @@ def read_result(
) -> ExecutionResult:
return self.bare_exec_response.read_result(limit, offset, timeout)

def read_result_binary(
self,
timeout: Union[int, float, tuple] | None = None,
) -> bytes:
"""
Reads the execution result in Apache Arrow binary format (IPC File or Stream).

Args:
timeout: request timeout in seconds. If a tuple is provided, it is used as
(connection timeout, read timeout).

Returns:
bytes: The execution result serialized as Apache Arrow IPC binary data.
"""
return self.bare_exec_response.read_result_binary(timeout)

def cancel(self) -> None:
"""
Cancels the execution.
Expand Down
27 changes: 27 additions & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/compute/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,33 @@ def for_exec_def(
else None,
)

def retrieve_result_binary(
self,
workspace_id: str,
result_id: str,
timeout: Union[int, float, tuple] | None = None,
) -> bytes:
"""
Gets a single execution result in Apache Arrow binary format (IPC File or Stream).

Args:
workspace_id (str): workspace identifier
result_id (str): execution result ID
timeout: request timeout in seconds. If a tuple is provided, it is used as
(connection timeout, read timeout).

Returns:
bytes: The execution result serialized as Apache Arrow IPC binary data.
"""
response = self._actions_api.retrieve_result_binary(
workspace_id=workspace_id,
result_id=result_id,
_check_return_type=False,
_preload_content=False,
_request_timeout=timeout,
)
return response.data

def retrieve_result_cache_metadata(self, workspace_id: str, result_id: str) -> ResultCacheMetadata:
"""
Gets execution result's metadata from GoodData.CN workspace for given execution result ID.
Expand Down
151 changes: 151 additions & 0 deletions packages/gooddata-sdk/tests/compute/test_retrieve_result_binary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# (C) 2024 GoodData Corporation
from __future__ import annotations

from unittest.mock import MagicMock

from gooddata_sdk.compute.model.execution import BareExecutionResponse
from gooddata_sdk.compute.service import ComputeService


def _make_mock_execution_response(result_id: str = "test-result-id") -> MagicMock:
"""Create a mock AfmExecutionResponse with a given result_id."""
mock_response = MagicMock()
mock_response.__getitem__ = MagicMock(
side_effect=lambda key: {
"execution_response": MagicMock(
__getitem__=MagicMock(
side_effect=lambda k: {
"links": {"executionResult": result_id},
"dimensions": [],
}[k]
)
)
}[key]
)
return mock_response


def test_bare_execution_response_read_result_binary():
"""Test that BareExecutionResponse.read_result_binary calls retrieve_result_binary correctly."""
fake_binary_data = b"\x00\x01\x02\x03arrow_data"
mock_http_response = MagicMock()
mock_http_response.data = fake_binary_data

mock_actions_api = MagicMock()
mock_actions_api.retrieve_result_binary.return_value = mock_http_response

mock_api_client = MagicMock()
mock_api_client.actions_api = mock_actions_api

mock_exec_response = _make_mock_execution_response("result-123")

bare = BareExecutionResponse(
api_client=mock_api_client,
workspace_id="my-workspace",
execution_response=mock_exec_response,
cancel_token=None,
)

result = bare.read_result_binary()

assert result == fake_binary_data
mock_actions_api.retrieve_result_binary.assert_called_once_with(
workspace_id="my-workspace",
result_id="result-123",
_check_return_type=False,
_preload_content=False,
_request_timeout=None,
)


def test_bare_execution_response_read_result_binary_with_cancel_token():
"""Test that BareExecutionResponse.read_result_binary forwards cancel token."""
fake_binary_data = b"arrow_stream_data"
mock_http_response = MagicMock()
mock_http_response.data = fake_binary_data

mock_actions_api = MagicMock()
mock_actions_api.retrieve_result_binary.return_value = mock_http_response

mock_api_client = MagicMock()
mock_api_client.actions_api = mock_actions_api

mock_exec_response = _make_mock_execution_response("result-456")

bare = BareExecutionResponse(
api_client=mock_api_client,
workspace_id="my-workspace",
execution_response=mock_exec_response,
cancel_token="cancel-token-xyz",
)

result = bare.read_result_binary(timeout=30)

assert result == fake_binary_data
mock_actions_api.retrieve_result_binary.assert_called_once_with(
workspace_id="my-workspace",
result_id="result-456",
_check_return_type=False,
_preload_content=False,
_request_timeout=30,
x_gdc_cancel_token="cancel-token-xyz",
)


def test_compute_service_retrieve_result_binary():
"""Test that ComputeService.retrieve_result_binary calls the API correctly."""
fake_binary_data = b"apache_arrow_file_data"
mock_http_response = MagicMock()
mock_http_response.data = fake_binary_data

mock_actions_api = MagicMock()
mock_actions_api.retrieve_result_binary.return_value = mock_http_response

mock_api_client = MagicMock()
mock_api_client.actions_api = mock_actions_api

service = ComputeService(api_client=mock_api_client)

result = service.retrieve_result_binary(
workspace_id="workspace-1",
result_id="result-789",
)

assert result == fake_binary_data
mock_actions_api.retrieve_result_binary.assert_called_once_with(
workspace_id="workspace-1",
result_id="result-789",
_check_return_type=False,
_preload_content=False,
_request_timeout=None,
)


def test_compute_service_retrieve_result_binary_with_timeout():
"""Test that ComputeService.retrieve_result_binary forwards timeout correctly."""
fake_binary_data = b"apache_arrow_stream_data"
mock_http_response = MagicMock()
mock_http_response.data = fake_binary_data

mock_actions_api = MagicMock()
mock_actions_api.retrieve_result_binary.return_value = mock_http_response

mock_api_client = MagicMock()
mock_api_client.actions_api = mock_actions_api

service = ComputeService(api_client=mock_api_client)

result = service.retrieve_result_binary(
workspace_id="workspace-2",
result_id="result-abc",
timeout=(5, 60),
)

assert result == fake_binary_data
mock_actions_api.retrieve_result_binary.assert_called_once_with(
workspace_id="workspace-2",
result_id="result-abc",
_check_return_type=False,
_preload_content=False,
_request_timeout=(5, 60),
)
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ dependencies = [
"gooddata-api-client",
"gooddata-pipelines",
"tests-support",
"pytest>=8.3.5",
"vcrpy>=8.0.0",
]

[tool.uv]
Expand Down
12 changes: 12 additions & 0 deletions result.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"status": "implemented",
"cluster_id": "C010",
"summary": "Added SDK wrapper methods for the new binary execution result endpoint (GET /api/v1/actions/workspaces/{workspaceId}/execution/afm/execute/result/{resultId}/binary). \n\nThree new methods were added:\n1. `BareExecutionResponse.read_result_binary(timeout)` in `execution.py` — calls `retrieve_result_binary` with `_preload_content=False` and returns raw `bytes` (Apache Arrow IPC binary data). Forwards cancel token when present.\n2. `Execution.read_result_binary(timeout)` in `execution.py` — delegates to `bare_exec_response.read_result_binary()` for consistent API on the primary user-facing class.\n3. `ComputeService.retrieve_result_binary(workspace_id, result_id, timeout)` in `service.py` — standalone method for retrieving binary results directly when only workspace_id and result_id are available.\n\nAll methods use `_check_return_type=False` and `_preload_content=False` following SDK conventions, and return `bytes`. Four unit tests were added to verify parameter forwarding including cancel token and timeout handling.",
"files_changed": [
"packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py",
"packages/gooddata-sdk/src/gooddata_sdk/compute/service.py",
"packages/gooddata-sdk/tests/compute/test_retrieve_result_binary.py"
],
"reason": "",
"cost_usd": 1.1491305
}
4 changes: 4 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading