From 2efb7570d7a8839ad251c9b5da473d87e76a5c1b Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Jul 2026 11:13:52 +0200
Subject: [PATCH 1/6] Start using ORM for SetupTag
---
src/database/schema/tags.py | 11 ++++++++
src/database/setups.py | 35 ++++++++-----------------
src/database/tasks.py | 2 +-
src/routers/openml/setups.py | 15 ++++++-----
tests/routers/openml/setups_tag_test.py | 29 ++++++++------------
5 files changed, 42 insertions(+), 50 deletions(-)
diff --git a/src/database/schema/tags.py b/src/database/schema/tags.py
index a6b6a78..5fd687e 100644
--- a/src/database/schema/tags.py
+++ b/src/database/schema/tags.py
@@ -28,3 +28,14 @@ class TaskTag(ExpDBReflected, Tag, Base):
def task_id(self) -> Identifier:
"""Identifier of the task which is tagged by this tag."""
return self.entity_id
+
+
+class SetupTag(ExpDBReflected, Tag, Base):
+ """Tags belonging to a setup."""
+
+ __tablename__ = "setup_tag"
+
+ @property
+ def setup_id(self) -> Identifier:
+ """Identifier of the setup which is tagged by this tag."""
+ return self.entity_id
diff --git a/src/database/setups.py b/src/database/setups.py
index c78986c..1d2e636 100644
--- a/src/database/setups.py
+++ b/src/database/setups.py
@@ -1,8 +1,9 @@
"""All database operations that directly operate on setups."""
+from collections.abc import Sequence
from typing import TYPE_CHECKING
-from sqlalchemy import text
+from sqlalchemy import select, text
from sqlalchemy.exc import IntegrityError
from database.exceptions import (
@@ -12,11 +13,12 @@
ForeignKeyConstraintError,
)
from database.schema.base import UntypedRow
+from database.schema.tags import SetupTag
from routers.types import Identifier, TagString
if TYPE_CHECKING:
from sqlalchemy.engine import RowMapping
- from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
async def get(setup_id: Identifier, connection: AsyncConnection) -> UntypedRow | None:
@@ -61,19 +63,10 @@ async def get_parameters(setup_id: Identifier, connection: AsyncConnection) -> l
return list(rows.mappings().all())
-async def get_tags(setup_id: Identifier, connection: AsyncConnection) -> list[UntypedRow]:
+async def get_tags(setup_id: Identifier, session: AsyncSession) -> Sequence[SetupTag]:
"""Get all tags for setup with `setup_id` from the database."""
- rows = await connection.execute(
- text(
- """
- SELECT *
- FROM setup_tag
- WHERE id = :setup_id
- """,
- ),
- parameters={"setup_id": setup_id},
- )
- return list(rows.all())
+ stmt = select(SetupTag).where(SetupTag.entity_id == setup_id)
+ return (await session.scalars(stmt)).all()
async def untag(setup_id: Identifier, tag: TagString, connection: AsyncConnection) -> None:
@@ -93,19 +86,13 @@ async def tag(
setup_id: Identifier,
tag: TagString,
user_id: Identifier,
- connection: AsyncConnection,
+ session: AsyncSession,
) -> None:
"""Add tag `tag` to setup with id `setup_id`."""
+ tag_ = SetupTag(entity_id=setup_id, tag=tag, uploader_id=user_id)
try:
- await connection.execute(
- text(
- """
- INSERT INTO setup_tag (id, tag, uploader)
- VALUES (:setup_id, :tag, :user_id)
- """,
- ),
- parameters={"setup_id": setup_id, "tag": tag, "user_id": user_id},
- )
+ session.add(tag_)
+ await session.flush()
except IntegrityError as e:
if e.orig is None:
raise
diff --git a/src/database/tasks.py b/src/database/tasks.py
index 9d1d120..ae8aaa0 100644
--- a/src/database/tasks.py
+++ b/src/database/tasks.py
@@ -161,8 +161,8 @@ async def tag(
user_id: Identifier,
session: AsyncSession,
) -> None:
+ tag = TaskTag(entity_id=id_, uploader_id=user_id, tag=tag_)
try:
- tag = TaskTag(entity_id=id_, uploader_id=user_id, tag=tag_)
session.add(tag)
await session.flush()
except IntegrityError as e:
diff --git a/src/routers/openml/setups.py b/src/routers/openml/setups.py
index 34b28e0..4638ba3 100644
--- a/src/routers/openml/setups.py
+++ b/src/routers/openml/setups.py
@@ -15,12 +15,12 @@
)
from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from database.users import User
-from routers.dependencies import expdb_connection, fetch_user_or_raise
+from routers.dependencies import expdb_connection, expdb_session, fetch_user_or_raise
from routers.types import Identifier, TagString
from schemas.setups import SetupParameters, SetupResponse
if TYPE_CHECKING:
- from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
router = APIRouter(prefix="/setup", tags=["setup"])
@@ -52,11 +52,11 @@ async def tag_setup(
setup_id: Annotated[Identifier, Body()],
tag: Annotated[TagString, Body()],
user: Annotated[User, Depends(fetch_user_or_raise)],
- expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
+ expdb_session: Annotated[AsyncSession, Depends(expdb_session)],
) -> dict[str, dict[str, str | list[str]]]:
"""Add tag `tag` to setup with id `setup_id`."""
try:
- await database.setups.tag(setup_id, tag, user.user_id, expdb_db)
+ await database.setups.tag(setup_id, tag, user.user_id, expdb_session)
except ForeignKeyConstraintError:
msg = f"Setup {setup_id} not found."
raise SetupNotFoundError(msg, code=472) from None
@@ -65,7 +65,7 @@ async def tag_setup(
raise TagAlreadyExistsError(msg) from None
logger.info("Setup {setup_id} tagged '{tag}'.", setup_id=setup_id, tag=tag)
- all_tag_rows = await database.setups.get_tags(setup_id, expdb_db)
+ all_tag_rows = await database.setups.get_tags(setup_id, expdb_session)
all_tags = [t.tag for t in all_tag_rows]
return {"setup_tag": {"id": str(setup_id), "tag": all_tags}}
@@ -77,11 +77,12 @@ async def untag_setup(
tag: Annotated[TagString, Body()],
user: Annotated[User, Depends(fetch_user_or_raise)],
expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
+ expdb_session: Annotated[AsyncSession, Depends(expdb_session)],
) -> dict[str, dict[str, str | list[str]]]:
"""Remove tag `tag` from setup with id `setup_id`."""
setup, setup_tags = await asyncio.gather(
database.setups.get(setup_id, expdb_db),
- database.setups.get_tags(setup_id, expdb_db),
+ database.setups.get_tags(setup_id, expdb_session),
)
if not setup:
msg = f"Setup {setup_id} not found."
@@ -92,7 +93,7 @@ async def untag_setup(
msg = f"Setup {setup_id} does not have tag {tag!r}."
raise TagNotFoundError(msg)
- if matched_tag_row.uploader != user.user_id and not await user.is_admin():
+ if matched_tag_row.uploader_id != user.user_id and not await user.is_admin():
msg = (
f"You may not remove tag {tag!r} of setup {setup_id} because it was not created by you."
)
diff --git a/tests/routers/openml/setups_tag_test.py b/tests/routers/openml/setups_tag_test.py
index 32c85ef..60488c2 100644
--- a/tests/routers/openml/setups_tag_test.py
+++ b/tests/routers/openml/setups_tag_test.py
@@ -14,7 +14,7 @@
if TYPE_CHECKING:
import httpx
- from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
async def test_setup_tag_missing_auth(py_api: httpx.AsyncClient) -> None:
@@ -48,46 +48,39 @@ async def test_setup_tag_api_success(
# ── Direct call tests: tag_setup ──
-async def test_setup_tag_unknown_setup(expdb_test: AsyncConnection) -> None:
+async def test_setup_tag_unknown_setup(expdb_session: AsyncSession) -> None:
with pytest.raises(SetupNotFoundError, match=r"Setup \d+ not found."):
await tag_setup(
setup_id=999999,
tag="test_tag",
user=SOME_USER,
- expdb_db=expdb_test,
+ expdb_session=expdb_session,
)
@pytest.mark.mut
-async def test_setup_tag_already_exists(expdb_test: AsyncConnection) -> None:
+async def test_setup_tag_already_exists(expdb_session: AsyncSession) -> None:
tag = "setup_tag_conflict"
- await expdb_test.execute(
- text("INSERT INTO setup_tag (id, tag, uploader) VALUES (1, :tag, 2);"),
- parameters={"tag": tag},
- )
+ await tag_setup(setup_id=1, tag=tag, user=SOME_USER, expdb_session=expdb_session)
+
with pytest.raises(TagAlreadyExistsError, match=rf"Setup 1 already tagged with '{tag}'\."):
- await tag_setup(
- setup_id=1,
- tag=tag,
- user=SOME_USER,
- expdb_db=expdb_test,
- )
+ await tag_setup(setup_id=1, tag=tag, user=SOME_USER, expdb_session=expdb_session)
@pytest.mark.mut
-async def test_setup_tag_direct_success(expdb_test: AsyncConnection) -> None:
+async def test_setup_tag_direct_success(expdb_session: AsyncSession) -> None:
tag = "setup_tag_via_direct"
result = await tag_setup(
setup_id=1,
tag=tag,
user=SOME_USER,
- expdb_db=expdb_test,
+ expdb_session=expdb_session,
)
assert result["setup_tag"]["tag"][-1] == tag
- rows = await expdb_test.execute(
+ rows = await expdb_session.execute(
text("SELECT * FROM setup_tag WHERE id = 1 AND tag = :tag"),
- parameters={"tag": tag},
+ params={"tag": tag},
)
assert len(rows.all()) == 1
From 29388b909158d792a5086c1766e85a210e2b32e2 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Thu, 2 Jul 2026 14:41:46 +0200
Subject: [PATCH 2/6] Do not automatically force a commit at the session layer
---
src/routers/dependencies.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routers/dependencies.py b/src/routers/dependencies.py
index 2ee8548..87ea538 100644
--- a/src/routers/dependencies.py
+++ b/src/routers/dependencies.py
@@ -29,7 +29,7 @@ async def userdb_connection() -> AsyncIterator[AsyncConnection]:
async def expdb_session(
connection: Annotated[AsyncConnection, Depends(expdb_connection)],
) -> AsyncIterator[AsyncSession]:
- async with AsyncSession(connection) as session, session.begin():
+ async with AsyncSession(connection) as session:
yield session
From ec699866968746f8eda7e7a10de370f7385f79e9 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Tue, 7 Jul 2026 13:29:06 +0200
Subject: [PATCH 3/6] Start supporting ORM for SetupTag untag
---
src/database/setups.py | 14 +++-------
src/routers/openml/setups.py | 2 +-
tests/routers/openml/setups_untag_test.py | 31 +++++++++++++++--------
3 files changed, 26 insertions(+), 21 deletions(-)
diff --git a/src/database/setups.py b/src/database/setups.py
index 1d2e636..19c5da7 100644
--- a/src/database/setups.py
+++ b/src/database/setups.py
@@ -3,7 +3,7 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING
-from sqlalchemy import select, text
+from sqlalchemy import delete, select, text
from sqlalchemy.exc import IntegrityError
from database.exceptions import (
@@ -69,16 +69,10 @@ async def get_tags(setup_id: Identifier, session: AsyncSession) -> Sequence[Setu
return (await session.scalars(stmt)).all()
-async def untag(setup_id: Identifier, tag: TagString, connection: AsyncConnection) -> None:
+async def untag(setup_id: Identifier, tag: TagString, session: AsyncSession) -> None:
"""Remove tag `tag` from setup with id `setup_id`."""
- await connection.execute(
- text(
- """
- DELETE FROM setup_tag
- WHERE id = :setup_id AND tag = :tag
- """,
- ),
- parameters={"setup_id": setup_id, "tag": tag},
+ await session.execute(
+ delete(SetupTag).where(SetupTag.entity_id == setup_id and SetupTag.tag == tag),
)
diff --git a/src/routers/openml/setups.py b/src/routers/openml/setups.py
index 4638ba3..968bc3f 100644
--- a/src/routers/openml/setups.py
+++ b/src/routers/openml/setups.py
@@ -104,7 +104,7 @@ async def untag_setup(
)
raise TagNotOwnedError(msg)
- await database.setups.untag(setup_id, matched_tag_row.tag, expdb_db)
+ await database.setups.untag(setup_id, matched_tag_row.tag, expdb_session)
logger.info("Setup {setup_id} had tag '{tag}' removed.", setup_id=setup_id, tag=tag)
remaining_tags = [
t.tag for t in setup_tags if t.tag.casefold() != matched_tag_row.tag.casefold()
diff --git a/tests/routers/openml/setups_untag_test.py b/tests/routers/openml/setups_untag_test.py
index 1491fd1..79cf8ef 100644
--- a/tests/routers/openml/setups_untag_test.py
+++ b/tests/routers/openml/setups_untag_test.py
@@ -14,7 +14,7 @@
if TYPE_CHECKING:
import httpx
- from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
async def test_setup_untag_missing_auth(py_api: httpx.AsyncClient) -> None:
@@ -53,17 +53,22 @@ async def test_setup_untag_api_success(
# ── Direct call tests: untag_setup ──
-async def test_setup_untag_unknown_setup(expdb_test: AsyncConnection) -> None:
+async def test_setup_untag_unknown_setup(
+ expdb_test: AsyncConnection, expdb_session: AsyncSession
+) -> None:
with pytest.raises(SetupNotFoundError, match=r"Setup \d+ not found."):
await untag_setup(
setup_id=999999,
tag="test_tag",
user=SOME_USER,
expdb_db=expdb_test,
+ expdb_session=expdb_session,
)
-async def test_setup_untag_tag_not_found(expdb_test: AsyncConnection) -> None:
+async def test_setup_untag_tag_not_found(
+ expdb_test: AsyncConnection, expdb_session: AsyncSession
+) -> None:
tag = "non_existent_tag_12345"
with pytest.raises(TagNotFoundError, match=rf"Setup 1 does not have tag '{tag}'\."):
await untag_setup(
@@ -71,15 +76,18 @@ async def test_setup_untag_tag_not_found(expdb_test: AsyncConnection) -> None:
tag=tag,
user=SOME_USER,
expdb_db=expdb_test,
+ expdb_session=expdb_session,
)
@pytest.mark.mut
-async def test_setup_untag_not_owned_by_you(expdb_test: AsyncConnection) -> None:
+async def test_setup_untag_not_owned_by_you(
+ expdb_test: AsyncConnection, expdb_session: AsyncSession
+) -> None:
tag = "setup_untag_forbidden"
- await expdb_test.execute(
+ await expdb_session.execute(
text("INSERT INTO setup_tag (id, tag, uploader) VALUES (1, :tag, 2);"),
- parameters={"tag": tag},
+ params={"tag": tag},
)
with pytest.raises(
TagNotOwnedError,
@@ -90,6 +98,7 @@ async def test_setup_untag_not_owned_by_you(expdb_test: AsyncConnection) -> None
tag=tag,
user=OWNER_USER,
expdb_db=expdb_test,
+ expdb_session=expdb_session,
)
rows = await expdb_test.execute(
text("SELECT * FROM setup_tag WHERE id = 1 AND tag = :tag"),
@@ -101,12 +110,13 @@ async def test_setup_untag_not_owned_by_you(expdb_test: AsyncConnection) -> None
@pytest.mark.mut
async def test_setup_untag_admin_removes_tag_uploaded_by_another_user(
expdb_test: AsyncConnection,
+ expdb_session: AsyncSession,
) -> None:
"""Administrator can remove a tag uploaded by another user."""
tag = "setup_untag_via_direct"
- await expdb_test.execute(
+ await expdb_session.execute(
text("INSERT INTO setup_tag (id, tag, uploader) VALUES (1, :tag, 2);"),
- parameters={"tag": tag},
+ params={"tag": tag},
)
result = await untag_setup(
@@ -114,13 +124,14 @@ async def test_setup_untag_admin_removes_tag_uploaded_by_another_user(
tag=tag,
user=ADMIN_USER,
expdb_db=expdb_test,
+ expdb_session=expdb_session,
)
assert result == {"setup_untag": {"id": "1", "tag": []}}
- rows = await expdb_test.execute(
+ rows = await expdb_session.execute(
text("SELECT * FROM setup_tag WHERE id = 1 AND tag = :tag"),
- parameters={"tag": tag},
+ params={"tag": tag},
)
assert len(rows.all()) == 0
From b4a27cd0e64186eb6005f5c74890b989a29006ac Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Tue, 7 Jul 2026 13:45:22 +0200
Subject: [PATCH 4/6] Continue using the ORM layer more for untag
---
src/database/setups.py | 15 +++++++++------
src/routers/openml/setups.py | 27 +++++++++++----------------
2 files changed, 20 insertions(+), 22 deletions(-)
diff --git a/src/database/setups.py b/src/database/setups.py
index 19c5da7..ebbc1f2 100644
--- a/src/database/setups.py
+++ b/src/database/setups.py
@@ -3,7 +3,7 @@
from collections.abc import Sequence
from typing import TYPE_CHECKING
-from sqlalchemy import delete, select, text
+from sqlalchemy import select, text
from sqlalchemy.exc import IntegrityError
from database.exceptions import (
@@ -69,11 +69,14 @@ async def get_tags(setup_id: Identifier, session: AsyncSession) -> Sequence[Setu
return (await session.scalars(stmt)).all()
-async def untag(setup_id: Identifier, tag: TagString, session: AsyncSession) -> None:
- """Remove tag `tag` from setup with id `setup_id`."""
- await session.execute(
- delete(SetupTag).where(SetupTag.entity_id == setup_id and SetupTag.tag == tag),
- )
+async def get_tag(setup_id: Identifier, tag: TagString, session: AsyncSession) -> SetupTag | None:
+ """Get the tag `tag` for setup with id `setup_id`."""
+ return await session.get(SetupTag, {"tag": tag, "entity_id": setup_id})
+
+
+async def delete_tag(tag: SetupTag, session: AsyncSession) -> None:
+ """Delete a setup tag."""
+ await session.delete(tag)
async def tag(
diff --git a/src/routers/openml/setups.py b/src/routers/openml/setups.py
index 968bc3f..a9e5fe4 100644
--- a/src/routers/openml/setups.py
+++ b/src/routers/openml/setups.py
@@ -1,6 +1,5 @@
"""All endpoints that relate to setups."""
-import asyncio
from typing import TYPE_CHECKING, Annotated
from fastapi import APIRouter, Body, Depends, Path
@@ -80,20 +79,18 @@ async def untag_setup(
expdb_session: Annotated[AsyncSession, Depends(expdb_session)],
) -> dict[str, dict[str, str | list[str]]]:
"""Remove tag `tag` from setup with id `setup_id`."""
- setup, setup_tags = await asyncio.gather(
- database.setups.get(setup_id, expdb_db),
- database.setups.get_tags(setup_id, expdb_session),
- )
- if not setup:
- msg = f"Setup {setup_id} not found."
- raise SetupNotFoundError(msg)
- matched_tag_row = next((t for t in setup_tags if t.tag.casefold() == tag.casefold()), None)
-
- if not matched_tag_row:
+ # Setups don't really have an owner, they are associated with runs.
+ # So only the tagger or admins can remove the tag.
+ tag_orm = await database.setups.get_tag(setup_id, tag, expdb_session)
+ if not tag_orm:
+ setup = await database.setups.get(setup_id, expdb_db)
+ if not setup:
+ msg = f"Setup {setup_id} not found."
+ raise SetupNotFoundError(msg)
msg = f"Setup {setup_id} does not have tag {tag!r}."
raise TagNotFoundError(msg)
- if matched_tag_row.uploader_id != user.user_id and not await user.is_admin():
+ if tag_orm.uploader_id != user.user_id and not await user.is_admin():
msg = (
f"You may not remove tag {tag!r} of setup {setup_id} because it was not created by you."
)
@@ -104,9 +101,7 @@ async def untag_setup(
)
raise TagNotOwnedError(msg)
- await database.setups.untag(setup_id, matched_tag_row.tag, expdb_session)
+ await database.setups.delete_tag(tag_orm, expdb_session)
logger.info("Setup {setup_id} had tag '{tag}' removed.", setup_id=setup_id, tag=tag)
- remaining_tags = [
- t.tag for t in setup_tags if t.tag.casefold() != matched_tag_row.tag.casefold()
- ]
+ remaining_tags = [t.tag for t in await database.setups.get_tags(setup_id, expdb_session)]
return {"setup_untag": {"id": str(setup_id), "tag": remaining_tags}}
From 1a541eafad00953a70a36c3c0b1e17e34b52e515 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Tue, 7 Jul 2026 14:23:04 +0200
Subject: [PATCH 5/6] Add an ORM class for Setup
---
src/database/schema/setups.py | 16 +++++++++++++
src/database/setups.py | 16 +++----------
src/routers/openml/runs.py | 28 ++++++++++++-----------
src/routers/openml/setups.py | 8 +++----
tests/routers/openml/setups_untag_test.py | 21 ++++-------------
5 files changed, 43 insertions(+), 46 deletions(-)
create mode 100644 src/database/schema/setups.py
diff --git a/src/database/schema/setups.py b/src/database/schema/setups.py
new file mode 100644
index 0000000..5723d84
--- /dev/null
+++ b/src/database/schema/setups.py
@@ -0,0 +1,16 @@
+"""Setup ORM Model."""
+
+from sqlalchemy.orm import Mapped, mapped_column
+
+from database.schema.base import Base, ExpDBReflected
+from routers.types import Identifier
+
+
+class Setup(Base, ExpDBReflected):
+ """Specifies the hyperparameter configuration of a Flow used in a Run."""
+
+ __tablename__ = "algorithm_setup"
+ id: Mapped[Identifier] = mapped_column("sid", primary_key=True)
+ flow_id: Mapped[Identifier] = mapped_column("implementation_id")
+
+ setup_string: Mapped[str]
diff --git a/src/database/setups.py b/src/database/setups.py
index ebbc1f2..2bd20f8 100644
--- a/src/database/setups.py
+++ b/src/database/setups.py
@@ -12,7 +12,7 @@
DuplicatePrimaryKeyError,
ForeignKeyConstraintError,
)
-from database.schema.base import UntypedRow
+from database.schema.setups import Setup
from database.schema.tags import SetupTag
from routers.types import Identifier, TagString
@@ -21,19 +21,9 @@
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
-async def get(setup_id: Identifier, connection: AsyncConnection) -> UntypedRow | None:
+async def get(setup_id: Identifier, session: AsyncSession) -> Setup | None:
"""Get the setup with id `setup_id` from the database."""
- row = await connection.execute(
- text(
- """
- SELECT *
- FROM algorithm_setup
- WHERE sid = :setup_id
- """,
- ),
- parameters={"setup_id": setup_id},
- )
- return row.first()
+ return await session.get(Setup, setup_id)
async def get_parameters(setup_id: Identifier, connection: AsyncConnection) -> list[RowMapping]:
diff --git a/src/routers/openml/runs.py b/src/routers/openml/runs.py
index 88296cf..3869df8 100644
--- a/src/routers/openml/runs.py
+++ b/src/routers/openml/runs.py
@@ -14,7 +14,8 @@
import database.users
from core.errors import RunNotFoundError, RunTraceNotFoundError
from database.schema.base import UntypedRow
-from routers.dependencies import expdb_connection, userdb_connection
+from database.schema.setups import Setup
+from routers.dependencies import expdb_connection, expdb_session, userdb_connection
from routers.types import Identifier
from schemas.runs import (
EvaluationScore,
@@ -28,7 +29,7 @@
)
if TYPE_CHECKING:
- from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
router = APIRouter(prefix="/run", tags=["run"])
@@ -75,15 +76,15 @@ class RunContext:
evaluation_rows: list[UntypedRow]
task_type: str | None
task_evaluation_measure: str | None
- setup: UntypedRow | None
+ setup: Setup | None
parameter_rows: list[UntypedRow]
async def _load_run_context(
run: UntypedRow,
- run_id: int,
expdb: AsyncConnection,
userdb: AsyncConnection,
+ expdb_session: AsyncSession,
engine_ids: list[int],
) -> RunContext:
(
@@ -97,16 +98,16 @@ async def _load_run_context(
setup,
parameter_rows,
) = cast(
- "tuple[Any, list[str], list[UntypedRow], list[UntypedRow], list[UntypedRow], str | None, str | None, UntypedRow | None, list[UntypedRow]]", # noqa: E501
+ "tuple[Any, list[str], list[UntypedRow], list[UntypedRow], list[UntypedRow], str | None, str | None, Setup | None, list[UntypedRow]]", # noqa: E501
await asyncio.gather(
database.users.get_user(user_id=run.uploader, connection=userdb),
- database.runs.get_tags(run_id, expdb),
- database.runs.get_input_data(run_id, expdb),
- database.runs.get_output_files(run_id, expdb),
- database.runs.get_evaluations(run_id, expdb, evaluation_engine_ids=engine_ids),
+ database.runs.get_tags(run.rid, expdb),
+ database.runs.get_input_data(run.rid, expdb),
+ database.runs.get_output_files(run.rid, expdb),
+ database.runs.get_evaluations(run.rid, expdb, evaluation_engine_ids=engine_ids),
database.tasks.get_task_type_name(run.task_id, expdb),
database.tasks.get_task_evaluation_measure(run.task_id, expdb),
- database.setups.get(run.setup, expdb),
+ database.setups.get(run.setup, expdb_session),
database.setups.get_parameters(run.setup, expdb),
),
)
@@ -146,6 +147,7 @@ async def get_run(
run_id: int,
expdb: Annotated[AsyncConnection, Depends(expdb_connection)],
userdb: Annotated[AsyncConnection, Depends(userdb_connection)],
+ expdb_session: Annotated[AsyncSession, Depends(expdb_session)],
) -> Run:
"""Get full metadata for a run by ID.
@@ -158,9 +160,9 @@ async def get_run(
raise RunNotFoundError(msg, code=236)
engine_ids: list[int] = config.get_config().development.run_evaluation_engine_ids
- ctx = await _load_run_context(run, run_id, expdb, userdb, engine_ids)
+ ctx = await _load_run_context(run, expdb, userdb, expdb_session, engine_ids)
- flow = await database.flows.get(ctx.setup.implementation_id, expdb) if ctx.setup else None
+ flow = await database.flows.get(ctx.setup.flow_id, expdb) if ctx.setup else None
evaluations = _build_evaluations(ctx.evaluation_rows)
normalised_measure = ctx.task_evaluation_measure or None
@@ -173,7 +175,7 @@ async def get_run(
task_id=run.task_id,
task_type=ctx.task_type,
task_evaluation_measure=normalised_measure,
- flow_id=ctx.setup.implementation_id if ctx.setup else None,
+ flow_id=ctx.setup.flow_id if ctx.setup else None,
flow_name=flow.full_name if flow else None,
setup_id=run.setup,
setup_string=ctx.setup.setup_string if ctx.setup else None,
diff --git a/src/routers/openml/setups.py b/src/routers/openml/setups.py
index a9e5fe4..d7d8694 100644
--- a/src/routers/openml/setups.py
+++ b/src/routers/openml/setups.py
@@ -28,9 +28,10 @@
async def get_setup(
setup_id: Annotated[Identifier, Path()],
expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
+ expdb_session: Annotated[AsyncSession, Depends(expdb_session)],
) -> SetupResponse:
"""Get setup by id."""
- setup = await database.setups.get(setup_id, expdb_db)
+ setup = await database.setups.get(setup_id, expdb_session)
if not setup:
msg = f"Setup {setup_id} not found."
raise SetupNotFoundError(msg, code=281)
@@ -39,7 +40,7 @@ async def get_setup(
params_model = SetupParameters(
setup_id=setup_id,
- flow_id=setup.implementation_id,
+ flow_id=setup.flow_id,
parameter=setup_parameters or None,
)
@@ -75,7 +76,6 @@ async def untag_setup(
setup_id: Annotated[Identifier, Body()],
tag: Annotated[TagString, Body()],
user: Annotated[User, Depends(fetch_user_or_raise)],
- expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
expdb_session: Annotated[AsyncSession, Depends(expdb_session)],
) -> dict[str, dict[str, str | list[str]]]:
"""Remove tag `tag` from setup with id `setup_id`."""
@@ -83,7 +83,7 @@ async def untag_setup(
# So only the tagger or admins can remove the tag.
tag_orm = await database.setups.get_tag(setup_id, tag, expdb_session)
if not tag_orm:
- setup = await database.setups.get(setup_id, expdb_db)
+ setup = await database.setups.get(setup_id, expdb_session)
if not setup:
msg = f"Setup {setup_id} not found."
raise SetupNotFoundError(msg)
diff --git a/tests/routers/openml/setups_untag_test.py b/tests/routers/openml/setups_untag_test.py
index 79cf8ef..c4424f1 100644
--- a/tests/routers/openml/setups_untag_test.py
+++ b/tests/routers/openml/setups_untag_test.py
@@ -53,37 +53,29 @@ async def test_setup_untag_api_success(
# ── Direct call tests: untag_setup ──
-async def test_setup_untag_unknown_setup(
- expdb_test: AsyncConnection, expdb_session: AsyncSession
-) -> None:
+async def test_setup_untag_unknown_setup(expdb_session: AsyncSession) -> None:
with pytest.raises(SetupNotFoundError, match=r"Setup \d+ not found."):
await untag_setup(
setup_id=999999,
tag="test_tag",
user=SOME_USER,
- expdb_db=expdb_test,
expdb_session=expdb_session,
)
-async def test_setup_untag_tag_not_found(
- expdb_test: AsyncConnection, expdb_session: AsyncSession
-) -> None:
+async def test_setup_untag_tag_not_found(expdb_session: AsyncSession) -> None:
tag = "non_existent_tag_12345"
with pytest.raises(TagNotFoundError, match=rf"Setup 1 does not have tag '{tag}'\."):
await untag_setup(
setup_id=1,
tag=tag,
user=SOME_USER,
- expdb_db=expdb_test,
expdb_session=expdb_session,
)
@pytest.mark.mut
-async def test_setup_untag_not_owned_by_you(
- expdb_test: AsyncConnection, expdb_session: AsyncSession
-) -> None:
+async def test_setup_untag_not_owned_by_you(expdb_session: AsyncSession) -> None:
tag = "setup_untag_forbidden"
await expdb_session.execute(
text("INSERT INTO setup_tag (id, tag, uploader) VALUES (1, :tag, 2);"),
@@ -97,19 +89,17 @@ async def test_setup_untag_not_owned_by_you(
setup_id=1,
tag=tag,
user=OWNER_USER,
- expdb_db=expdb_test,
expdb_session=expdb_session,
)
- rows = await expdb_test.execute(
+ rows = await expdb_session.execute(
text("SELECT * FROM setup_tag WHERE id = 1 AND tag = :tag"),
- parameters={"tag": tag},
+ params={"tag": tag},
)
assert len(rows.all()) == 1
@pytest.mark.mut
async def test_setup_untag_admin_removes_tag_uploaded_by_another_user(
- expdb_test: AsyncConnection,
expdb_session: AsyncSession,
) -> None:
"""Administrator can remove a tag uploaded by another user."""
@@ -123,7 +113,6 @@ async def test_setup_untag_admin_removes_tag_uploaded_by_another_user(
setup_id=1,
tag=tag,
user=ADMIN_USER,
- expdb_db=expdb_test,
expdb_session=expdb_session,
)
From fd6fa4982bc79a7e08d9e37300f36ff88b15cbd5 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Tue, 7 Jul 2026 14:51:33 +0200
Subject: [PATCH 6/6] Make inheritance order consistent with other ORM classes
---
src/database/schema/setups.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/database/schema/setups.py b/src/database/schema/setups.py
index 5723d84..36f9669 100644
--- a/src/database/schema/setups.py
+++ b/src/database/schema/setups.py
@@ -6,7 +6,7 @@
from routers.types import Identifier
-class Setup(Base, ExpDBReflected):
+class Setup(ExpDBReflected, Base):
"""Specifies the hyperparameter configuration of a Flow used in a Run."""
__tablename__ = "algorithm_setup"