Skip to content

Feat: Implement GET /setup/{id} endpoint#280

Merged
PGijsbers merged 8 commits intoopenml:mainfrom
igennova:issue/61
Mar 27, 2026
Merged

Feat: Implement GET /setup/{id} endpoint#280
PGijsbers merged 8 commits intoopenml:mainfrom
igennova:issue/61

Conversation

@igennova
Copy link
Copy Markdown
Contributor

@igennova igennova commented Mar 17, 2026

FIxes: #61

Description

  • Implemented the new GET /setup/{id} endpoint in Python, bridging database input settings into structured setup JSON, identical to the old PHP API.
  • Introduced strongly-typed Pydantic schemas (SetupResponseand SetupParameter) under src/schemas/setups.py strictly representing the returned topology.

Checklist

Always:

  • I have performed a self-review of my own pull request to ensure it contains all relevant information, and the proposed changes are minimal but sufficient to accomplish their task.

Required for code changes:

  • Tests pass locally
  • I have commented my code in hard-to-understand areas, and provided or updated docstrings as needed
  • Changes are already covered under existing tests
  • I have added tests that cover the changes (only required if not already under coverage)

If applicable:

  • I have made corresponding changes to the documentation pages (/docs)

Extra context:

  • This PR and the commits have been created autonomously by a bot/agent.

Screenshot:
Screenshot 2026-03-18 at 1 21 02 AM

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds an async DB helper get_parameters(setup_id: int, connection: AsyncConnection) -> list[RowMapping] to retrieve per-input parameter metadata and stored values for a setup. Introduces a GET /setup/{setup_id} router endpoint returning SetupResponse (with SetupParameters and SetupParameter schemas) and raising 404 when a setup is missing. Adds tests for successful and unknown setup retrieval and cross-API migration comparisons. Updates docs to note integer JSON fields and omission of empty parameter lists. nested_remove_nones gains remove_empty_list: bool = False.

Possibly related PRs

  • openml/server-api PR 246 — Modifies the same setups feature and touches the same modules (src/database/setups.py, src/routers/openml/setups.py), indicating a direct code-level connection.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing the GET /setup/{id} endpoint.
Description check ✅ Passed The description is directly related to the changeset, detailing implementation of the GET /setup/{id} endpoint and schema changes.
Linked Issues check ✅ Passed The PR implements the GET /setup/{id} endpoint as required by issue #61, with database integration, Pydantic schemas, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are scoped to the GET /setup/{id} implementation: database query function, API endpoint, schemas, tests, documentation, and a utility function enhancement for response filtering.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/database/setups.py" line_range="38-42" />
<code_context>
+                t_input.dataType AS data_type,
+                t_input.defaultValue AS default_value,
+                t_setting.value AS value
+            FROM input_setting t_setting
+            JOIN input t_input ON t_setting.input_id = t_input.id
+            JOIN implementation t_impl ON t_input.implementation_id = t_impl.id
</code_context>
<issue_to_address>
**suggestion:** Consider adding an explicit ORDER BY clause to make parameter ordering deterministic.

Without an `ORDER BY`, the database may return parameters in any order, which can break callers that assume stable ordering (e.g., for diffs, cache keys, or UI). Please add an explicit sort, e.g. `ORDER BY t_impl.id, t_input.id`, to make the result deterministic.

```suggestion
            FROM input_setting t_setting
            JOIN input t_input ON t_setting.input_id = t_input.id
            JOIN implementation t_impl ON t_input.implementation_id = t_impl.id
            WHERE t_setting.setup = :setup_id
            ORDER BY t_impl.id, t_input.id
            """,
```
</issue_to_address>

### Comment 2
<location path="src/schemas/setups.py" line_range="17" />
<code_context>
+    name: str
+    data_type: str | None = None
+    default_value: str | None = None
+    value: str
+
+    model_config = ConfigDict(from_attributes=True)
</code_context>
<issue_to_address>
**issue:** Check whether `value` should be nullable to match the database and avoid validation issues.

Since `t_setting.value` is selected directly and may be NULL at the DB level, `value: str` will cause Pydantic validation failures for such rows. If NULL is valid, use `value: str | None = None` or normalize NULL to a non-null value (e.g., empty string) before model validation to avoid unexpected 500/422 responses.
</issue_to_address>

