Skip to content

Commit 7fe0dbb

Browse files
committed
feat(gooddata-sdk): [AUTO] Add ExecutionResultLimitBreak schema for partial data detection
1 parent e1d6ad4 commit 7fe0dbb

4 files changed

Lines changed: 187 additions & 0 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@
316316
RankingFilter,
317317
RelativeDateFilter,
318318
)
319+
from gooddata_sdk.compute.model.limit_break import ExecutionResultLimitBreak
319320
from gooddata_sdk.compute.model.metric import (
320321
ArithmeticMetric,
321322
InlineMetric,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# (C) 2024 GoodData Corporation
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
import attrs
7+
8+
9+
@attrs.define(kw_only=True)
10+
class ExecutionResultLimitBreak:
11+
"""Describes a limit that was broken, resulting in partial data being returned.
12+
13+
When the server truncates execution results because a configured threshold was
14+
exceeded, it returns one or more ``ExecutionResultLimitBreak`` objects inside
15+
``ExecutionResult.metadata["limitBreaks"]``. Use :meth:`from_dict` to convert
16+
each raw dict entry into a typed instance.
17+
18+
Example::
19+
20+
result = execution.read_result(limit=10000)
21+
raw_breaks = result.metadata.get("limitBreaks") or []
22+
limit_breaks = [ExecutionResultLimitBreak.from_dict(lb) for lb in raw_breaks]
23+
if limit_breaks:
24+
print("Result is partial!")
25+
for lb in limit_breaks:
26+
print(f" {lb.limit_type}: value={lb.value}, limit={lb.limit}")
27+
"""
28+
29+
limit: int
30+
"""The configured threshold value."""
31+
32+
limit_type: str
33+
"""Type of the limit that was broken, e.g. ``"rowCount"``."""
34+
35+
value: int | None = None
36+
"""The actual value that triggered the limit; ``None`` when it cannot be determined exactly."""
37+
38+
@classmethod
39+
def from_dict(cls, data: dict[str, Any]) -> ExecutionResultLimitBreak:
40+
"""Create an :class:`ExecutionResultLimitBreak` from a raw API response dict.
41+
42+
Args:
43+
data: A single item from the ``limitBreaks`` list in the execution result
44+
metadata, as returned by the GoodData API.
45+
46+
Returns:
47+
A typed :class:`ExecutionResultLimitBreak` instance.
48+
"""
49+
return cls(
50+
limit=data["limit"],
51+
limit_type=data["limitType"],
52+
value=data.get("value"),
53+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# (C) 2024 GoodData Corporation
2+
"""Integration test: verify ExecutionResultLimitBreak is surfaced correctly.
3+
4+
The cassette for this test must be recorded against a server whose row-count
5+
limit is configured lower than the result size produced by the execution below.
6+
When the limit is broken the backend returns a non-empty ``limitBreaks`` list
7+
inside ``metadata``; this test asserts that each item can be parsed into an
8+
``ExecutionResultLimitBreak`` instance with the expected field types.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from pathlib import Path
14+
15+
from gooddata_sdk import (
16+
Attribute,
17+
ExecutionDefinition,
18+
ExecutionResultLimitBreak,
19+
GoodDataSdk,
20+
ObjId,
21+
SimpleMetric,
22+
TableDimension,
23+
)
24+
from tests_support.vcrpy_utils import get_vcr
25+
26+
gd_vcr = get_vcr()
27+
28+
_current_dir = Path(__file__).parent.absolute()
29+
_fixtures_dir = _current_dir / "fixtures"
30+
31+
32+
@gd_vcr.use_cassette(str(_fixtures_dir / "test_execution_result_limit_break.yaml"))
33+
def test_execution_result_limit_break(test_config):
34+
"""Execution result metadata exposes limit-break info via raw dict access.
35+
36+
The cassette captures a response where the row-count limit is triggered so
37+
that ``metadata["limitBreaks"]`` is non-empty. This test verifies that:
38+
* the raw field is accessible from the result metadata dict
39+
* every item parses into an ``ExecutionResultLimitBreak`` with correct types
40+
* required fields (``limit``, ``limit_type``) are populated
41+
* optional ``value`` is ``int | None``
42+
"""
43+
sdk = GoodDataSdk.create(host_=test_config["host"], token_=test_config["token"])
44+
workspace_id = test_config["workspace"]
45+
46+
exec_def = ExecutionDefinition(
47+
attributes=[Attribute(local_id="a_region", label="region")],
48+
metrics=[SimpleMetric(local_id="m_order_amount", item=ObjId(type="metric", id="order_amount"))],
49+
filters=[],
50+
dimensions=[
51+
TableDimension(item_ids=["a_region"]),
52+
TableDimension(item_ids=["m_order_amount"]),
53+
],
54+
)
55+
56+
execution = sdk.compute.for_exec_def(workspace_id=workspace_id, exec_def=exec_def)
57+
result = execution.read_result(limit=1000)
58+
59+
# Access limitBreaks from the raw metadata dict (the generated client does not
60+
# expose this field yet — it will once gooddata-api-client is regenerated).
61+
raw_breaks = result.metadata.get("limitBreaks") or [] # type: ignore[union-attr]
62+
63+
# The cassette is recorded with at least one limit break present.
64+
assert len(raw_breaks) > 0, (
65+
"Expected at least one limitBreak in the result metadata. "
66+
"Re-record the cassette against a server with a low row-count limit."
67+
)
68+
69+
for raw in raw_breaks:
70+
lb = ExecutionResultLimitBreak.from_dict(raw)
71+
assert isinstance(lb.limit, int), f"limit must be int, got {type(lb.limit)}"
72+
assert isinstance(lb.limit_type, str), f"limit_type must be str, got {type(lb.limit_type)}"
73+
assert lb.value is None or isinstance(lb.value, int), f"value must be int or None, got {type(lb.value)}"
74+
assert lb.limit > 0, "limit threshold must be positive"
75+
assert lb.limit_type, "limit_type must not be empty"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# (C) 2024 GoodData Corporation
2+
from __future__ import annotations
3+
4+
import pytest
5+
from gooddata_sdk import ExecutionResultLimitBreak
6+
7+
8+
@pytest.mark.parametrize(
9+
"scenario, data, expected_limit, expected_limit_type, expected_value",
10+
[
11+
(
12+
"required_fields_only",
13+
{"limit": 100000, "limitType": "rowCount"},
14+
100000,
15+
"rowCount",
16+
None,
17+
),
18+
(
19+
"with_known_value",
20+
{"limit": 100000, "limitType": "rowCount", "value": 123456},
21+
100000,
22+
"rowCount",
23+
123456,
24+
),
25+
(
26+
"with_null_value",
27+
{"limit": 500, "limitType": "columnCount", "value": None},
28+
500,
29+
"columnCount",
30+
None,
31+
),
32+
(
33+
"different_limit_type",
34+
{"limit": 50, "limitType": "dimensionItemCount"},
35+
50,
36+
"dimensionItemCount",
37+
None,
38+
),
39+
],
40+
)
41+
def test_from_dict(
42+
scenario: str,
43+
data: dict,
44+
expected_limit: int,
45+
expected_limit_type: str,
46+
expected_value: int | None,
47+
) -> None:
48+
lb = ExecutionResultLimitBreak.from_dict(data)
49+
50+
assert lb.limit == expected_limit
51+
assert lb.limit_type == expected_limit_type
52+
assert lb.value == expected_value
53+
54+
55+
def test_importable_from_gooddata_sdk() -> None:
56+
"""ExecutionResultLimitBreak must be importable from the top-level package."""
57+
# The top-level import at the module level already validates this.
58+
assert ExecutionResultLimitBreak.__name__ == "ExecutionResultLimitBreak"

0 commit comments

Comments
 (0)