Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ permissions:
jobs:
tests:
name: "${{ matrix.php_api == true && 'Migration' || 'Python-only' }} ${{ matrix.mutations == true && 'with mutations' || 'read-only' }}"
env:
TEST_MARKER: "${{ matrix.php_api == true && 'php_api' || 'not php_api' }} and ${{ matrix.mutations == true && 'mut' || 'not mut' }}"
strategy:
matrix:
php_api: [true, false]
Expand All @@ -34,10 +36,13 @@ jobs:
fi
docker compose $profiles up --detach --wait --remove-orphans || exit $(docker compose ps -q | xargs docker inspect -f '{{.State.ExitCode}}' | grep -v '^0' | wc -l)

- name: Run tests
- name: Run internal tests
run: |
marker="${{ matrix.php_api == true && 'php_api' || 'not php_api' }} and ${{ matrix.mutations == true && 'mut' || 'not mut' }}"
docker exec openml-python-rest-api coverage run -m pytest -n auto -v -m "$marker"
docker exec openml-python-rest-api coverage run -m pytest tests/database tests/config_test.py -n auto -v -m "$TEST_MARKER"
- name: Run API tests
if: always()
run: |
docker exec openml-python-rest-api coverage run -a -m pytest tests/routers -n auto -v -m "$TEST_MARKER"
- name: Produce coverage report
run: docker exec openml-python-rest-api coverage xml
- name: Upload results to Codecov
Expand Down
23 changes: 18 additions & 5 deletions src/routers/openml/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
import database.flows
from core.conversions import _str_to_num
from routers.dependencies import expdb_connection
from schemas.flows import Flow, Parameter, Subflow
from schemas.flows import Flow, FlowExistsBody, Parameter, Subflow

router = APIRouter(prefix="/flows", tags=["flows"])


