Skip to content

Commit b11b410

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

5 files changed

Lines changed: 123 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", "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
@@ -219,6 +219,29 @@ def as_api_model(self) -> models.AfmExecution:
219219
ResultSizeDimensions = tuple[int | None, ...]
220220

221221

222+
@define
223+
class ExecutionResultLimitBreak:
224+
"""Describes a limit that was broken, resulting in partial data being returned."""
225+
226+
limit: int
227+
"""The configured threshold value."""
228+
229+
limit_type: str
230+
"""Type of the limit that was broken, e.g. 'rowCount'."""
231+
232+
value: int | None = None
233+
"""The actual value that triggered the limit; None when it cannot be determined exactly."""
234+
235+
@classmethod
236+
def from_api(cls, entity: dict[str, Any]) -> ExecutionResultLimitBreak:
237+
raw_value = entity.get("value")
238+
return cls(
239+
limit=entity["limit"],
240+
limit_type=entity["limitType"],
241+
value=None if raw_value is None else int(raw_value),
242+
)
243+
244+
222245
class ResultSizeDimensionsLimitsExceeded(Exception):
223246
def __init__(
224247
self,
@@ -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+
Returns an empty list when the result is complete (no limits were broken).
302+
"""
303+
metadata: Any = self._metadata
304+
raw = 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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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
8+
from gooddata_sdk.compute.model.attribute import Attribute
9+
from gooddata_sdk.compute.model.execution import ExecutionDefinition, ExecutionResult, TableDimension
10+
from tests_support.vcrpy_utils import get_vcr
11+
12+
gd_vcr = get_vcr()
13+
_fixtures_dir = Path(__file__).parent / "fixtures"
14+
15+
16+
@pytest.mark.parametrize(
17+
"scenario,data,expected_limit,expected_limit_type,expected_value",
18+
[
19+
("full", {"limit": 1000, "limitType": "rowCount", "value": 1500}, 1000, "rowCount", 1500),
20+
("no_value", {"limit": 500, "limitType": "rowCount"}, 500, "rowCount", None),
21+
("null_value", {"limit": 200, "limitType": "cellCount", "value": None}, 200, "cellCount", None),
22+
],
23+
)
24+
def test_limit_break_from_api(scenario, data, expected_limit, expected_limit_type, expected_value):
25+
"""ExecutionResultLimitBreak.from_api correctly maps camelCase keys and handles absent value."""
26+
result = ExecutionResultLimitBreak.from_api(data)
27+
assert result.limit == expected_limit
28+
assert result.limit_type == expected_limit_type
29+
assert result.value == expected_value
30+
31+
32+
def _make_execution_result(limit_breaks=None):
33+
"""Return a minimal ExecutionResult dict for unit testing."""
34+
metadata = {"dataSourceMessages": []}
35+
if limit_breaks is not None:
36+
metadata["limitBreaks"] = limit_breaks
37+
return {
38+
"data": [],
39+
"dimension_headers": [],
40+
"grand_totals": [],
41+
"metadata": metadata,
42+
"paging": {"count": [0], "offset": [0], "total": [0]},
43+
}
44+
45+
46+
def test_execution_result_limit_breaks_absent():
47+
"""When limitBreaks is absent from metadata, limit_breaks returns empty list."""
48+
raw = _make_execution_result()
49+
result = ExecutionResult(raw) # type: ignore[arg-type]
50+
assert result.limit_breaks == []
51+
52+
53+
def test_execution_result_limit_breaks_present():
54+
"""When limitBreaks is present in metadata, limit_breaks returns correctly parsed list."""
55+
raw = _make_execution_result(limit_breaks=[{"limit": 1000, "limitType": "rowCount", "value": 1200}])
56+
result = ExecutionResult(raw) # type: ignore[arg-type]
57+
breaks = result.limit_breaks
58+
assert len(breaks) == 1
59+
assert breaks[0].limit == 1000
60+
assert breaks[0].limit_type == "rowCount"
61+
assert breaks[0].value == 1200
62+
63+
64+
@gd_vcr.use_cassette(str(_fixtures_dir / "test_execution_result_limit_breaks.yaml"))
65+
def test_execution_result_limit_breaks_integration(test_config):
66+
"""Integration test: limit_breaks property is accessible from a live ExecutionResult."""
67+
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
68+
workspace_id = test_config["workspace"]
69+
70+
exec_def = ExecutionDefinition(
71+
attributes=[Attribute(local_id="a1", label="campaign_channel_id")],
72+
metrics=[],
73+
filters=[],
74+
dimensions=[TableDimension(item_ids=["a1"])],
75+
)
76+
77+
execution = sdk.compute.for_exec_def(workspace_id, exec_def)
78+
result = execution.read_result(limit=10)
79+
80+
# limit_breaks returns a list (empty when result is complete, no limits broken)
81+
assert isinstance(result.limit_breaks, list)

0 commit comments

Comments
 (0)