From 05de3a490ef72ef79158e8368cf4649ab99a1a2e Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 11:39:05 +0200
Subject: [PATCH 01/15] towards shared tagging logic
---
src/database/tags.py | 27 +++++++++++++++++++++++++
src/routers/openml/datasets.py | 36 +++++++++++++++++++++++++++-------
2 files changed, 56 insertions(+), 7 deletions(-)
create mode 100644 src/database/tags.py
diff --git a/src/database/tags.py b/src/database/tags.py
new file mode 100644
index 00000000..ac4ab291
--- /dev/null
+++ b/src/database/tags.py
@@ -0,0 +1,27 @@
+from typing import Callable
+from sqlalchemy.exc import IntegrityError
+
+_FOREIGN_KEY_CONSTRAINT_FAILED = 1452
+_DUPLICATE_ENTRY = 1062
+
+
+class ForeignKeyConstraintError(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+
+class DuplicatePrimaryKeyError(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+
+async def tag_entity(tag_function: Callable) -> None:
+ try:
+ await tag_function()
+ except IntegrityError as e:
+ code, msg = e.orig.args
+ if code == _FOREIGN_KEY_CONSTRAINT_FAILED:
+ raise ForeignKeyConstraintError(msg) from e
+ if code == _DUPLICATE_ENTRY:
+ raise DuplicatePrimaryKeyError(msg) from e
+ raise
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index 51d26d21..ed302021 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -1,5 +1,6 @@
import asyncio
import re
+from collections.abc import Callable
from datetime import datetime
from enum import StrEnum
from typing import Annotated, Any, Literal, NamedTuple
@@ -32,6 +33,7 @@
_format_dataset_url,
_format_parquet_url,
)
+from database.tags import DuplicatePrimaryKeyError, ForeignKeyConstraintError, tag_entity
from database.users import User
from routers.dependencies import (
Pagination,
@@ -46,6 +48,22 @@
router = APIRouter(prefix="/datasets", tags=["datasets"])
+async def tag_entity_as_api(
+ tag_function: Callable,
+ entity_name: str,
+ identifier: int,
+ tag: str,
+) -> None:
+ try:
+ await tag_entity(tag_function)
+ except ForeignKeyConstraintError:
+ msg = "Not under test yet."
+ raise DatasetNotFoundError(msg) from None
+ except DuplicatePrimaryKeyError:
+ msg = f"{entity_name} {identifier} already tagged with {tag!r}."
+ raise TagAlreadyExistsError(msg) from None
+
+
@router.post(
path="/tag",
)
@@ -53,18 +71,22 @@ async def tag_dataset(
data_id: Annotated[int, Body()],
tag: Annotated[str, SystemString64],
user: Annotated[User, Depends(fetch_user_or_raise)],
- expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)] = None,
+ expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
) -> dict[str, dict[str, Any]]:
- assert expdb_db is not None # noqa: S101
+ if not isinstance(expdb_db, AsyncConnection):
+ msg = "`expdb_db` should be an AsyncConnection, is {type(expdb_db)}"
+ raise TypeError(msg)
+
+ async def tag_dataset() -> None:
+ await database.datasets.tag(data_id, tag, user_id=user.user_id, connection=expdb_db)
+
+ await tag_entity_as_api(tag_dataset, "Dataset", data_id, tag)
+
tags = await database.datasets.get_tags_for(data_id, expdb_db)
- if tag.casefold() in [t.casefold() for t in tags]:
- msg = f"Dataset {data_id} already tagged with {tag!r}."
- raise TagAlreadyExistsError(msg)
- await database.datasets.tag(data_id, tag, user_id=user.user_id, connection=expdb_db)
logger.info("Dataset {dataset_id} tagged '{tag}'.", dataset_id=data_id, tag=tag)
return {
- "data_tag": {"id": str(data_id), "tag": [*tags, tag]},
+ "data_tag": {"id": str(data_id), "tag": tags},
}
From d3024993378803ba89dec08f80159c3d5803078b Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 12:15:30 +0200
Subject: [PATCH 02/15] Continue with shared tagging logic
---
src/routers/openml/datasets.py | 70 +++++++++++++++++++++++++---------
1 file changed, 52 insertions(+), 18 deletions(-)
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index ed302021..56037efe 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -1,12 +1,12 @@
import asyncio
import re
-from collections.abc import Callable
from datetime import datetime
from enum import StrEnum
-from typing import Annotated, Any, Literal, NamedTuple
+from typing import Annotated, Any, Literal, NamedTuple, Protocol
from fastapi import APIRouter, Body, Depends
from loguru import logger
+from pydantic import Field
from sqlalchemy import bindparam, text
from sqlalchemy.engine import Row
from sqlalchemy.ext.asyncio import AsyncConnection
@@ -47,28 +47,64 @@
router = APIRouter(prefix="/datasets", tags=["datasets"])
+Identifier = Annotated[int, Field(gt=0)]
+
+
+class TagFunction(Protocol):
+ def __call__(
+ self,
+ entity_identifier: Identifier,
+ tag: str,
+ *,
+ user_id: Identifier,
+ connection: AsyncConnection,
+ ) -> None: ...
+
+
+class GetTagsFunction(Protocol):
+ def __call__(self, entity_identifier: Identifier, connection: AsyncConnection) -> list[str]: ...
+
async def tag_entity_as_api(
- tag_function: Callable,
+ tag_function: TagFunction,
+ get_tags_function: GetTagsFunction,
entity_name: str,
- identifier: int,
+ user_identifier: Identifier,
+ entity_identifier: Identifier,
tag: str,
+ connection: AsyncConnection,
) -> None:
+ async def tagger() -> None:
+ await tag_function(entity_identifier, tag, user_id=user_identifier, connection=connection)
+
try:
- await tag_entity(tag_function)
+ await tag_entity(tagger)
except ForeignKeyConstraintError:
msg = "Not under test yet."
raise DatasetNotFoundError(msg) from None
except DuplicatePrimaryKeyError:
- msg = f"{entity_name} {identifier} already tagged with {tag!r}."
+ msg = f"{entity_name} {entity_identifier} already tagged with {tag!r}."
raise TagAlreadyExistsError(msg) from None
+ logger.info(
+ "{entity_name} {entity_identifier} tagged '{tag}'.",
+ entity_name=entity_name,
+ entity_identifier=entity_identifier,
+ tag=tag,
+ )
+
+ tags = await get_tags_function(entity_identifier, connection)
+
+ return {
+ "data_tag": {"id": str(entity_identifier), "tag": tags},
+ }
+
@router.post(
path="/tag",
)
async def tag_dataset(
- data_id: Annotated[int, Body()],
+ data_id: Annotated[Identifier, Body()],
tag: Annotated[str, SystemString64],
user: Annotated[User, Depends(fetch_user_or_raise)],
expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
@@ -77,17 +113,15 @@ async def tag_dataset(
msg = "`expdb_db` should be an AsyncConnection, is {type(expdb_db)}"
raise TypeError(msg)
- async def tag_dataset() -> None:
- await database.datasets.tag(data_id, tag, user_id=user.user_id, connection=expdb_db)
-
- await tag_entity_as_api(tag_dataset, "Dataset", data_id, tag)
-
- tags = await database.datasets.get_tags_for(data_id, expdb_db)
-
- logger.info("Dataset {dataset_id} tagged '{tag}'.", dataset_id=data_id, tag=tag)
- return {
- "data_tag": {"id": str(data_id), "tag": tags},
- }
+ return await tag_entity_as_api(
+ database.datasets.tag,
+ database.datasets.get_tags_for,
+ "Dataset",
+ user.user_id,
+ data_id,
+ tag,
+ expdb_db,
+ )
class DatasetStatusFilter(StrEnum):
From 738d5ddc7433f94c08453f61cc713ba5ed050e49 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 12:19:02 +0200
Subject: [PATCH 03/15] Add error type parameter
---
src/routers/openml/datasets.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index 56037efe..e84b2013 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -26,6 +26,7 @@
DatasetStatusTransitionError,
InternalError,
NoResultsError,
+ ProblemDetailError,
TagAlreadyExistsError,
)
from core.formatting import (
@@ -68,6 +69,7 @@ def __call__(self, entity_identifier: Identifier, connection: AsyncConnection) -
async def tag_entity_as_api(
tag_function: TagFunction,
get_tags_function: GetTagsFunction,
+ not_found_error: type[ProblemDetailError],
entity_name: str,
user_identifier: Identifier,
entity_identifier: Identifier,
@@ -80,8 +82,8 @@ async def tagger() -> None:
try:
await tag_entity(tagger)
except ForeignKeyConstraintError:
- msg = "Not under test yet."
- raise DatasetNotFoundError(msg) from None
+ msg = f"{entity_name} {entity_identifier} not found."
+ raise not_found_error(msg, code=472) from None
except DuplicatePrimaryKeyError:
msg = f"{entity_name} {entity_identifier} already tagged with {tag!r}."
raise TagAlreadyExistsError(msg) from None
@@ -116,6 +118,7 @@ async def tag_dataset(
return await tag_entity_as_api(
database.datasets.tag,
database.datasets.get_tags_for,
+ DatasetNotFoundError,
"Dataset",
user.user_id,
data_id,
From 43c1eca4666ea5197e726b0cf3d14b607d288e40 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 14:52:35 +0200
Subject: [PATCH 04/15] Go back to the direct method of creating the tag
---
src/database/datasets.py | 41 ++++++++++-----
src/database/tags.py | 27 ----------
src/routers/openml/datasets.py | 93 +++++++++-------------------------
src/routers/types.py | 5 ++
4 files changed, 58 insertions(+), 108 deletions(-)
delete mode 100644 src/database/tags.py
diff --git a/src/database/datasets.py b/src/database/datasets.py
index 26eb33d8..d6f91706 100644
--- a/src/database/datasets.py
+++ b/src/database/datasets.py
@@ -5,8 +5,15 @@
from sqlalchemy import text
from sqlalchemy.engine import Row
+from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncConnection
+from database.exceptions import (
+ _DUPLICATE_ENTRY,
+ _FOREIGN_KEY_CONSTRAINT_FAILED,
+ DuplicatePrimaryKeyError,
+ ForeignKeyConstraintError,
+)
from schemas.datasets.openml import Feature
@@ -54,19 +61,27 @@ async def get_tags_for(id_: int, connection: AsyncConnection) -> list[str]:
async def tag(id_: int, tag_: str, *, user_id: int, connection: AsyncConnection) -> None:
- await connection.execute(
- text(
- """
- INSERT INTO dataset_tag(`id`, `tag`, `uploader`)
- VALUES (:dataset_id, :tag, :user_id)
- """,
- ),
- parameters={
- "dataset_id": id_,
- "user_id": user_id,
- "tag": tag_,
- },
- )
+ try:
+ await connection.execute(
+ text(
+ """
+ INSERT INTO dataset_tag(`id`, `tag`, `uploader`)
+ VALUES (:dataset_id, :tag, :user_id)
+ """,
+ ),
+ parameters={
+ "dataset_id": id_,
+ "user_id": user_id,
+ "tag": tag_,
+ },
+ )
+ except IntegrityError as e:
+ code, msg = e.orig.args
+ if code == _FOREIGN_KEY_CONSTRAINT_FAILED:
+ raise ForeignKeyConstraintError(msg) from e
+ if code == _DUPLICATE_ENTRY:
+ raise DuplicatePrimaryKeyError(msg) from e
+ raise
async def get_description(
diff --git a/src/database/tags.py b/src/database/tags.py
deleted file mode 100644
index ac4ab291..00000000
--- a/src/database/tags.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import Callable
-from sqlalchemy.exc import IntegrityError
-
-_FOREIGN_KEY_CONSTRAINT_FAILED = 1452
-_DUPLICATE_ENTRY = 1062
-
-
-class ForeignKeyConstraintError(Exception):
- def __init__(self, msg):
- self.msg = msg
-
-
-class DuplicatePrimaryKeyError(Exception):
- def __init__(self, msg):
- self.msg = msg
-
-
-async def tag_entity(tag_function: Callable) -> None:
- try:
- await tag_function()
- except IntegrityError as e:
- code, msg = e.orig.args
- if code == _FOREIGN_KEY_CONSTRAINT_FAILED:
- raise ForeignKeyConstraintError(msg) from e
- if code == _DUPLICATE_ENTRY:
- raise DuplicatePrimaryKeyError(msg) from e
- raise
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index e84b2013..5f29cd25 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -2,11 +2,10 @@
import re
from datetime import datetime
from enum import StrEnum
-from typing import Annotated, Any, Literal, NamedTuple, Protocol
+from typing import Annotated, Any, Literal, NamedTuple
from fastapi import APIRouter, Body, Depends
from loguru import logger
-from pydantic import Field
from sqlalchemy import bindparam, text
from sqlalchemy.engine import Row
from sqlalchemy.ext.asyncio import AsyncConnection
@@ -26,7 +25,6 @@
DatasetStatusTransitionError,
InternalError,
NoResultsError,
- ProblemDetailError,
TagAlreadyExistsError,
)
from core.formatting import (
@@ -34,7 +32,7 @@
_format_dataset_url,
_format_parquet_url,
)
-from database.tags import DuplicatePrimaryKeyError, ForeignKeyConstraintError, tag_entity
+from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from database.users import User
from routers.dependencies import (
Pagination,
@@ -43,64 +41,17 @@
fetch_user_or_raise,
userdb_connection,
)
-from routers.types import CasualString128, IntegerRange, SystemString64, integer_range_regex
+from routers.types import (
+ CasualString128,
+ Identifier,
+ IntegerRange,
+ SystemString64,
+ integer_range_regex,
+)
from schemas.datasets.openml import DatasetMetadata, DatasetStatus, Feature, FeatureType
router = APIRouter(prefix="/datasets", tags=["datasets"])
-Identifier = Annotated[int, Field(gt=0)]
-
-
-class TagFunction(Protocol):
- def __call__(
- self,
- entity_identifier: Identifier,
- tag: str,
- *,
- user_id: Identifier,
- connection: AsyncConnection,
- ) -> None: ...
-
-
-class GetTagsFunction(Protocol):
- def __call__(self, entity_identifier: Identifier, connection: AsyncConnection) -> list[str]: ...
-
-
-async def tag_entity_as_api(
- tag_function: TagFunction,
- get_tags_function: GetTagsFunction,
- not_found_error: type[ProblemDetailError],
- entity_name: str,
- user_identifier: Identifier,
- entity_identifier: Identifier,
- tag: str,
- connection: AsyncConnection,
-) -> None:
- async def tagger() -> None:
- await tag_function(entity_identifier, tag, user_id=user_identifier, connection=connection)
-
- try:
- await tag_entity(tagger)
- except ForeignKeyConstraintError:
- msg = f"{entity_name} {entity_identifier} not found."
- raise not_found_error(msg, code=472) from None
- except DuplicatePrimaryKeyError:
- msg = f"{entity_name} {entity_identifier} already tagged with {tag!r}."
- raise TagAlreadyExistsError(msg) from None
-
- logger.info(
- "{entity_name} {entity_identifier} tagged '{tag}'.",
- entity_name=entity_name,
- entity_identifier=entity_identifier,
- tag=tag,
- )
-
- tags = await get_tags_function(entity_identifier, connection)
-
- return {
- "data_tag": {"id": str(entity_identifier), "tag": tags},
- }
-
@router.post(
path="/tag",
@@ -115,16 +66,22 @@ async def tag_dataset(
msg = "`expdb_db` should be an AsyncConnection, is {type(expdb_db)}"
raise TypeError(msg)
- return await tag_entity_as_api(
- database.datasets.tag,
- database.datasets.get_tags_for,
- DatasetNotFoundError,
- "Dataset",
- user.user_id,
- data_id,
- tag,
- expdb_db,
- )
+ try:
+ await database.datasets.tag(data_id, tag, user_id=user.user_id, connection=expdb_db)
+ except ForeignKeyConstraintError:
+ msg = f"Dataset {data_id} not found."
+ raise DatasetNotFoundError(msg, code=472) from None
+ except DuplicatePrimaryKeyError:
+ msg = f"Dataset {data_id} already tagged with {tag!r}."
+ raise TagAlreadyExistsError(msg) from None
+
+ logger.info("Dataset {data_id} tagged '{tag}'.", data_id=data_id, tag=tag)
+
+ tags = await database.datasets.get_tags_for(data_id, expdb_db)
+
+ return {
+ "data_tag": {"id": str(data_id), "tag": tags},
+ }
class DatasetStatusFilter(StrEnum):
diff --git a/src/routers/types.py b/src/routers/types.py
index 121875f4..e107ff35 100644
--- a/src/routers/types.py
+++ b/src/routers/types.py
@@ -1,9 +1,14 @@
+from typing import Annotated
+
from fastapi import Body
+from pydantic import Field
SystemString64 = Body(pattern=r"^[\w\-\.]+$", min_length=1, max_length=64)
CasualString128 = Body(pattern=r"^[\w\-\.\(\),]+$", min_length=1, max_length=128)
+Identifier = Annotated[int, Field(gt=0)]
+
integer_range_regex = r"^(\d+)(\.\.\d+)?$"
IntegerRange = Body(
pattern=integer_range_regex,
From d32df385a334349922c0e58cebd7ce1a0fc486db Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 15:22:14 +0200
Subject: [PATCH 05/15] Add test to verify behavior if dataset is not present
---
tests/routers/openml/dataset_tag_test.py | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/tests/routers/openml/dataset_tag_test.py b/tests/routers/openml/dataset_tag_test.py
index f1eab3c0..26a8300e 100644
--- a/tests/routers/openml/dataset_tag_test.py
+++ b/tests/routers/openml/dataset_tag_test.py
@@ -6,7 +6,7 @@
from sqlalchemy.ext.asyncio import AsyncConnection
from core.conversions import nested_remove_single_element_list
-from core.errors import TagAlreadyExistsError
+from core.errors import DatasetNotFoundError, TagAlreadyExistsError
from database.datasets import get_tags_for
from database.users import User
from routers.openml.datasets import tag_dataset
@@ -96,6 +96,20 @@ async def test_dataset_tag_fails_if_tag_exists(expdb_test: AsyncConnection) -> N
assert tag in e.value.detail
+async def test_dataset_tag_fails_if_dataset_does_not_exist(expdb_test: AsyncConnection) -> None:
+ dataset_id = 1_000_000
+ with pytest.raises(DatasetNotFoundError) as e:
+ await tag_dataset(
+ data_id=dataset_id,
+ tag="foo",
+ user=ADMIN_USER,
+ expdb_db=expdb_test,
+ )
+ assert str(dataset_id) in e.value.detail
+ dataset_not_found_in_tag_endpoint = 472
+ assert e.value.code == dataset_not_found_in_tag_endpoint
+
+
# -- migration tests --
From 50d6d3377ec5cbed344672556545c5f68a9badb1 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 16:54:43 +0200
Subject: [PATCH 06/15] Update migration test to include testing behavior for
dataset 404
---
tests/constants.py | 3 +++
tests/routers/openml/dataset_tag_test.py | 17 ++++++++++++++++-
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/tests/constants.py b/tests/constants.py
index 3c847f7c..5563dece 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -1,5 +1,8 @@
+DATASET_ID_THAT_DOES_NOT_EXIST = 9_9999_999
+SOME_PRIVATE_DATASET_ID = 130
PRIVATE_DATASET_ID = {130}
IN_PREPARATION_ID = {33, 161, 162, 163}
+SOME_DEACTIVATED_DATASET_ID = 131
DEACTIVATED_DATASETS = {131}
DATASETS = set(range(1, 132)) | {161, 162, 163}
diff --git a/tests/routers/openml/dataset_tag_test.py b/tests/routers/openml/dataset_tag_test.py
index 26a8300e..79c982f3 100644
--- a/tests/routers/openml/dataset_tag_test.py
+++ b/tests/routers/openml/dataset_tag_test.py
@@ -116,7 +116,12 @@ async def test_dataset_tag_fails_if_dataset_does_not_exist(expdb_test: AsyncConn
@pytest.mark.mut
@pytest.mark.parametrize(
"dataset_id",
- [*range(1, 10), 101, 131],
+ [
+ *range(1, 10),
+ 101,
+ constants.SOME_DEACTIVATED_DATASET_ID,
+ constants.DATASET_ID_THAT_DOES_NOT_EXIST,
+ ],
)
@pytest.mark.parametrize(
"api_key",
@@ -156,6 +161,7 @@ async def test_dataset_tag_response_is_identical(
and php_response.json()["error"]["message"] == "An Elastic Search Exception occured."
):
pytest.skip("Encountered Elastic Search error.")
+
py_response = await py_api.post(
f"/datasets/tag?api_key={api_key}",
json={"data_id": dataset_id, "tag": tag},
@@ -172,6 +178,15 @@ async def test_dataset_tag_response_is_identical(
)
return
+ if py_response.status_code == HTTPStatus.NOT_FOUND:
+ assert php_response.status_code == HTTPStatus.PRECONDITION_FAILED
+ py_error = py_response.json()
+ php_error = php_response.json()["error"]
+ assert py_error["code"] == php_error["code"]
+ assert php_error["message"] == "Entity not found."
+ assert re.match(r"Dataset \d+ not found.", py_error["detail"])
+ return
+
assert py_response.status_code == php_response.status_code, php_response.json()
if py_response.status_code != HTTPStatus.OK:
assert py_response.json()["code"] == php_response.json()["error"]["code"]
From 1e8337fd44e8d65b58e433670a598da2777e7b8b Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sat, 2 May 2026 17:17:07 +0000
Subject: [PATCH 07/15] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/routers/openml/datasets.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index 5f29cd25..b1f940dc 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -4,6 +4,7 @@
from enum import StrEnum
from typing import Annotated, Any, Literal, NamedTuple
+from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from fastapi import APIRouter, Body, Depends
from loguru import logger
from sqlalchemy import bindparam, text
@@ -32,7 +33,6 @@
_format_dataset_url,
_format_parquet_url,
)
-from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from database.users import User
from routers.dependencies import (
Pagination,
From b2720251429f08e16dfb1d43100aa448ca0d4e11 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 19:20:04 +0200
Subject: [PATCH 08/15] Remove typecheck; covered by mypy and pydantic
---
src/routers/openml/datasets.py | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index b1f940dc..68d86aed 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -4,7 +4,6 @@
from enum import StrEnum
from typing import Annotated, Any, Literal, NamedTuple
-from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from fastapi import APIRouter, Body, Depends
from loguru import logger
from sqlalchemy import bindparam, text
@@ -33,6 +32,7 @@
_format_dataset_url,
_format_parquet_url,
)
+from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from database.users import User
from routers.dependencies import (
Pagination,
@@ -62,10 +62,6 @@ async def tag_dataset(
user: Annotated[User, Depends(fetch_user_or_raise)],
expdb_db: Annotated[AsyncConnection, Depends(expdb_connection)],
) -> dict[str, dict[str, Any]]:
- if not isinstance(expdb_db, AsyncConnection):
- msg = "`expdb_db` should be an AsyncConnection, is {type(expdb_db)}"
- raise TypeError(msg)
-
try:
await database.datasets.tag(data_id, tag, user_id=user.user_id, connection=expdb_db)
except ForeignKeyConstraintError:
From bd71889aaeee34c8bfd123b00839ff53cbfdfd3d Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sat, 2 May 2026 17:20:32 +0000
Subject: [PATCH 09/15] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/routers/openml/datasets.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index 68d86aed..b5112096 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -4,6 +4,7 @@
from enum import StrEnum
from typing import Annotated, Any, Literal, NamedTuple
+from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from fastapi import APIRouter, Body, Depends
from loguru import logger
from sqlalchemy import bindparam, text
@@ -32,7 +33,6 @@
_format_dataset_url,
_format_parquet_url,
)
-from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from database.users import User
from routers.dependencies import (
Pagination,
From 18f177a6e7b807ecd09d0e91dda0aa0d6a1674b9 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 19:51:21 +0200
Subject: [PATCH 10/15] Add healthcheck to avoid silent failure (esp. in CI)
---
compose.yaml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/compose.yaml b/compose.yaml
index c51a4afc..c90c29ef 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -66,3 +66,9 @@ services:
OPENML_REST_API_CONFIG_DIRECTORY: "/config"
depends_on:
- database
+ healthcheck:
+ test: curl -f 127.0.0.1:8000 || exit 1
+ start_period: 20s
+ start_interval: 5s
+ timeout: 3s
+ interval: 1m
From bd4623ba0845e219859b61b1a87ddfb48104d262 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 19:57:19 +0200
Subject: [PATCH 11/15] Define the exceptions
---
src/database/exceptions.py | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 src/database/exceptions.py
diff --git a/src/database/exceptions.py b/src/database/exceptions.py
new file mode 100644
index 00000000..5fdf95f0
--- /dev/null
+++ b/src/database/exceptions.py
@@ -0,0 +1,22 @@
+"""Defines exceptions of the database layer."""
+
+_FOREIGN_KEY_CONSTRAINT_FAILED = 1452
+_DUPLICATE_ENTRY = 1062
+
+
+class ForeignKeyConstraintError(Exception):
+ """Foreign key constraint violated."""
+
+ def __init__(self, msg: str) -> None:
+ """Initialize the error with a message `msg`."""
+ super().__init__()
+ self.msg: str = msg
+
+
+class DuplicatePrimaryKeyError(Exception):
+ """Primary key already present."""
+
+ def __init__(self, msg: str) -> None:
+ """Initialize the error with a message `msg`."""
+ super().__init__()
+ self.msg: str = msg
From 3005335790a08d4c2ed6c65f388a72dfc97c2f39 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 20:07:31 +0200
Subject: [PATCH 12/15] Remove the crutch that was needed for startup container
Previously there was a database startup service, which
would cause docker compose up to exit with error code 1
(see also the referenced issue). But since this is no
longer the case, we can just rely on normal tool exit code.
---
.github/workflows/tests.yml | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ea005b33..9ae6abc2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -28,8 +28,6 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: 3.x
-
- # https://github.com/docker/compose/issues/10596
- name: Start services
run: |
services="python-api"
@@ -37,8 +35,7 @@ jobs:
sed -i 's/INDEX_ES_DURING_STARTUP=false/INDEX_ES_DURING_STARTUP=true/' docker/php/.env
services="$services php-api"
fi
- docker compose up $services --detach --wait --remove-orphans || exit $(docker compose ps -q | xargs docker inspect -f '{{.State.ExitCode}}' | grep -v '^0' | wc -l)
-
+ docker compose up $services --detach --wait --remove-orphans
- name: Run tests
run: |
marker="${{ matrix.php_api == true && 'php_api' || 'not php_api' }} and ${{ matrix.mutations == true && 'mut' || 'not mut' }}"
From ad61f211f2203efc4b8a1e3c975c6d9a20884bb4 Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 20:15:00 +0200
Subject: [PATCH 13/15] Remove health check
There is no status that depends on it, and it's not required to
make sure that the `docker compose` command exits with code 1
if startup fails.
---
compose.yaml | 6 ------
1 file changed, 6 deletions(-)
diff --git a/compose.yaml b/compose.yaml
index c90c29ef..c51a4afc 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -66,9 +66,3 @@ services:
OPENML_REST_API_CONFIG_DIRECTORY: "/config"
depends_on:
- database
- healthcheck:
- test: curl -f 127.0.0.1:8000 || exit 1
- start_period: 20s
- start_interval: 5s
- timeout: 3s
- interval: 1m
From a7e9e92bca588a1bdbdb78faedad60265d3ae58d Mon Sep 17 00:00:00 2001
From: PGijsbers
Date: Sat, 2 May 2026 20:23:02 +0200
Subject: [PATCH 14/15] [no ci] add .claude/ and .ignore/
---
.gitignore | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.gitignore b/.gitignore
index 5bcdce76..21e2bcf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,6 @@
docker/mysql/data
+.claude/
+.ignore/
*.log
logs/
.DS_Store
From d9814e1d322f2ebdf606ce7c121f8ca24915c37f Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Sat, 2 May 2026 18:23:20 +0000
Subject: [PATCH 15/15] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
src/routers/openml/datasets.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routers/openml/datasets.py b/src/routers/openml/datasets.py
index b5112096..68d86aed 100644
--- a/src/routers/openml/datasets.py
+++ b/src/routers/openml/datasets.py
@@ -4,7 +4,6 @@
from enum import StrEnum
from typing import Annotated, Any, Literal, NamedTuple
-from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from fastapi import APIRouter, Body, Depends
from loguru import logger
from sqlalchemy import bindparam, text
@@ -33,6 +32,7 @@
_format_dataset_url,
_format_parquet_url,
)
+from database.exceptions import DuplicatePrimaryKeyError, ForeignKeyConstraintError
from database.users import User
from routers.dependencies import (
Pagination,