@router.get("/exists/{name}/{external_version}")
@router.post("/exists")
def flow_exists(
name: str,
external_version: str,
body: FlowExistsBody,
expdb: Annotated[Connection, Depends(expdb_connection)],
) -> dict[Literal["flow_id"], int]:
"""Check if a Flow with the name and version exists, if so, return the flow id."""
flow = database.flows.get_by_name(name=name, external_version=external_version, expdb=expdb)
flow = database.flows.get_by_name(
name=body.name,
external_version=body.external_version,
expdb=expdb,
)
if flow is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
Expand All @@ -28,6 +31,16 @@ def flow_exists(
return {"flow_id": flow.id}


@router.get("/exists/{name}/{external_version}", deprecated=True)
def flow_exists_get(
name: str,
external_version: str,
expdb: Annotated[Connection, Depends(expdb_connection)],
) -> dict[Literal["flow_id"], int]:
"""Deprecated: use POST /flows/exists instead."""
return flow_exists(FlowExistsBody(name=name, external_version=external_version), expdb)
Comment on lines +34 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Please add coverage for the deprecated shim.

flow_exists_get is now part of the public compatibility surface, but none of the updated tests exercise GET /flows/exists/{name}/{external_version}. A small smoke test would keep that wrapper from silently drifting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routers/openml/flows.py` around lines 34 - 41, Add a small smoke test
that exercises the deprecated GET shim function flow_exists_get by calling the
route GET /flows/exists/{name}/{external_version} via the test client and
asserting it returns the same structure/value as the POST /flows/exists helper
(or directly asserts {"flow_id": <expected>}), to prevent regressions; locate
the test near other flows tests, create or reuse a fixture to ensure the flow
exists (or use an existing FlowExistsBody/flow creation helper), call
client.get(f"/flows/exists/{name}/{external_version}"), and assert status code
200 and the JSON payload matches the expected flow_id.



@router.get("/{flow_id}")
def get_flow(flow_id: int, expdb: Annotated[Connection, Depends(expdb_connection)] = None) -> Flow:
flow = database.flows.get(flow_id, expdb)
Expand Down
5 changes: 5 additions & 0 deletions src/schemas/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from pydantic import BaseModel, ConfigDict, Field


class FlowExistsBody(BaseModel):
name: str = Field(max_length=1024)
external_version: str = Field(max_length=128)


class Parameter(BaseModel):
name: str
default_value: Any
Expand Down
40 changes: 34 additions & 6 deletions tests/routers/openml/flows_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import pytest
from fastapi import HTTPException
from pytest_mock import MockerFixture
from sqlalchemy import Connection
from sqlalchemy import Connection, text
from starlette.testclient import TestClient

from routers.openml.flows import flow_exists
from schemas.flows import FlowExistsBody
from tests.conftest import Flow


Expand All @@ -25,7 +26,7 @@ def test_flow_exists_calls_db_correctly(
mocker: MockerFixture,
) -> None:
mocked_db = mocker.patch("database.flows.get_by_name")
flow_exists(name, external_version, expdb_test)
flow_exists(FlowExistsBody(name=name, external_version=external_version), expdb_test)
mocked_db.assert_called_once_with(
name=name,
external_version=external_version,
Expand All @@ -47,30 +48,57 @@ def test_flow_exists_processes_found(
"database.flows.get_by_name",
return_value=fake_flow,
)
response = flow_exists("name", "external_version", expdb_test)
response = flow_exists(
FlowExistsBody(name="name", external_version="external_version"), expdb_test
)
assert response == {"flow_id": fake_flow.id}


def test_flow_exists_handles_flow_not_found(mocker: MockerFixture, expdb_test: Connection) -> None:
mocker.patch("database.flows.get_by_name", return_value=None)
with pytest.raises(HTTPException) as error:
flow_exists("foo", "bar", expdb_test)
flow_exists(FlowExistsBody(name="foo", external_version="bar"), expdb_test)
assert error.value.status_code == HTTPStatus.NOT_FOUND
assert error.value.detail == "Flow not found."


def test_flow_exists(flow: Flow, py_api: TestClient) -> None:
response = py_api.get(f"/flows/exists/{flow.name}/{flow.external_version}")
response = py_api.post(
"/flows/exists", json={"name": flow.name, "external_version": flow.external_version}
)
assert response.status_code == HTTPStatus.OK
assert response.json() == {"flow_id": flow.id}


def test_flow_exists_not_exists(py_api: TestClient) -> None:
response = py_api.get("/flows/exists/foo/bar")
response = py_api.post("/flows/exists", json={"name": "foo", "external_version": "bar"})
assert response.status_code == HTTPStatus.NOT_FOUND
assert response.json()["detail"] == "Flow not found."


def test_flow_exists_get_deprecated(flow: Flow, py_api: TestClient) -> None:
response = py_api.get(f"/flows/exists/{flow.name}/{flow.external_version}")
assert response.status_code == HTTPStatus.OK
assert response.json() == {"flow_id": flow.id}


def test_flow_exists_uri_unsafe(expdb_test: Connection, py_api: TestClient) -> None:
expdb_test.execute(
text(
"""
INSERT INTO implementation(fullname,name,version,external_version,uploadDate)
VALUES ('weka/ZeroR','weka/ZeroR',2,'1.0/beta','2024-02-02 02:23:23');
""",
),
)
(flow_id,) = expdb_test.execute(text("""SELECT LAST_INSERT_ID();""")).one()
response = py_api.post(
"/flows/exists", json={"name": "weka/ZeroR", "external_version": "1.0/beta"}
)
assert response.status_code == HTTPStatus.OK
assert response.json() == {"flow_id": flow_id}


def test_get_flow_no_subflow(py_api: TestClient) -> None:
response = py_api.get("/flows/1")
assert response.status_code == HTTPStatus.OK
Expand Down
15 changes: 9 additions & 6 deletions tests/routers/openml/migration/flows_migration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ def test_flow_exists_not(
py_api: TestClient,
php_api: TestClient,
) -> None:
path = "exists/foo/bar"
py_response = py_api.get(f"/flows/{path}")
php_response = php_api.get(f"/flow/{path}")
py_response = py_api.post("/flows/exists", json={"name": "foo", "external_version": "bar"})
php_response = php_api.get("/flow/exists/foo/bar")

assert py_response.status_code == HTTPStatus.NOT_FOUND
assert php_response.status_code == HTTPStatus.OK
Expand All @@ -36,9 +35,13 @@ def test_flow_exists(
py_api: TestClient,
php_api: TestClient,
) -> None:
path = f"exists/{persisted_flow.name}/{persisted_flow.external_version}"
py_response = py_api.get(f"/flows/{path}")
php_response = php_api.get(f"/flow/{path}")
py_response = py_api.post(
"/flows/exists",
json={"name": persisted_flow.name, "external_version": persisted_flow.external_version},
)
php_response = php_api.get(
f"/flow/exists/{persisted_flow.name}/{persisted_flow.external_version}"
)

assert py_response.status_code == php_response.status_code, php_response.content

Expand Down