From 685d4e0120964c5dd367055f7c990e9254957fe2 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 18 May 2026 09:28:10 +0000 Subject: [PATCH 1/3] feat(gooddata-sdk): [AUTO] Add ExecutionResultLimitBreak schema for partial results --- packages/gooddata-sdk/pyproject.toml | 2 +- .../gooddata-sdk/src/gooddata_sdk/__init__.py | 1 + .../gooddata_sdk/compute/model/execution.py | 39 +++++- .../src/gooddata_sdk/compute/model/filter.py | 6 +- .../compute/test_execution_limit_breaks.py | 130 ++++++++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 packages/gooddata-sdk/tests/compute/test_execution_limit_breaks.py diff --git a/packages/gooddata-sdk/pyproject.toml b/packages/gooddata-sdk/pyproject.toml index 7a218dbe6..950ac70da 100644 --- a/packages/gooddata-sdk/pyproject.toml +++ b/packages/gooddata-sdk/pyproject.toml @@ -76,7 +76,7 @@ test = [ ] [tool.ty.analysis] -allowed-unresolved-imports = ["gooddata_api_client.**"] +allowed-unresolved-imports = ["gooddata_api_client.**", "pyarrow.**"] [tool.hatch.build.targets.wheel] packages = ["src/gooddata_sdk"] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 91f87c918..9eb026963 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -290,6 +290,7 @@ ExecutionDefinition, ExecutionResponse, ExecutionResult, + ExecutionResultLimitBreak, ResultCacheMetadata, ResultSizeBytesLimitExceeded, ResultSizeDimensions, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py index df5284ec6..d916e2a7b 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py @@ -19,8 +19,8 @@ import pyarrow as _pyarrow from pyarrow import ipc as _ipc except ImportError: - _pyarrow = None # type: ignore - _ipc = None # type: ignore + _pyarrow = None + _ipc = None from gooddata_sdk.client import GoodDataApiClient from gooddata_sdk.compute.model.attribute import Attribute @@ -30,6 +30,29 @@ logger = logging.getLogger(__name__) +@define +class ExecutionResultLimitBreak: + """Describes a limit that was broken, resulting in partial data being returned.""" + + limit: int + """The configured threshold value.""" + + limit_type: str + """Type of the limit that was broken, e.g. 'rowCount'.""" + + value: int | None = None + """The actual value that triggered the limit; None when it cannot be determined exactly.""" + + @classmethod + def from_api(cls, data: dict[str, Any]) -> ExecutionResultLimitBreak: + raw_value = data.get("value") + return cls( + limit=int(data["limit"]), + limit_type=str(data["limitType"]), + value=int(raw_value) if raw_value is not None else None, + ) + + @define class TotalDimension: idx: int @@ -271,6 +294,18 @@ def paging_offset(self) -> list[int]: def metadata(self) -> models.ExecutionResultMetadata: return self._metadata + @property + def limit_breaks(self) -> list[ExecutionResultLimitBreak]: + """Returns limits that were broken during result computation. + + When no limits were broken (result is complete), returns an empty list. + The ``limitBreaks`` field is absent from the API response in that case. + """ + raw = self._metadata.get("limitBreaks") + if not raw: + return [] + return [ExecutionResultLimitBreak.from_api(item) for item in raw] + def is_complete(self, dim: int = 0) -> bool: return self.paging_offset[dim] + self.paging_count[dim] >= self.paging_total[dim] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py index 94171f156..bbac98e7b 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py @@ -326,7 +326,7 @@ def __init__( self._from_shift = from_shift self._to_shift = to_shift self._bounded_filter = bounded_filter - self._empty_value_handling = empty_value_handling + self._empty_value_handling: EmptyValueHandling | None = empty_value_handling @property def dataset(self) -> ObjId: @@ -435,7 +435,7 @@ def __init__( self._dataset = dataset self._granularity = granularity - self._empty_value_handling = empty_value_handling + self._empty_value_handling: EmptyValueHandling | None = empty_value_handling @property def dataset(self) -> ObjId: @@ -490,7 +490,7 @@ def __init__( self._dataset = dataset self._from_date = from_date self._to_date = to_date - self._empty_value_handling = empty_value_handling + self._empty_value_handling: EmptyValueHandling | None = empty_value_handling @property def dataset(self) -> ObjId: diff --git a/packages/gooddata-sdk/tests/compute/test_execution_limit_breaks.py b/packages/gooddata-sdk/tests/compute/test_execution_limit_breaks.py new file mode 100644 index 000000000..6c137cd2a --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/test_execution_limit_breaks.py @@ -0,0 +1,130 @@ +# (C) 2026 GoodData Corporation +from __future__ import annotations + +from pathlib import Path + +import pytest +from gooddata_sdk import ExecutionResultLimitBreak, GoodDataSdk, ObjId, SimpleMetric, TableDimension +from gooddata_sdk.compute.model.execution import ExecutionDefinition, ExecutionResult +from tests_support.vcrpy_utils import get_vcr + +gd_vcr = get_vcr() + +_current_dir = Path(__file__).parent.absolute() +_fixtures_dir = _current_dir / "fixtures" + + +# --------------------------------------------------------------------------- +# Unit tests — ExecutionResultLimitBreak.from_api +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "scenario, data, expected_limit, expected_limit_type, expected_value", + [ + ( + "all_fields", + {"limit": 1000, "limitType": "rowCount", "value": 1500}, + 1000, + "rowCount", + 1500, + ), + ( + "absent_value", + {"limit": 500, "limitType": "cellCount"}, + 500, + "cellCount", + None, + ), + ( + "null_value", + {"limit": 200, "limitType": "rowCount", "value": None}, + 200, + "rowCount", + None, + ), + ], +) +def test_execution_result_limit_break_from_api( + scenario: str, + data: dict, + expected_limit: int, + expected_limit_type: str, + expected_value: int | None, +) -> None: + lb = ExecutionResultLimitBreak.from_api(data) + assert lb.limit == expected_limit + assert lb.limit_type == expected_limit_type + assert lb.value == expected_value + + +# --------------------------------------------------------------------------- +# Unit tests — ExecutionResult.limit_breaks property +# --------------------------------------------------------------------------- + + +def _make_execution_result(metadata: dict) -> ExecutionResult: + """Build an ExecutionResult from a plain-dict mock result.""" + result = { + "data": [], + "dimension_headers": [], + "grand_totals": [], + "paging": {"total": [0], "count": [0], "offset": [0]}, + "metadata": metadata, + } + return ExecutionResult(result) + + +def test_limit_breaks_absent_returns_empty_list() -> None: + """When limitBreaks is not in the metadata, limit_breaks returns [].""" + er = _make_execution_result({"dataSourceMessages": []}) + assert er.limit_breaks == [] + + +def test_limit_breaks_present_returns_parsed_objects() -> None: + """When limitBreaks is present, limit_breaks returns a list of ExecutionResultLimitBreak.""" + metadata = { + "dataSourceMessages": [], + "limitBreaks": [ + {"limit": 1000, "limitType": "rowCount", "value": 1234}, + {"limit": 500, "limitType": "cellCount"}, + ], + } + er = _make_execution_result(metadata) + breaks = er.limit_breaks + assert len(breaks) == 2 + + assert breaks[0].limit == 1000 + assert breaks[0].limit_type == "rowCount" + assert breaks[0].value == 1234 + + assert breaks[1].limit == 500 + assert breaks[1].limit_type == "cellCount" + assert breaks[1].value is None + + +# --------------------------------------------------------------------------- +# Integration test — limit_breaks accessible after real execution +# --------------------------------------------------------------------------- + + +@gd_vcr.use_cassette(str(_fixtures_dir / "test_execution_limit_breaks.yaml")) +def test_execution_limit_breaks_integration(test_config): + """Integration test: execute a computation and verify limit_breaks is accessible. + + In normal operation (no limits exceeded) limit_breaks returns an empty list. + This test verifies the SDK correctly handles the absent limitBreaks field. + """ + sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"]) + + exec_def = ExecutionDefinition( + attributes=None, + metrics=[SimpleMetric(local_id="m1", item=ObjId(type="metric", id="order_amount"))], + filters=None, + dimensions=[TableDimension(item_ids=["measureGroup"])], + ) + + execution = sdk.compute.for_exec_def(test_config["workspace"], exec_def) + result = execution.read_result(limit=1) + + assert isinstance(result.limit_breaks, list) From eaaa9b729f514acd27912cccf0896a0d186051cf Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 18 May 2026 10:02:41 +0000 Subject: [PATCH 2/3] fix(gooddata-sdk): [AUTO] fix-agent attempt 1 --- .../fixtures/test_execution_limit_breaks.yaml | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml diff --git a/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml b/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml new file mode 100644 index 000000000..2b7da795b --- /dev/null +++ b/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml @@ -0,0 +1,104 @@ +interactions: + - request: + body: + execution: + attributes: [] + filters: [] + measures: + - definition: + measure: + computeRatio: false + filters: [] + item: + identifier: + id: order_amount + type: metric + localIdentifier: m1 + resultSpec: + dimensions: + - itemIdentifiers: + - measureGroup + localIdentifier: dim_0 + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + Content-Type: + - application/json + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: POST + uri: http://localhost:3000/api/v1/actions/workspaces/demo/execution/afm/execute + response: + body: + string: + executionResponse: + dimensions: + - headers: + - measureGroupHeaders: + - format: $#,##0 + localIdentifier: m1 + name: Order Amount + localIdentifier: dim_0 + links: + executionResult: EXECUTION_NORMALIZED_1 + headers: + Content-Type: + - application/json + DATE: + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-CANCEL-TOKEN: + - PLACEHOLDER + X-GDC-TRACE-ID: + - PLACEHOLDER + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/actions/workspaces/demo/execution/afm/execute/result/EXECUTION_NORMALIZED_1?offset=0&limit=1 + response: + body: + string: + detail: An error has occurred while calculating the result + reason: Cannot reach the URL + resultId: d7b1e3aaa86bd0f1a31337e6d88c5449e3326183 + status: 400 + title: Bad Request + traceId: NORMALIZED_TRACE_ID_000000000000 + headers: + Content-Type: + - application/problem+json + DATE: + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: + - PLACEHOLDER + status: + code: 400 + message: Bad Request +version: 1 From a746b7a01c8c2dfe8db30c705f8d8bdb59621d53 Mon Sep 17 00:00:00 2001 From: yenkins-admin <5391010+yenkins-admin@users.noreply.github.com> Date: Mon, 18 May 2026 10:03:44 +0000 Subject: [PATCH 3/3] fix(gooddata-sdk): [AUTO] fix-agent attempt 2 --- .../fixtures/test_execution_limit_breaks.yaml | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml b/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml index 2b7da795b..233af0de9 100644 --- a/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml +++ b/packages/gooddata-sdk/tests/compute/fixtures/test_execution_limit_breaks.yaml @@ -101,4 +101,106 @@ interactions: status: code: 400 message: Bad Request + - request: + body: + execution: + attributes: [] + filters: [] + measures: + - definition: + measure: + computeRatio: false + filters: [] + item: + identifier: + id: order_amount + type: metric + localIdentifier: m1 + resultSpec: + dimensions: + - itemIdentifiers: + - measureGroup + localIdentifier: dim_0 + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + Content-Type: + - application/json + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: POST + uri: http://localhost:3000/api/v1/actions/workspaces/demo/execution/afm/execute + response: + body: + string: + executionResponse: + dimensions: + - headers: + - measureGroupHeaders: + - format: $#,##0 + localIdentifier: m1 + name: Order Amount + localIdentifier: dim_0 + links: + executionResult: EXECUTION_NORMALIZED_1 + headers: + Content-Type: + - application/json + DATE: + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-CANCEL-TOKEN: + - PLACEHOLDER + X-GDC-TRACE-ID: + - PLACEHOLDER + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - br, gzip, deflate + X-GDC-VALIDATE-RELATIONS: + - 'true' + X-Requested-With: + - XMLHttpRequest + method: GET + uri: http://localhost:3000/api/v1/actions/workspaces/demo/execution/afm/execute/result/EXECUTION_NORMALIZED_1?offset=0&limit=1 + response: + body: + string: + detail: An error has occurred while calculating the result + reason: Cannot reach the URL + resultId: 67da26dd311be7504d1af3a312ea1d382cfdf313 + status: 400 + title: Bad Request + traceId: NORMALIZED_TRACE_ID_000000000000 + headers: + Content-Type: + - application/problem+json + DATE: + - PLACEHOLDER + Expires: + - '0' + Pragma: + - no-cache + X-Content-Type-Options: + - nosniff + X-GDC-TRACE-ID: + - PLACEHOLDER + status: + code: 400 + message: Bad Request version: 1