Skip to content

Commit 465a2d8

Browse files
authored
chore: consolidate check helpers into resources/_check_helpers (#87)
1 parent 694594c commit 465a2d8

7 files changed

Lines changed: 102 additions & 99 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Request-side helpers for converting `CheckConfigParam` into the wire format."""
2+
3+
from typing import Any, Dict, Iterable, Optional, cast
4+
5+
from .._models import BaseModel
6+
from ..types.check import CheckConfigParam
7+
8+
# Maps a built-in check identifier to its `kind` discriminator.
9+
IDENTIFIER_TO_KIND: Dict[str, str] = {
10+
"correctness": "hub_correctness",
11+
"conformity": "hub_conformity",
12+
"groundedness": "hub_groundedness",
13+
"string_match": "string_matching",
14+
"metadata": "hub_metadata",
15+
"semantic_similarity": "semantic_similarity",
16+
}
17+
18+
19+
def check_param_to_spec(identifier: Optional[str], params: Any) -> Dict[str, Any]:
20+
"""Build a `spec` dict, deriving `kind` from `params["type"]` then `identifier`."""
21+
if isinstance(params, BaseModel):
22+
params_dict: Dict[str, Any] = params.model_dump(exclude_none=True)
23+
elif isinstance(params, dict):
24+
params_dict = dict(cast(Dict[str, Any], params))
25+
else:
26+
params_dict = {}
27+
type_from_params = params_dict.pop("type", None)
28+
type_str = type_from_params or identifier or ""
29+
if not type_str:
30+
raise ValueError(
31+
"Cannot derive check kind: provide 'identifier' or include 'type' in 'params', "
32+
"or pass 'spec' directly with an explicit 'kind'."
33+
)
34+
kind = IDENTIFIER_TO_KIND.get(type_str, type_str)
35+
return {"kind": kind, **params_dict}
36+
37+
38+
def check_params_to_specs(
39+
checks: Iterable[CheckConfigParam],
40+
*,
41+
flat: bool = False,
42+
) -> list[Dict[str, Any]]:
43+
"""Convert checks to the wire format.
44+
45+
`flat=False` (default) wraps params under a `spec` key:
46+
`{identifier, enabled, spec: {kind, ...params}}`.
47+
48+
`flat=True` spreads params alongside `identifier`:
49+
`{identifier, ...params}` (with the redundant `type` key stripped).
50+
"""
51+
result: list[Dict[str, Any]] = []
52+
for check in checks:
53+
identifier = check["identifier"]
54+
params = check.get("params") or {}
55+
if flat:
56+
result.append({"identifier": identifier, **{k: v for k, v in params.items() if k != "type"}})
57+
else:
58+
entry: Dict[str, Any] = {"identifier": identifier, "enabled": check.get("enabled", True)}
59+
if params:
60+
entry["spec"] = check_param_to_spec(identifier, params)
61+
result.append(entry)
62+
return result

src/giskard_hub/resources/checks.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
CheckCreateParams,
2222
CheckUpdateParams,
2323
CheckBulkDeleteParams,
24-
_check_param_to_spec,
2524
)
2625
from .._base_client import make_request_options
2726
from ..types.common import APIResponse
27+
from ._check_helpers import check_param_to_spec
2828

2929
__all__ = ["ChecksResource", "AsyncChecksResource"]
3030

@@ -113,7 +113,7 @@ def create(
113113
if not params_provided and not spec_provided:
114114
raise ValueError("Must provide either 'params' or 'spec'.")
115115

116-
api_spec = spec if spec_provided else _check_param_to_spec(identifier, params)
116+
api_spec = spec if spec_provided else check_param_to_spec(identifier, params)
117117
response = self._post(
118118
"/v2/checks",
119119
body=maybe_transform(
@@ -256,7 +256,7 @@ def update(
256256
api_spec = None
257257
else:
258258
type_or_id = identifier if isinstance(identifier, str) else None
259-
api_spec = _check_param_to_spec(type_or_id, params)
259+
api_spec = check_param_to_spec(type_or_id, params)
260260
else:
261261
api_spec = omit
262262
response = self._patch(
@@ -509,7 +509,7 @@ async def create(
509509
if not params_provided and not spec_provided:
510510
raise ValueError("Must provide either 'params' or 'spec'.")
511511

512-
api_spec = spec if spec_provided else _check_param_to_spec(identifier, params)
512+
api_spec = spec if spec_provided else check_param_to_spec(identifier, params)
513513
response = await self._post(
514514
"/v2/checks",
515515
body=await async_maybe_transform(
@@ -652,7 +652,7 @@ async def update(
652652
api_spec = None
653653
else:
654654
type_or_id = identifier if isinstance(identifier, str) else None
655-
api_spec = _check_param_to_spec(type_or_id, params)
655+
api_spec = check_param_to_spec(type_or_id, params)
656656
else:
657657
api_spec = omit
658658
response = await self._patch(

src/giskard_hub/resources/evaluations/evaluations.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from ..._base_client import make_request_options
4040
from ...types.common import APIResponse, APIResponseWithIncluded
4141
from ...types.dataset import DatasetSubsetParam
42+
from .._check_helpers import check_params_to_specs
4243
from ...types.evaluation import (
4344
Evaluation,
4445
EvaluationListParams,
@@ -70,20 +71,6 @@ def _validate_dataset_or_old_evaluation(
7071
raise ValueError("Exactly one of `dataset_id` or `old_evaluation_id` must be provided")
7172

7273

73-
def _check_params_to_api(
74-
checks: Iterable[CheckConfigParam],
75-
) -> Iterable[dict[str, object]]:
76-
# Flat shape for /v2/evaluations/run-single (FlatCheckSpec). `type` is stripped because it
77-
# duplicates `identifier` and would leak into the spec extras.
78-
return [
79-
{
80-
"identifier": check["identifier"],
81-
**{k: v for k, v in check.get("params", {}).items() if k != "type"},
82-
}
83-
for check in checks
84-
]
85-
86-
8774
def _normalize_agent_output(
8875
agent_output: AgentOutputParam | str,
8976
) -> AgentOutputParam:
@@ -721,7 +708,7 @@ def run_single(
721708
# Use input_data if provided, otherwise fall back to messages
722709
final_input_data = input_data if input_data_provided else messages
723710

724-
api_checks: Iterable[dict[str, object]] = _check_params_to_api(checks)
711+
api_checks: Iterable[dict[str, object]] = check_params_to_specs(checks, flat=True)
725712

726713
response = self._post(
727714
"/v2/evaluations/run-single",
@@ -1373,7 +1360,7 @@ async def run_single(
13731360
# Use input_data if provided, otherwise fall back to messages
13741361
final_input_data = input_data if input_data_provided else messages
13751362

1376-
api_checks: Iterable[dict[str, object]] = _check_params_to_api(checks)
1363+
api_checks: Iterable[dict[str, object]] = check_params_to_specs(checks, flat=True)
13771364

13781365
response = await self._post(
13791366
"/v2/evaluations/run-single",

src/giskard_hub/resources/test_cases/test_cases.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@
3333
async_to_streamed_response_wrapper,
3434
)
3535
from ...types.chat import ChatMessageParam, ChatMessageWithMetadataParam
36-
from ...types.check import CheckConfigParam, _check_params_to_api
36+
from ...types.check import CheckConfigParam
3737
from ..._base_client import make_request_options
3838
from ...types.common import APIResponse
39+
from .._check_helpers import check_params_to_specs
3940
from ...types.test_case import (
4041
TestCase,
4142
TestCaseCreateParams,
@@ -159,7 +160,7 @@ def create(
159160
# Use input_data if provided, otherwise fall back to messages
160161
final_input_data = input_data if input_data_provided else messages
161162

162-
api_checks: Iterable[object] | Omit = _check_params_to_api(checks) if not isinstance(checks, Omit) else omit
163+
api_checks: Iterable[object] | Omit = check_params_to_specs(checks) if not isinstance(checks, Omit) else omit
163164
api_demo_output = _normalize_demo_output(demo_output)
164165
response = self._post(
165166
"/v2/test-cases",
@@ -321,7 +322,7 @@ def update(
321322
if checks is None or isinstance(checks, Omit):
322323
api_checks = checks # type: ignore[assignment]
323324
else:
324-
api_checks = _check_params_to_api(checks)
325+
api_checks = check_params_to_specs(checks)
325326
api_demo_output = _normalize_demo_output(demo_output)
326327
response = self._patch(
327328
f"/v2/test-cases/{test_case_id}",
@@ -683,7 +684,7 @@ async def create(
683684
# Use input_data if provided, otherwise fall back to messages
684685
final_input_data = input_data if input_data_provided else messages
685686

686-
api_checks: Iterable[object] | Omit = _check_params_to_api(checks) if not isinstance(checks, Omit) else omit
687+
api_checks: Iterable[object] | Omit = check_params_to_specs(checks) if not isinstance(checks, Omit) else omit
687688
api_demo_output = _normalize_demo_output(demo_output)
688689
response = await self._post(
689690
"/v2/test-cases",
@@ -845,7 +846,7 @@ async def update(
845846
if checks is None or isinstance(checks, Omit):
846847
api_checks = checks # type: ignore[assignment]
847848
else:
848-
api_checks = _check_params_to_api(checks)
849+
api_checks = check_params_to_specs(checks)
849850
api_demo_output = _normalize_demo_output(demo_output)
850851
response = await self._patch(
851852
f"/v2/test-cases/{test_case_id}",

src/giskard_hub/types/check.py

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -240,18 +240,8 @@ class TestCaseCheckConfigParam(TypedDict, total=False):
240240
# ---------------------------------------------------------------------------
241241

242242

243-
# Mirrors the backend
244-
_IDENTIFIER_TO_KIND: Dict[str, str] = {
245-
"correctness": "hub_correctness",
246-
"conformity": "hub_conformity",
247-
"groundedness": "hub_groundedness",
248-
"string_match": "string_matching",
249-
"metadata": "hub_metadata",
250-
"semantic_similarity": "semantic_similarity",
251-
}
252-
253-
254243
def _extract_check_params(check: Dict[str, Any]) -> Dict[str, Any]:
244+
"""Strip `kind` from `spec` to derive the user-facing `params` dict."""
255245
spec: Any = check.get("spec") or {}
256246
if isinstance(spec, BaseModel):
257247
return spec.model_dump(exclude={"kind"}, exclude_none=True)
@@ -260,25 +250,6 @@ def _extract_check_params(check: Dict[str, Any]) -> Dict[str, Any]:
260250
return {}
261251

262252

263-
def _check_param_to_spec(identifier: Optional[str], params: Any) -> Dict[str, Any]:
264-
"""Build a `spec` dict, deriving `kind` from `params["type"]` then `identifier`."""
265-
if isinstance(params, BaseModel):
266-
params_dict: Dict[str, Any] = params.model_dump(exclude_none=True)
267-
elif isinstance(params, dict):
268-
params_dict = dict(cast(Dict[str, Any], params))
269-
else:
270-
params_dict = {}
271-
type_from_params = params_dict.pop("type", None)
272-
type_str = type_from_params or identifier or ""
273-
if not type_str:
274-
raise ValueError(
275-
"Cannot derive check kind: provide 'identifier' or include 'type' in 'params', "
276-
"or pass 'spec' directly with an explicit 'kind'."
277-
)
278-
kind = _IDENTIFIER_TO_KIND.get(type_str, type_str)
279-
return {"kind": kind, **params_dict}
280-
281-
282253
class CheckConfig(BaseModel):
283254
identifier: str
284255
enabled: Optional[bool] = None
@@ -303,23 +274,6 @@ class CheckConfigParam(TypedDict, total=False):
303274
params: Dict[str, Any]
304275

305276

306-
def _check_params_to_api( # pyright: ignore[reportUnusedFunction]
307-
checks: Iterable[CheckConfigParam],
308-
) -> list[Dict[str, Any]]:
309-
return [
310-
{
311-
"identifier": check["identifier"],
312-
"enabled": check.get("enabled", True),
313-
**(
314-
{"spec": _check_param_to_spec(check["identifier"], check.get("params", {}))}
315-
if check.get("params")
316-
else {}
317-
),
318-
}
319-
for check in checks
320-
]
321-
322-
323277
# ---------------------------------------------------------------------------
324278
# Check params
325279
# ---------------------------------------------------------------------------

tests/test_checks.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
CorrectnessParams,
1616
JsonPathRuleParam,
1717
)
18-
from giskard_hub.types.check import (
19-
_IDENTIFIER_TO_KIND,
20-
_check_param_to_spec,
21-
_check_params_to_api,
22-
_extract_check_params,
18+
from giskard_hub.types.check import _extract_check_params
19+
from giskard_hub.resources._check_helpers import (
20+
IDENTIFIER_TO_KIND,
21+
check_param_to_spec,
22+
check_params_to_specs,
2323
)
2424

2525
# ---------------------------------------------------------------------------
@@ -28,7 +28,7 @@
2828

2929

3030
def test_identifier_to_kind_mapping() -> None:
31-
assert _IDENTIFIER_TO_KIND == {
31+
assert IDENTIFIER_TO_KIND == {
3232
"correctness": "hub_correctness",
3333
"conformity": "hub_conformity",
3434
"groundedness": "hub_groundedness",
@@ -39,27 +39,27 @@ def test_identifier_to_kind_mapping() -> None:
3939

4040

4141
def test_check_param_to_spec_prefers_params_type_over_identifier() -> None:
42-
spec = _check_param_to_spec("custom_name", {"type": "conformity", "rules": ["r"]})
42+
spec = check_param_to_spec("custom_name", {"type": "conformity", "rules": ["r"]})
4343
assert spec == {"kind": "hub_conformity", "rules": ["r"]}
4444

4545

4646
def test_check_param_to_spec_falls_back_to_identifier() -> None:
47-
spec = _check_param_to_spec("correctness", {"reference": "x"})
47+
spec = check_param_to_spec("correctness", {"reference": "x"})
4848
assert spec == {"kind": "hub_correctness", "reference": "x"}
4949

5050

5151
def test_check_param_to_spec_passes_through_unknown_kind() -> None:
52-
spec = _check_param_to_spec("future_kind", {"foo": 1})
52+
spec = check_param_to_spec("future_kind", {"foo": 1})
5353
assert spec == {"kind": "future_kind", "foo": 1}
5454

5555

5656
def test_check_param_to_spec_raises_when_no_kind_derivable() -> None:
5757
with pytest.raises(ValueError, match="Cannot derive check kind"):
58-
_check_param_to_spec(None, {"reference": "x"})
58+
check_param_to_spec(None, {"reference": "x"})
5959

6060

6161
def test_check_param_to_spec_accepts_basemodel() -> None:
62-
spec = _check_param_to_spec("correctness", CorrectnessParams(reference="x"))
62+
spec = check_param_to_spec("correctness", CorrectnessParams(reference="x"))
6363
assert spec == {"kind": "hub_correctness", "reference": "x"}
6464

6565

@@ -73,8 +73,8 @@ def test_extract_check_params_empty_when_no_spec() -> None:
7373
assert _extract_check_params({"spec": None}) == {}
7474

7575

76-
def test_check_params_to_api_emits_spec_with_kind() -> None:
77-
api = _check_params_to_api([{"identifier": "correctness", "params": {"reference": "x"}}])
76+
def test_check_params_to_specs_emits_nested_with_kind() -> None:
77+
api = check_params_to_specs([{"identifier": "correctness", "params": {"reference": "x"}}])
7878
assert api == [
7979
{
8080
"identifier": "correctness",
@@ -84,13 +84,13 @@ def test_check_params_to_api_emits_spec_with_kind() -> None:
8484
]
8585

8686

87-
def test_check_params_to_api_omits_spec_when_no_params() -> None:
88-
api = _check_params_to_api([{"identifier": "tone_pro_xyz"}])
87+
def test_check_params_to_specs_omits_spec_when_no_params() -> None:
88+
api = check_params_to_specs([{"identifier": "tone_pro_xyz"}])
8989
assert api == [{"identifier": "tone_pro_xyz", "enabled": True}]
9090

9191

92-
def test_check_params_to_api_strips_redundant_type() -> None:
93-
api = _check_params_to_api([{"identifier": "string_match", "params": {"type": "string_match", "keyword": "k"}}])
92+
def test_check_params_to_specs_strips_redundant_type() -> None:
93+
api = check_params_to_specs([{"identifier": "string_match", "params": {"type": "string_match", "keyword": "k"}}])
9494
assert api == [
9595
{
9696
"identifier": "string_match",

tests/test_evaluations.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,23 @@
55
import pytest
66

77
from giskard_hub import HubClient, AsyncHubClient
8-
from giskard_hub.resources.evaluations.evaluations import (
9-
_check_params_to_api,
10-
_normalize_agent_output,
11-
)
8+
from giskard_hub.resources._check_helpers import check_params_to_specs
9+
from giskard_hub.resources.evaluations.evaluations import _normalize_agent_output
1210

1311
# ---------------------------------------------------------------------------
1412
# Helpers
1513
# ---------------------------------------------------------------------------
1614

1715

18-
def test_check_params_to_api_emits_flat_shape() -> None:
19-
api = list(_check_params_to_api([{"identifier": "correctness", "params": {"reference": "x"}}]))
16+
def test_check_params_to_specs_emits_flat_shape() -> None:
17+
api = check_params_to_specs([{"identifier": "correctness", "params": {"reference": "x"}}], flat=True)
2018
assert api == [{"identifier": "correctness", "reference": "x"}]
2119

2220

23-
def test_check_params_to_api_strips_redundant_type() -> None:
24-
api = list(
25-
_check_params_to_api([{"identifier": "string_match", "params": {"type": "string_match", "keyword": "k"}}])
21+
def test_check_params_to_specs_strips_redundant_type_when_flat() -> None:
22+
api = check_params_to_specs(
23+
[{"identifier": "string_match", "params": {"type": "string_match", "keyword": "k"}}],
24+
flat=True,
2625
)
2726
assert api == [{"identifier": "string_match", "keyword": "k"}]
2827

0 commit comments

Comments
 (0)