### Comment 3
<location path="src/routers/openml/setups.py" line_range="36" />
<code_context>
+
+    setup_parameters = await database.setups.get_parameters(setup_id, expdb_db)
+
+    params_model = SetupParameters(
+        setup_id=str(setup_id),
+        flow_id=str(setup.implementation_id),
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new endpoint by always returning a list for `parameter` and letting Pydantic handle object conversion instead of manually building dicts.

You can simplify the new endpoint a bit without changing behavior.

1. **Always return a list for `parameter`**

Avoid the `None` vs `[]` duality and the extra conditional. This keeps the schema simpler for clients and reduces branching:

```python
params_model = SetupParameters(
    setup_id=str(setup_id),
    flow_id=str(setup.implementation_id),
    parameter=[dict(param) for param in setup_parameters],
)
```

If `setup_parameters` is empty, `parameter` will just be `[]`.

2. **Avoid the dict conversion if possible**

Since you already have Pydantic models and likely `from_attributes=True` on `SetupParameter`, you can skip the `dict` round-trip and let Pydantic do the adaptation. For example, if `get_parameters` returns SQLAlchemy rows, you can validate them directly:

```python
from schemas.setups import SetupParameter

params_model = SetupParameters(
    setup_id=str(setup_id),
    flow_id=str(setup.implementation_id),
    parameter=[
        SetupParameter.model_validate(param, from_attributes=True)
        for param in setup_parameters
    ],
)
```

If this doesn’t work with the current return type of `get_parameters`, consider updating `get_parameters` to return objects with attribute access (`.name`, `.value`, etc.) instead of dict-like mappings, so Pydantic can map them directly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 56.25000% with 14 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@5357b01). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/routers/openml/setups.py 30.00% 7 Missing ⚠️
src/core/conversions.py 28.57% 5 Missing ⚠️
src/database/setups.py 50.00% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #280   +/-   ##
=======================================
  Coverage        ?   54.20%           
=======================================
  Files           ?       38           
  Lines           ?     1581           
  Branches        ?      137           
=======================================
  Hits            ?      857           
  Misses          ?      722           
  Partials        ?        2           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/database/setups.py (1)

28-42: Stabilize parameter ordering in the SQL result.

At Line 41, the query filters but does not sort. Without ORDER BY, parameter list order is not guaranteed and can become flaky for response comparisons.

Suggested diff
             FROM input_setting t_setting
             JOIN input t_input ON t_setting.input_id = t_input.id
             JOIN implementation t_impl ON t_input.implementation_id = t_impl.id
             WHERE t_setting.setup = :setup_id
