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,