Skip to content

Commit 685d4e0

Browse files
committed
feat(gooddata-sdk): [AUTO] Add ExecutionResultLimitBreak schema for partial results
1 parent 38b0798 commit 685d4e0

5 files changed

Lines changed: 172 additions & 6 deletions

File tree

packages/gooddata-sdk/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ test = [
7676
]
7777

7878
[tool.ty.analysis]
79-
allowed-unresolved-imports = ["gooddata_api_client.**"]
79+
allowed-unresolved-imports = ["gooddata_api_client.**", "pyarrow.**"]
8080

8181
[tool.hatch.build.targets.wheel]
8282
packages = ["src/gooddata_sdk"]

packages/gooddata-sdk/src/gooddata_sdk/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@
290290
ExecutionDefinition,
291291
ExecutionResponse,
292292
ExecutionResult,
293+
ExecutionResultLimitBreak,
293294
ResultCacheMetadata,
294295
ResultSizeBytesLimitExceeded,
295296
ResultSizeDimensions,

packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import pyarrow as _pyarrow
2020
from pyarrow import ipc as _ipc
2121
except ImportError:
22-
_pyarrow = None # type: ignore
23-
_ipc = None # type: ignore
22+
_pyarrow = None
23+
_ipc = None
2424

2525
from gooddata_sdk.client import GoodDataApiClient
2626
from gooddata_sdk.compute.model.attribute import Attribute
@@ -30,6 +30,29 @@
3030
logger = logging.getLogger(__name__)
3131

3232

33+
@define
34+
class ExecutionResultLimitBreak:
35+
"""Describes a limit that was broken, resulting in partial data being returned."""
36+
37+
limit: int
38+
"""The configured threshold value."""
39+
40+
limit_type: str
41+
"""Type of the limit that was broken, e.g. 'rowCount'."""
42+
43+
value: int | None = None
44+
"""The actual value that triggered the limit; None when it cannot be determined exactly."""
45+
46+
@classmethod
47+
def from_api(cls, data: dict[str, Any]) -> ExecutionResultLimitBreak:
48+
raw_value = data.get("value")
49+
return cls(
50+
limit=int(data["limit"]),
51+
limit_type=str(data["limitType"]),
52+
value=int(raw_value) if raw_value is not None else None,
53+
)
54+
55+
3356
@define
3457
class TotalDimension:
3558
idx: int
@@ -271,6 +294,18 @@ def paging_offset(self) -> list[int]:
271294
def metadata(self) -> models.ExecutionResultMetadata:
272295
return self._metadata
273296

297+
@property
298+
def limit_breaks(self) -> list[ExecutionResultLimitBreak]:
299+
"""Returns limits that were broken during result computation.
300+
301+
When no limits were broken (result is complete), returns an empty list.
302+
The ``limitBreaks`` field is absent from the API response in that case.
303+
"""
304+
raw = self._metadata.get("limitBreaks")
305+
if not raw:
306+
return []
307+
return [ExecutionResultLimitBreak.from_api(item) for item in raw]
308+
274309
def is_complete(self, dim: int = 0) -> bool:
275310
return self.paging_offset[dim] + self.paging_count[dim] >= self.paging_total[dim]
276311

packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def __init__(
326326
self._from_shift = from_shift
327327
self._to_shift = to_shift
328328
self._bounded_filter = bounded_filter
329-
self._empty_value_handling = empty_value_handling
329+
self._empty_value_handling: EmptyValueHandling | None = empty_value_handling
330330

331331
@property
332332
def dataset(self) -> ObjId:
@@ -435,7 +435,7 @@ def __init__(
435435

436436
self._dataset = dataset
437437
self._granularity = granularity
438-
self._empty_value_handling = empty_value_handling
438+
self._empty_value_handling: EmptyValueHandling | None = empty_value_handling
439439

440440
@property
441441
def dataset(self) -> ObjId:
@@ -490,7 +490,7 @@ def __init__(
490490
self._dataset = dataset
491491
self._from_date = from_date
492492
self._to_date = to_date
493-
self._empty_value_handling = empty_value_handling
493+
self._empty_value_handling: EmptyValueHandling | None = empty_value_handling
494494

495495
@property
496496
def dataset(self) -> ObjId:
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# (C) 2026 GoodData Corporation
2+
from __future__ import annotations
3+
4+
from pathlib import Path
5+
6+
import pytest
7+
from gooddata_sdk import ExecutionResultLimitBreak, GoodDataSdk, ObjId, SimpleMetric, TableDimension
8+
from gooddata_sdk.compute.model.execution import ExecutionDefinition, ExecutionResult
9+
from tests_support.vcrpy_utils import get_vcr
10+
11+
gd_vcr = get_vcr()
12+
13+
_current_dir = Path(__file__).parent.absolute()
14+
_fixtures_dir = _current_dir / "fixtures"
15+
16+
17+
# ---------------------------------------------------------------------------
18+
# Unit tests — ExecutionResultLimitBreak.from_api
19+
# ---------------------------------------------------------------------------
20+
21+
22+
@pytest.mark.parametrize(
23+
"scenario, data, expected_limit, expected_limit_type, expected_value",
24+
[
25+
(
26+
"all_fields",
27+
{"limit": 1000, "limitType": "rowCount", "value": 1500},
28+
1000,
29+
"rowCount",
30+
1500,
31+
),
32+
(
33+
"absent_value",
34+
{"limit": 500, "limitType": "cellCount"},
35+
500,
36+
"cellCount",
37+
None,
38+
),
39+
(
40+
"null_value",
41+
{"limit": 200, "limitType": "rowCount", "value": None},
42+
200,
43+
"rowCount",
44+
None,
45+
),
46+
],
47+
)
48+
def test_execution_result_limit_break_from_api(
49+
scenario: str,
50+
data: dict,
51+
expected_limit: int,
52+
expected_limit_type: str,
53+
expected_value: int | None,
54+
) -> None:
55+
lb = ExecutionResultLimitBreak.from_api(data)
56+
assert lb.limit == expected_limit
57+
assert lb.limit_type == expected_limit_type
58+
assert lb.value == expected_value
59+
60+
61+
# ---------------------------------------------------------------------------
62+
# Unit tests — ExecutionResult.limit_breaks property
63+
# ---------------------------------------------------------------------------
64+
65+
66+
def _make_execution_result(metadata: dict) -> ExecutionResult:
67+
"""Build an ExecutionResult from a plain-dict mock result."""
68+
result = {
69+
"data": [],
70+
"dimension_headers": [],
71+
"grand_totals": [],
72+
"paging": {"total": [0], "count": [0], "offset": [0]},
73+
"metadata": metadata,
74+
}
75+
return ExecutionResult(result)
76+
77+
78+
def test_limit_breaks_absent_returns_empty_list() -> None:
79+
"""When limitBreaks is not in the metadata, limit_breaks returns []."""
80+
er = _make_execution_result({"dataSourceMessages": []})
81+
assert er.limit_breaks == []
82+
83+
84+
def test_limit_breaks_present_returns_parsed_objects() -> None:
85+
"""When limitBreaks is present, limit_breaks returns a list of ExecutionResultLimitBreak."""
86+
metadata = {
87+
"dataSourceMessages": [],
88+
"limitBreaks": [
89+
{"limit": 1000, "limitType": "rowCount", "value": 1234},
90+
{"limit": 500, "limitType": "cellCount"},
91+
],
92+
}
93+
er = _make_execution_result(metadata)
94+
breaks = er.limit_breaks
95+
assert len(breaks) == 2
96+
97+
assert breaks[0].limit == 1000
98+
assert breaks[0].limit_type == "rowCount"
99+
assert breaks[0].value == 1234
100+
101+
assert breaks[1].limit == 500
102+
assert breaks[1].limit_type == "cellCount"
103+
assert breaks[1].value is None
104+
105+
106+
# ---------------------------------------------------------------------------
107+
# Integration test — limit_breaks accessible after real execution
108+
# ---------------------------------------------------------------------------
109+
110+
111+
@gd_vcr.use_cassette(str(_fixtures_dir / "test_execution_limit_breaks.yaml"))
112+
def test_execution_limit_breaks_integration(test_config):
113+
"""Integration test: execute a computation and verify limit_breaks is accessible.
114+
115+
In normal operation (no limits exceeded) limit_breaks returns an empty list.
116+
This test verifies the SDK correctly handles the absent limitBreaks field.
117+
"""
118+
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
119+
120+
exec_def = ExecutionDefinition(
121+
attributes=None,
122+
metrics=[SimpleMetric(local_id="m1", item=ObjId(type="metric", id="order_amount"))],
123+
filters=None,
124+
dimensions=[TableDimension(item_ids=["measureGroup"])],
125+
)
126+
127+
execution = sdk.compute.for_exec_def(test_config["workspace"], exec_def)
128+
result = execution.read_result(limit=1)
129+
130+
assert isinstance(result.limit_breaks, list)

0 commit comments

Comments
 (0)