From 16bfb13dadff928a78b1b18d4aec5a5eebd95e65 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 13:54:17 +0300 Subject: [PATCH 01/10] Add internal canonical ReportSpec assembler --- src/rtichoke/_report_spec.py | 106 +++++++++++++++++ tests/test_report_spec.py | 224 +++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/rtichoke/_report_spec.py create mode 100644 tests/test_report_spec.py diff --git a/src/rtichoke/_report_spec.py b/src/rtichoke/_report_spec.py new file mode 100644 index 00000000..9604ce1f --- /dev/null +++ b/src/rtichoke/_report_spec.py @@ -0,0 +1,106 @@ +"""Internal assembler for canonical ``rtichoke_viz`` ReportSpec objects. + +The assembler composes complete standalone canonical component specs. It does +not calculate statistics, normalize component specs, hoist evaluations, or +create report-global semantic registries. + +This path is intentionally separate from the existing public summary-report +API, which currently delegates to the historical R backend and Quarto +composition. A future migration can replace that composition layer with the +shared browser ``renderReport()`` once an immutable vendored ``rtichoke_viz`` +release exposes the report renderer. The existing public report behavior is +left unchanged here. +""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import TypedDict + +_SUPPORTED_COMPONENT_TYPES = { + "performance_table", + "roc", + "calibration", + "precision_recall", + "gains", + "lift", +} +_COMPONENT_ID_BASES = { + "performance_table": "performance-table", + "roc": "roc", + "calibration": "calibration", + "precision_recall": "precision-recall", + "gains": "gains", + "lift": "lift", +} + + +class _ReportComponentInput(TypedDict, total=False): + spec: Mapping[str, object] + title: str + + +class _ReportComponent(TypedDict, total=False): + id: str + title: str + spec: Mapping[str, object] + + +class _ReportSpec(TypedDict, total=False): + schemaVersion: str + type: str + title: str + components: list[_ReportComponent] + + +def _report_spec_from_components( + components: Sequence[_ReportComponentInput], + *, + title: str | None = None, +) -> _ReportSpec: + """Compose complete canonical component specs into a ReportSpec. + + Component order is preserved exactly. Component IDs are deterministic and + live in a report-local identity domain: the first component of a type gets + its boring base ID (for example ``roc``), and repeats get ``-2``, ``-3``, + and so on. Embedded specs are retained as-is, so evaluation IDs remain + component-local even when equal strings occur in multiple components. + """ + if not components: + raise ValueError("ReportSpec requires at least one component") + + type_counts: dict[str, int] = {} + report_components: list[_ReportComponent] = [] + for component in components: + spec = component.get("spec") + if spec is None: + raise ValueError("Report component is missing spec") + + component_type = spec.get("type") + if not isinstance(component_type, str): + raise ValueError("Report component spec is missing a string type") + if component_type not in _SUPPORTED_COMPONENT_TYPES: + raise ValueError(f"Unsupported ReportSpec component type: {component_type}") + + count = type_counts.get(component_type, 0) + 1 + type_counts[component_type] = count + base_id = _COMPONENT_ID_BASES[component_type] + component_id = base_id if count == 1 else f"{base_id}-{count}" + + assembled: _ReportComponent = { + "id": component_id, + "spec": spec, + } + component_title = component.get("title") + if component_title is not None: + assembled["title"] = component_title + report_components.append(assembled) + + report: _ReportSpec = { + "schemaVersion": "1.0", + "type": "report", + "components": report_components, + } + if title is not None: + report["title"] = title + return report diff --git a/tests/test_report_spec.py b/tests/test_report_spec.py new file mode 100644 index 00000000..4a466d44 --- /dev/null +++ b/tests/test_report_spec.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from rtichoke._report_spec import _report_spec_from_components +from rtichoke.summary_report import summary_report as legacy_summary_report + + +def _curve_spec( + chart_type: str, + *, + evaluations: list[dict[str, object]] | None = None, + horizon: float | None = None, +) -> dict[str, object]: + evaluations = evaluations or [ + {"id": "evaluation-1", "model": "model-a", "population": "population-a"} + ] + series: dict[str, object] = { + "id": "series-1", + "evaluationId": "evaluation-1", + "display": {"label": "Model A", "group": "Model A", "role": "model"}, + } + if horizon is not None: + series["horizon"] = horizon + return { + "schemaVersion": "2.0", + "type": chart_type, + "evaluations": evaluations, + "series": [series], + "data": [{"seriesId": "series-1", "cutoff": 0.5}], + "x": "false_positive_rate", + "y": "sensitivity", + "xAxis": {"label": "x", "domain": [0, 1]}, + "yAxis": {"label": "y", "domain": [0, 1]}, + "references": [], + } + + +def _performance_table_spec(*, time_dependent: bool = False) -> dict[str, object]: + row: dict[str, object] = { + "evaluationId": "evaluation-1", + "operatingPoint": {"type": "probability_threshold", "value": 0.5}, + "values": [{"metricId": "sensitivity", "estimate": 0.8}], + } + if time_dependent: + row["horizon"] = 365.0 + row["context"] = { + "censoringHeuristic": "include", + "competingEventHeuristic": "exclude", + } + return { + "schemaVersion": "2.0", + "type": "performance_table", + "evaluations": [ + {"id": "evaluation-1", "model": "model-a", "population": "population-a"} + ], + "metrics": [{"id": "sensitivity", "label": "Sensitivity"}], + "rows": [row], + } + + +def test_report_composes_performance_table_roc_and_calibration_in_order() -> None: + performance_table = _performance_table_spec() + roc = _curve_spec("roc") + calibration = _curve_spec("calibration") + + report = _report_spec_from_components( + [ + {"spec": performance_table, "title": "Performance"}, + {"spec": roc}, + {"spec": calibration, "title": "Calibration"}, + ], + title="Model report", + ) + + assert report["schemaVersion"] == "1.0" + assert report["type"] == "report" + assert report["title"] == "Model report" + assert [component["id"] for component in report["components"]] == [ + "performance-table", + "roc", + "calibration", + ] + assert [component["spec"]["type"] for component in report["components"]] == [ + "performance_table", + "roc", + "calibration", + ] + + +def test_component_ids_are_deterministic_unique_and_separate_from_series_ids() -> None: + first_roc = _curve_spec("roc") + second_roc = _curve_spec("roc") + + first = _report_spec_from_components( + [{"spec": first_roc}, {"spec": second_roc}, {"spec": _curve_spec("lift")}] + ) + second = _report_spec_from_components( + [{"spec": first_roc}, {"spec": second_roc}, {"spec": _curve_spec("lift")}] + ) + + ids = [component["id"] for component in first["components"]] + assert ids == ["roc", "roc-2", "lift"] + assert ids == [component["id"] for component in second["components"]] + assert len(ids) == len(set(ids)) + assert "series-1" not in ids + + +def test_embedded_specs_are_complete_unchanged_and_evaluations_are_component_local() -> None: + roc = _curve_spec("roc") + calibration = _curve_spec("calibration") + + report = _report_spec_from_components([{"spec": roc}, {"spec": calibration}]) + + assert report["components"][0]["spec"] is roc + assert report["components"][1]["spec"] is calibration + assert roc["evaluations"] == calibration["evaluations"] + assert report["components"][0]["spec"]["evaluations"][0]["id"] == "evaluation-1" + assert report["components"][1]["spec"]["evaluations"][0]["id"] == "evaluation-1" + + +def test_report_preserves_model_known_unknown_and_multiple_populations() -> None: + known = _curve_spec( + "roc", + evaluations=[ + {"id": "evaluation-1", "model": "model-a", "population": "population-a"}, + {"id": "evaluation-2", "model": "model-b", "population": "population-b"}, + ], + ) + unknown = _curve_spec( + "calibration", + evaluations=[{"id": "evaluation-1", "population": "population-c"}], + ) + + report = _report_spec_from_components([{"spec": known}, {"spec": unknown}]) + + known_evaluations = report["components"][0]["spec"]["evaluations"] + unknown_evaluation = report["components"][1]["spec"]["evaluations"][0] + assert known_evaluations[0]["model"] == "model-a" + assert {item["population"] for item in known_evaluations} == { + "population-a", + "population-b", + } + assert "model" not in unknown_evaluation + assert unknown_evaluation["population"] == "population-c" + + +def test_time_dependent_component_is_embedded_without_recomputation() -> None: + table = _performance_table_spec(time_dependent=True) + gains = _curve_spec("gains", horizon=365.0) + + report = _report_spec_from_components([{"spec": table}, {"spec": gains}]) + + assert report["components"][0]["spec"] is table + assert report["components"][1]["spec"] is gains + assert table["rows"][0]["horizon"] == 365.0 + assert gains["series"][0]["horizon"] == 365.0 + + +def test_all_first_report_component_types_are_supported() -> None: + specs = [ + _performance_table_spec(), + _curve_spec("roc"), + _curve_spec("calibration"), + _curve_spec("precision_recall"), + _curve_spec("gains"), + _curve_spec("lift"), + ] + + report = _report_spec_from_components([{"spec": spec} for spec in specs]) + + assert [component["id"] for component in report["components"]] == [ + "performance-table", + "roc", + "calibration", + "precision-recall", + "gains", + "lift", + ] + + +def test_report_requires_components_and_rejects_out_of_scope_types() -> None: + with pytest.raises(ValueError, match="at least one component"): + _report_spec_from_components([]) + + with pytest.raises(ValueError, match="Unsupported ReportSpec component type"): + _report_spec_from_components([{"spec": {"type": "decision_curve"}}]) + + +def test_existing_public_summary_report_still_uses_r_backend(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[dict[str, Any]] = [] + + class _Response: + def json(self) -> list[dict[str, object]]: + return [{"report": object()}] + + def fake_send_requests_to_rtichoke_r(**kwargs: Any) -> _Response: + calls.append(kwargs) + return _Response() + + monkeypatch.setattr( + legacy_summary_report, + "send_requests_to_rtichoke_r", + fake_send_requests_to_rtichoke_r, + ) + + legacy_summary_report.create_summary_report( + {"model-a": [0.1, 0.9]}, + [0, 1], + url_api="http://example.test/", + ) + + assert calls == [ + { + "dictionary_to_send": { + "probs": {"model-a": [0.1, 0.9]}, + "reals": [0, 1], + }, + "url_api": "http://example.test/", + "endpoint": "create_summary_report", + } + ] From f7dca8d7ee4995b9d17131d8300f08232653cc08 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 13:56:55 +0300 Subject: [PATCH 02/10] Format ReportSpec tests --- tests/test_report_spec.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/test_report_spec.py b/tests/test_report_spec.py index 4a466d44..dde8fe0a 100644 --- a/tests/test_report_spec.py +++ b/tests/test_report_spec.py @@ -13,7 +13,7 @@ def _curve_spec( *, evaluations: list[dict[str, object]] | None = None, horizon: float | None = None, -) -> dict[str, object]: +) -> dict[str, Any]: evaluations = evaluations or [ {"id": "evaluation-1", "model": "model-a", "population": "population-a"} ] @@ -38,7 +38,7 @@ def _curve_spec( } -def _performance_table_spec(*, time_dependent: bool = False) -> dict[str, object]: +def _performance_table_spec(*, time_dependent: bool = False) -> dict[str, Any]: row: dict[str, object] = { "evaluationId": "evaluation-1", "operatingPoint": {"type": "probability_threshold", "value": 0.5}, @@ -90,7 +90,9 @@ def test_report_composes_performance_table_roc_and_calibration_in_order() -> Non ] -def test_component_ids_are_deterministic_unique_and_separate_from_series_ids() -> None: +def test_component_ids_are_deterministic_unique_and_separate_from_series_ids() -> ( + None +): first_roc = _curve_spec("roc") second_roc = _curve_spec("roc") @@ -108,7 +110,9 @@ def test_component_ids_are_deterministic_unique_and_separate_from_series_ids() - assert "series-1" not in ids -def test_embedded_specs_are_complete_unchanged_and_evaluations_are_component_local() -> None: +def test_embedded_specs_are_complete_unchanged_and_evaluations_are_component_local() -> ( + None +): roc = _curve_spec("roc") calibration = _curve_spec("calibration") @@ -117,8 +121,8 @@ def test_embedded_specs_are_complete_unchanged_and_evaluations_are_component_loc assert report["components"][0]["spec"] is roc assert report["components"][1]["spec"] is calibration assert roc["evaluations"] == calibration["evaluations"] - assert report["components"][0]["spec"]["evaluations"][0]["id"] == "evaluation-1" - assert report["components"][1]["spec"]["evaluations"][0]["id"] == "evaluation-1" + assert roc["evaluations"][0]["id"] == "evaluation-1" + assert calibration["evaluations"][0]["id"] == "evaluation-1" def test_report_preserves_model_known_unknown_and_multiple_populations() -> None: @@ -136,15 +140,15 @@ def test_report_preserves_model_known_unknown_and_multiple_populations() -> None report = _report_spec_from_components([{"spec": known}, {"spec": unknown}]) - known_evaluations = report["components"][0]["spec"]["evaluations"] - unknown_evaluation = report["components"][1]["spec"]["evaluations"][0] - assert known_evaluations[0]["model"] == "model-a" - assert {item["population"] for item in known_evaluations} == { + assert report["components"][0]["spec"] is known + assert report["components"][1]["spec"] is unknown + assert known["evaluations"][0]["model"] == "model-a" + assert {item["population"] for item in known["evaluations"]} == { "population-a", "population-b", } - assert "model" not in unknown_evaluation - assert unknown_evaluation["population"] == "population-c" + assert "model" not in unknown["evaluations"][0] + assert unknown["evaluations"][0]["population"] == "population-c" def test_time_dependent_component_is_embedded_without_recomputation() -> None: @@ -189,7 +193,9 @@ def test_report_requires_components_and_rejects_out_of_scope_types() -> None: _report_spec_from_components([{"spec": {"type": "decision_curve"}}]) -def test_existing_public_summary_report_still_uses_r_backend(monkeypatch: pytest.MonkeyPatch) -> None: +def test_existing_public_summary_report_still_uses_r_backend( + monkeypatch: pytest.MonkeyPatch, +) -> None: calls: list[dict[str, Any]] = [] class _Response: From 75cf1fa36804e9c025e1c07cbf6cebec29ea3e56 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 13:58:24 +0300 Subject: [PATCH 03/10] Normalize ReportSpec test formatting --- tests/test_report_spec.py | 152 +++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 42 deletions(-) diff --git a/tests/test_report_spec.py b/tests/test_report_spec.py index dde8fe0a..d4aca291 100644 --- a/tests/test_report_spec.py +++ b/tests/test_report_spec.py @@ -8,6 +8,20 @@ from rtichoke.summary_report import summary_report as legacy_summary_report +def _evaluation( + evaluation_id: str, + population: str, + model: str | None = None, +) -> dict[str, object]: + result: dict[str, object] = { + "id": evaluation_id, + "population": population, + } + if model is not None: + result["model"] = model + return result + + def _curve_spec( chart_type: str, *, @@ -15,12 +29,17 @@ def _curve_spec( horizon: float | None = None, ) -> dict[str, Any]: evaluations = evaluations or [ - {"id": "evaluation-1", "model": "model-a", "population": "population-a"} + _evaluation("evaluation-1", "population-a", "model-a") ] + display = { + "label": "Model A", + "group": "Model A", + "role": "model", + } series: dict[str, object] = { "id": "series-1", "evaluationId": "evaluation-1", - "display": {"label": "Model A", "group": "Model A", "role": "model"}, + "display": display, } if horizon is not None: series["horizon"] = horizon @@ -41,8 +60,16 @@ def _curve_spec( def _performance_table_spec(*, time_dependent: bool = False) -> dict[str, Any]: row: dict[str, object] = { "evaluationId": "evaluation-1", - "operatingPoint": {"type": "probability_threshold", "value": 0.5}, - "values": [{"metricId": "sensitivity", "estimate": 0.8}], + "operatingPoint": { + "type": "probability_threshold", + "value": 0.5, + }, + "values": [ + { + "metricId": "sensitivity", + "estimate": 0.8, + } + ], } if time_dependent: row["horizon"] = 365.0 @@ -54,23 +81,34 @@ def _performance_table_spec(*, time_dependent: bool = False) -> dict[str, Any]: "schemaVersion": "2.0", "type": "performance_table", "evaluations": [ - {"id": "evaluation-1", "model": "model-a", "population": "population-a"} + _evaluation("evaluation-1", "population-a", "model-a") + ], + "metrics": [ + { + "id": "sensitivity", + "label": "Sensitivity", + } ], - "metrics": [{"id": "sensitivity", "label": "Sensitivity"}], "rows": [row], } -def test_report_composes_performance_table_roc_and_calibration_in_order() -> None: +def test_core_composition_and_order() -> None: performance_table = _performance_table_spec() roc = _curve_spec("roc") calibration = _curve_spec("calibration") report = _report_spec_from_components( [ - {"spec": performance_table, "title": "Performance"}, + { + "spec": performance_table, + "title": "Performance", + }, {"spec": roc}, - {"spec": calibration, "title": "Calibration"}, + { + "spec": calibration, + "title": "Calibration", + }, ], title="Model report", ) @@ -78,45 +116,50 @@ def test_report_composes_performance_table_roc_and_calibration_in_order() -> Non assert report["schemaVersion"] == "1.0" assert report["type"] == "report" assert report["title"] == "Model report" - assert [component["id"] for component in report["components"]] == [ + component_ids = [component["id"] for component in report["components"]] + component_types = [component["spec"]["type"] for component in report["components"]] + assert component_ids == [ "performance-table", "roc", "calibration", ] - assert [component["spec"]["type"] for component in report["components"]] == [ + assert component_types == [ "performance_table", "roc", "calibration", ] -def test_component_ids_are_deterministic_unique_and_separate_from_series_ids() -> ( - None -): +def test_component_ids_are_deterministic_and_unique() -> None: first_roc = _curve_spec("roc") second_roc = _curve_spec("roc") + inputs = [ + {"spec": first_roc}, + {"spec": second_roc}, + {"spec": _curve_spec("lift")}, + ] - first = _report_spec_from_components( - [{"spec": first_roc}, {"spec": second_roc}, {"spec": _curve_spec("lift")}] - ) - second = _report_spec_from_components( - [{"spec": first_roc}, {"spec": second_roc}, {"spec": _curve_spec("lift")}] - ) + first = _report_spec_from_components(inputs) + second = _report_spec_from_components(inputs) ids = [component["id"] for component in first["components"]] + second_ids = [component["id"] for component in second["components"]] assert ids == ["roc", "roc-2", "lift"] - assert ids == [component["id"] for component in second["components"]] + assert ids == second_ids assert len(ids) == len(set(ids)) assert "series-1" not in ids -def test_embedded_specs_are_complete_unchanged_and_evaluations_are_component_local() -> ( - None -): +def test_specs_remain_complete_and_component_local() -> None: roc = _curve_spec("roc") calibration = _curve_spec("calibration") - report = _report_spec_from_components([{"spec": roc}, {"spec": calibration}]) + report = _report_spec_from_components( + [ + {"spec": roc}, + {"spec": calibration}, + ] + ) assert report["components"][0]["spec"] is roc assert report["components"][1]["spec"] is calibration @@ -125,25 +168,31 @@ def test_embedded_specs_are_complete_unchanged_and_evaluations_are_component_loc assert calibration["evaluations"][0]["id"] == "evaluation-1" -def test_report_preserves_model_known_unknown_and_multiple_populations() -> None: +def test_semantics_pass_through_unchanged() -> None: known = _curve_spec( "roc", evaluations=[ - {"id": "evaluation-1", "model": "model-a", "population": "population-a"}, - {"id": "evaluation-2", "model": "model-b", "population": "population-b"}, + _evaluation("evaluation-1", "population-a", "model-a"), + _evaluation("evaluation-2", "population-b", "model-b"), ], ) unknown = _curve_spec( "calibration", - evaluations=[{"id": "evaluation-1", "population": "population-c"}], + evaluations=[_evaluation("evaluation-1", "population-c")], ) - report = _report_spec_from_components([{"spec": known}, {"spec": unknown}]) + report = _report_spec_from_components( + [ + {"spec": known}, + {"spec": unknown}, + ] + ) assert report["components"][0]["spec"] is known assert report["components"][1]["spec"] is unknown assert known["evaluations"][0]["model"] == "model-a" - assert {item["population"] for item in known["evaluations"]} == { + populations = {item["population"] for item in known["evaluations"]} + assert populations == { "population-a", "population-b", } @@ -151,11 +200,16 @@ def test_report_preserves_model_known_unknown_and_multiple_populations() -> None assert unknown["evaluations"][0]["population"] == "population-c" -def test_time_dependent_component_is_embedded_without_recomputation() -> None: +def test_time_dependent_specs_pass_through_unchanged() -> None: table = _performance_table_spec(time_dependent=True) gains = _curve_spec("gains", horizon=365.0) - report = _report_spec_from_components([{"spec": table}, {"spec": gains}]) + report = _report_spec_from_components( + [ + {"spec": table}, + {"spec": gains}, + ] + ) assert report["components"][0]["spec"] is table assert report["components"][1]["spec"] is gains @@ -163,7 +217,7 @@ def test_time_dependent_component_is_embedded_without_recomputation() -> None: assert gains["series"][0]["horizon"] == 365.0 -def test_all_first_report_component_types_are_supported() -> None: +def test_first_report_component_types_are_supported() -> None: specs = [ _performance_table_spec(), _curve_spec("roc"), @@ -175,7 +229,8 @@ def test_all_first_report_component_types_are_supported() -> None: report = _report_spec_from_components([{"spec": spec} for spec in specs]) - assert [component["id"] for component in report["components"]] == [ + component_ids = [component["id"] for component in report["components"]] + assert component_ids == [ "performance-table", "roc", "calibration", @@ -185,15 +240,26 @@ def test_all_first_report_component_types_are_supported() -> None: ] -def test_report_requires_components_and_rejects_out_of_scope_types() -> None: +def test_invalid_report_components_are_rejected() -> None: with pytest.raises(ValueError, match="at least one component"): _report_spec_from_components([]) - with pytest.raises(ValueError, match="Unsupported ReportSpec component type"): - _report_spec_from_components([{"spec": {"type": "decision_curve"}}]) - - -def test_existing_public_summary_report_still_uses_r_backend( + with pytest.raises( + ValueError, + match="Unsupported ReportSpec component type", + ): + _report_spec_from_components( + [ + { + "spec": { + "type": "decision_curve", + } + } + ] + ) + + +def test_existing_summary_report_still_uses_r_backend( monkeypatch: pytest.MonkeyPatch, ) -> None: calls: list[dict[str, Any]] = [] @@ -221,7 +287,9 @@ def fake_send_requests_to_rtichoke_r(**kwargs: Any) -> _Response: assert calls == [ { "dictionary_to_send": { - "probs": {"model-a": [0.1, 0.9]}, + "probs": { + "model-a": [0.1, 0.9], + }, "reals": [0, 1], }, "url_api": "http://example.test/", From 7ca01af242ca4e6e1ba01a7ce4eb0814939e92ba Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 13:59:41 +0300 Subject: [PATCH 04/10] Temporarily show Ruff formatter diff --- .github/workflows/python-package.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1bc44d40..6ad18ed2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -32,7 +32,12 @@ jobs: run: uv run ruff check . - name: Check format - run: uv run ruff format --check . + run: | + if ! uv run ruff format --check .; then + uv run ruff format tests/test_report_spec.py + git diff -- tests/test_report_spec.py + exit 1 + fi - name: Check types run: uv run ty check src/rtichoke From 3d57d249a04569a70ace2420b67588ab567fa2d4 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 14:00:35 +0300 Subject: [PATCH 05/10] Apply Ruff formatting --- tests/test_report_spec.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_report_spec.py b/tests/test_report_spec.py index d4aca291..3ba52b0a 100644 --- a/tests/test_report_spec.py +++ b/tests/test_report_spec.py @@ -80,9 +80,7 @@ def _performance_table_spec(*, time_dependent: bool = False) -> dict[str, Any]: return { "schemaVersion": "2.0", "type": "performance_table", - "evaluations": [ - _evaluation("evaluation-1", "population-a", "model-a") - ], + "evaluations": [_evaluation("evaluation-1", "population-a", "model-a")], "metrics": [ { "id": "sensitivity", From edeb9f418b1f450d2f70d027e0304bb20e5c5716 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 14:00:48 +0300 Subject: [PATCH 06/10] Restore package CI workflow --- .github/workflows/python-package.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6ad18ed2..1bc44d40 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -32,12 +32,7 @@ jobs: run: uv run ruff check . - name: Check format - run: | - if ! uv run ruff format --check .; then - uv run ruff format tests/test_report_spec.py - git diff -- tests/test_report_spec.py - exit 1 - fi + run: uv run ruff format --check . - name: Check types run: uv run ty check src/rtichoke From ee5920ce262a2d2f40468bad7d84046fed8ed789 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 14:02:26 +0300 Subject: [PATCH 07/10] Temporarily run full documented ty scope --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1bc44d40..0f1469e0 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -35,7 +35,7 @@ jobs: run: uv run ruff format --check . - name: Check types - run: uv run ty check src/rtichoke + run: uv run ty check src tests - name: Build package run: uv build From 4dccb66257373192daf1ce76306a589a9bc25c46 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 14:04:18 +0300 Subject: [PATCH 08/10] Make ReportSpec assembler input type mapping-friendly --- src/rtichoke/_report_spec.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/rtichoke/_report_spec.py b/src/rtichoke/_report_spec.py index 9604ce1f..455406dc 100644 --- a/src/rtichoke/_report_spec.py +++ b/src/rtichoke/_report_spec.py @@ -35,11 +35,6 @@ } -class _ReportComponentInput(TypedDict, total=False): - spec: Mapping[str, object] - title: str - - class _ReportComponent(TypedDict, total=False): id: str title: str @@ -54,7 +49,7 @@ class _ReportSpec(TypedDict, total=False): def _report_spec_from_components( - components: Sequence[_ReportComponentInput], + components: Sequence[Mapping[str, object]], *, title: str | None = None, ) -> _ReportSpec: @@ -73,7 +68,7 @@ def _report_spec_from_components( report_components: list[_ReportComponent] = [] for component in components: spec = component.get("spec") - if spec is None: + if not isinstance(spec, Mapping): raise ValueError("Report component is missing spec") component_type = spec.get("type") @@ -93,6 +88,8 @@ def _report_spec_from_components( } component_title = component.get("title") if component_title is not None: + if not isinstance(component_title, str): + raise ValueError("Report component title must be a string") assembled["title"] = component_title report_components.append(assembled) From 3ed81a3ef152cc651e0ead901baf69a8a718b8f1 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 14:04:56 +0300 Subject: [PATCH 09/10] Fix ReportSpec type narrowing --- src/rtichoke/_report_spec.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/rtichoke/_report_spec.py b/src/rtichoke/_report_spec.py index 455406dc..0a604513 100644 --- a/src/rtichoke/_report_spec.py +++ b/src/rtichoke/_report_spec.py @@ -15,7 +15,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from typing import TypedDict +from typing import TypedDict, cast _SUPPORTED_COMPONENT_TYPES = { "performance_table", @@ -67,9 +67,10 @@ def _report_spec_from_components( type_counts: dict[str, int] = {} report_components: list[_ReportComponent] = [] for component in components: - spec = component.get("spec") - if not isinstance(spec, Mapping): + raw_spec = component.get("spec") + if not isinstance(raw_spec, Mapping): raise ValueError("Report component is missing spec") + spec = cast(Mapping[str, object], raw_spec) component_type = spec.get("type") if not isinstance(component_type, str): From 02d55e54f6cab7cbcaa1dfaaa46d0dbdcfdd6b55 Mon Sep 17 00:00:00 2001 From: Uriah Finkel Date: Mon, 24 Aug 2026 14:05:49 +0300 Subject: [PATCH 10/10] Restore package CI workflow --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0f1469e0..1bc44d40 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -35,7 +35,7 @@ jobs: run: uv run ruff format --check . - name: Check types - run: uv run ty check src tests + run: uv run ty check src/rtichoke - name: Build package run: uv build