+            ORDER BY t_input.id
             """,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/database/setups.py` around lines 28 - 42, The SQL query selecting
inputs/settings (the SELECT joining input_setting, input, implementation and
filtering by :setup_id) lacks a deterministic ORDER BY causing flaky parameter
ordering; update the query to append an ORDER BY clause (for example ORDER BY
t_input.implementation_id, t_input.name or ORDER BY t_impl.name, t_input.name)
so results are consistently sorted by flow and parameter name (refer to the
SELECT block, tables input_setting/input/implementation, and the :setup_id
filter).
tests/routers/openml/setups_test.py (1)

141-146: Strengthen the success-path assertions to lock the response contract.

Right now the test only checks setup_id and key presence. Adding a few schema-level assertions would catch more regressions.

Suggested diff
 async def test_get_setup_success(py_api: httpx.AsyncClient) -> None:
     response = await py_api.get("/setup/1")
     assert response.status_code == HTTPStatus.OK
     data = response.json()["setup_parameters"]
     assert data["setup_id"] == "1"
-    assert "parameter" in data
+    assert "flow_id" in data
+    assert isinstance(data.get("parameter"), list)
+    assert data["parameter"], "Expected setup 1 to contain at least one parameter"
+    first = data["parameter"][0]
+    assert {"id", "flow_id", "flow_name", "full_name", "parameter_name", "name", "value"} <= set(first)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/routers/openml/setups_test.py` around lines 141 - 146, The
test_get_setup_success should lock the response contract: assert that the
top-level response is a dict and contains only the expected keys for the setup
payload (verify set(data.keys()) == {'setup_id', 'parameter'}), assert
type(data['setup_id']) is str and equals "1", assert that data['parameter'] is
the expected container type (dict or list) and is non-empty, and if it's a list
assert each item is a dict with at least 'name' and 'value' keys (if it's a dict
assert it contains 'name' and 'value'); update the test_get_setup_success
assertions accordingly to enforce these schema-level checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/database/setups.py`:
- Around line 28-42: The SQL query selecting inputs/settings (the SELECT joining
input_setting, input, implementation and filtering by :setup_id) lacks a
deterministic ORDER BY causing flaky parameter ordering; update the query to
append an ORDER BY clause (for example ORDER BY t_input.implementation_id,
t_input.name or ORDER BY t_impl.name, t_input.name) so results are consistently
sorted by flow and parameter name (refer to the SELECT block, tables
input_setting/input/implementation, and the :setup_id filter).

In `@tests/routers/openml/setups_test.py`:
- Around line 141-146: The test_get_setup_success should lock the response
contract: assert that the top-level response is a dict and contains only the
expected keys for the setup payload (verify set(data.keys()) == {'setup_id',
'parameter'}), assert type(data['setup_id']) is str and equals "1", assert that
data['parameter'] is the expected container type (dict or list) and is
non-empty, and if it's a list assert each item is a dict with at least 'name'
and 'value' keys (if it's a dict assert it contains 'name' and 'value'); update
the test_get_setup_success assertions accordingly to enforce these schema-level
checks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 33a4cdd7-cfcd-4b24-9a38-8284b29f797c

📥 Commits

Reviewing files that changed from the base of the PR and between 5357b01 and fb34197.

📒 Files selected for processing (5)
  • src/database/setups.py
  • src/routers/openml/setups.py
  • src/schemas/setups.py
  • tests/routers/openml/migration/setups_migration_test.py
  • tests/routers/openml/setups_test.py

@igennova
Copy link
Copy Markdown
Contributor Author

@PGijsbers I intentionally built explicit BaseModel schemas under src/schemas/setups.py instead of directly throwing a dictionary out. This way, the API response is cleanly typed, strongly validated.

The PR is ready for review. Thank you

@igennova
Copy link
Copy Markdown
Contributor Author

@PGijsbers Gentle ping on the PR, Thanks!

Copy link
Copy Markdown
Contributor

@PGijsbers PGijsbers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution! Looks pretty good already, we can merge this after some minor changes :)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/routers/openml/migration/setups_migration_test.py (1)

303-305: ⚠️ Potential issue | 🟡 Minor

Validate error payload in the precondition early-return path too.

Line 303-305 currently checks only status before returning. Add code/detail assertions there as well to preserve parity guarantees for missing setups in this parameterized run.

Proposed fix
     if original.status_code == HTTPStatus.PRECONDITION_FAILED:
         assert new.status_code == HTTPStatus.NOT_FOUND
+        assert original.json()["error"]["code"] == new.json()["code"]
+        assert new.json()["detail"] == f"Setup {setup_id} not found."
         return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/routers/openml/migration/setups_migration_test.py` around lines 303 -
305, The early-return branch that handles original.status_code ==
HTTPStatus.PRECONDITION_FAILED only asserts status codes; add assertions to
validate the error payloads as well by parsing both responses (use
original.json() and new.json()) and asserting the relevant error fields (e.g.,
error code/message/details or the exact payload) match or satisfy the same
expectations used elsewhere in the test so parity is preserved for the
PRECONDITION_FAILED -> NOT_FOUND path; update the block guarded by original and
new and HTTPStatus.PRECONDITION_FAILED/HTTPStatus.NOT_FOUND accordingly.
🧹 Nitpick comments (1)
tests/routers/openml/setups_test.py (1)

141-146: Strengthen success-path assertions to lock the response contract.

Line 145-146 currently checks only setup_id and key presence. This can miss regressions in field types/shape (for example, int-vs-string changes in nested items).

Proposed test hardening
 async def test_get_setup_success(py_api: httpx.AsyncClient) -> None:
     response = await py_api.get("/setup/1")
     assert response.status_code == HTTPStatus.OK
     data = response.json()["setup_parameters"]
     assert data["setup_id"] == 1
-    assert "parameter" in data
+    assert isinstance(data["flow_id"], int)
+    assert "parameter" in data
+    assert isinstance(data["parameter"], list)
+    assert len(data["parameter"]) > 0
+    first_param = data["parameter"][0]
+    assert isinstance(first_param["id"], int)
+    assert isinstance(first_param["flow_id"], int)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/routers/openml/setups_test.py` around lines 141 - 146, The
test_get_setup_success success-path assertions are too loose; after obtaining
data = response.json()["setup_parameters"] update the test to lock the response
contract by asserting exact schema and types: assert that data is a dict, assert
isinstance(data["setup_id"], int) and equals 1, assert that "parameter" exists
and has the expected type (e.g., list or dict) and structure (check expected
keys/length and types of nested items), and assert any other required
fields/values returned by the endpoint so the test fails on type/shape
regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/routers/openml/migration/setups_migration_test.py`:
- Around line 317-327: The test should treat an empty "parameter" list the same
as a missing field before doing deep equality: in the block that inspects
setup_params (the "if 'parameter' in setup_params:" branch), if
setup_params["parameter"] is an empty list remove the "parameter" key (del
setup_params["parameter"]); otherwise continue converting p["id"]/p["flow_id"]
to int and deleting empty-list fields (p["data_type"], p["default_value"],
p["value"]) as currently done so comparisons won't fail when PHP returns [] but
Python omits the field.

---

Duplicate comments:
In `@tests/routers/openml/migration/setups_migration_test.py`:
- Around line 303-305: The early-return branch that handles original.status_code
== HTTPStatus.PRECONDITION_FAILED only asserts status codes; add assertions to
validate the error payloads as well by parsing both responses (use
original.json() and new.json()) and asserting the relevant error fields (e.g.,
error code/message/details or the exact payload) match or satisfy the same
expectations used elsewhere in the test so parity is preserved for the
PRECONDITION_FAILED -> NOT_FOUND path; update the block guarded by original and
new and HTTPStatus.PRECONDITION_FAILED/HTTPStatus.NOT_FOUND accordingly.

---

Nitpick comments:
In `@tests/routers/openml/setups_test.py`:
- Around line 141-146: The test_get_setup_success success-path assertions are
too loose; after obtaining data = response.json()["setup_parameters"] update the
test to lock the response contract by asserting exact schema and types: assert
that data is a dict, assert isinstance(data["setup_id"], int) and equals 1,
assert that "parameter" exists and has the expected type (e.g., list or dict)
and structure (check expected keys/length and types of nested items), and assert
any other required fields/values returned by the endpoint so the test fails on
type/shape regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a263b39f-cad0-4830-be47-6c9058e8c0f3

📥 Commits

Reviewing files that changed from the base of the PR and between 264043d and 0967c80.

📒 Files selected for processing (6)
  • docs/migration.md
  • src/database/setups.py
  • src/routers/openml/setups.py
  • src/schemas/setups.py
  • tests/routers/openml/migration/setups_migration_test.py
  • tests/routers/openml/setups_test.py
✅ Files skipped from review due to trivial changes (1)
  • docs/migration.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/database/setups.py
  • src/routers/openml/setups.py
  • src/schemas/setups.py

@igennova igennova requested a review from PGijsbers March 26, 2026 13:35
@igennova
Copy link
Copy Markdown
Contributor Author

@PGijsbers You were right! Expanding the test to range(1, 125) caught edge cases where legacy PHP encoded missing parameter fields as empty arrays [].

Using response_model_exclude_none=True, Python correctly omits these fields entirely. I've updated the tests to handle this legacy cleanup so the equality checks now pass across all 124 setups. This is ready for another review

@PGijsbers
Copy link
Copy Markdown
Contributor

Thanks! Minor remark to improve code reusability for (likely) common operations. Otherwise changes are good.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/conversions.py`:
- Around line 49-55: The dict comprehension in nested_remove_nones repeatedly
calls nested_remove_nones(val, ...) multiple times and thus reprocesses the same
value and misses cases where recursion turns a value into [] when
remove_empty_list=True; fix by normalizing each value exactly once: iterate over
obj.items() (or use a comprehension that assigns a temp like normalized =
nested_remove_nones(val,...)), use that normalized result for the None check,
empty-list check, and as the dict value, and ensure remove_empty_list behavior
checks the normalized value (not the original) before including the key; locate
and update the comprehension in nested_remove_nones to compute and reuse the
normalized result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9b91d048-1fc0-40f0-a2f7-91ea1433ec7a

📥 Commits

Reviewing files that changed from the base of the PR and between 0967c80 and 61761f7.

📒 Files selected for processing (2)
  • src/core/conversions.py
  • tests/routers/openml/migration/setups_migration_test.py

@igennova igennova requested a review from PGijsbers March 26, 2026 19:19
@igennova
Copy link
Copy Markdown
Contributor Author

@PGijsbers During testing, I found nested_str_to_num was aggressively converting legitimate configuration strings like "NaN" into float math.nan (which breaks dictionary equality because nan != nan). I updated core/conversions.py to preserve those specific edge-case strings.

@PGijsbers
Copy link
Copy Markdown
Contributor

I made a small change in the way the function was changed to accommodate removal of empty lists, but we're good now. Thanks!

@PGijsbers PGijsbers merged commit e2fc422 into openml:main Mar 27, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GET /setup/{id}

2 participants