diff --git a/.gitignore b/.gitignore
index c06e715ef..060db33be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -77,6 +77,7 @@ target/
# IDE
.idea
*.swp
+.vscode
# MYPY
.mypy_cache
diff --git a/doc/api.rst b/doc/api.rst
index 86bfd121e..288bf66fb 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -38,6 +38,7 @@ Dataset Functions
attributes_arff_from_df
check_datasets_active
create_dataset
+ delete_dataset
get_dataset
get_datasets
list_datasets
@@ -103,6 +104,7 @@ Flow Functions
:template: function.rst
assert_flows_equal
+ delete_flow
flow_exists
get_flow
list_flows
@@ -133,6 +135,7 @@ Run Functions
:toctree: generated/
:template: function.rst
+ delete_run
get_run
get_runs
get_run_trace
@@ -251,6 +254,7 @@ Task Functions
:template: function.rst
create_task
+ delete_task
get_task
get_tasks
list_tasks
diff --git a/doc/progress.rst b/doc/progress.rst
index 48dc2a1a3..d981c09c0 100644
--- a/doc/progress.rst
+++ b/doc/progress.rst
@@ -10,10 +10,10 @@ Changelog
~~~~~~
* Add new contributions here.
- * ADD#1144: Add locally computed results to the ``OpenMLRun`` object's representation.
+ * ADD#1028: Add functions to delete runs, flows, datasets, and tasks (e.g., ``openml.datasets.delete_dataset``).
+ * ADD#1144: Add locally computed results to the ``OpenMLRun`` object's representation if the run was created locally and not downloaded from the server.
* FIX #1197 #559 #1131: Fix the order of ground truth and predictions in the ``OpenMLRun`` object and in ``format_prediction``.
* FIX #1198: Support numpy 1.24 and higher.
- * ADD#1144: Add locally computed results to the ``OpenMLRun`` object's representation if the run was created locally and not downloaded from the server.
0.13.0
~~~~~~
diff --git a/openml/_api_calls.py b/openml/_api_calls.py
index 5140a3470..f7b2a34c5 100644
--- a/openml/_api_calls.py
+++ b/openml/_api_calls.py
@@ -351,10 +351,12 @@ def _send_request(request_method, url, data, files=None, md5_checksum=None):
xml.parsers.expat.ExpatError,
OpenMLHashException,
) as e:
- if isinstance(e, OpenMLServerException):
- if e.code not in [107]:
- # 107: database connection error
- raise
+ if isinstance(e, OpenMLServerException) and e.code != 107:
+ # Propagate all server errors to the calling functions, except
+ # for 107 which represents a database connection error.
+ # These are typically caused by high server load,
+ # which means trying again might resolve the issue.
+ raise
elif isinstance(e, xml.parsers.expat.ExpatError):
if request_method != "get" or retry_counter >= n_retries:
raise OpenMLServerError(
diff --git a/openml/datasets/__init__.py b/openml/datasets/__init__.py
index abde85c06..efa5a5d5b 100644
--- a/openml/datasets/__init__.py
+++ b/openml/datasets/__init__.py
@@ -11,6 +11,7 @@
list_qualities,
edit_dataset,
fork_dataset,
+ delete_dataset,
)
from .dataset import OpenMLDataset
from .data_feature import OpenMLDataFeature
@@ -28,4 +29,5 @@
"list_qualities",
"edit_dataset",
"fork_dataset",
+ "delete_dataset",
]
diff --git a/openml/datasets/functions.py b/openml/datasets/functions.py
index 770413a23..4307c8008 100644
--- a/openml/datasets/functions.py
+++ b/openml/datasets/functions.py
@@ -1271,3 +1271,22 @@ def _get_online_dataset_format(dataset_id):
dataset_xml = openml._api_calls._perform_api_call("data/%d" % dataset_id, "get")
# build a dict from the xml and get the format from the dataset description
return xmltodict.parse(dataset_xml)["oml:data_set_description"]["oml:format"].lower()
+
+
+def delete_dataset(dataset_id: int) -> bool:
+ """Delete dataset with id `dataset_id` from the OpenML server.
+
+ This can only be done if you are the owner of the dataset and
+ no tasks are attached to the dataset.
+
+ Parameters
+ ----------
+ dataset_id : int
+ OpenML id of the dataset
+
+ Returns
+ -------
+ bool
+ True if the deletion was successful. False otherwise.
+ """
+ return openml.utils._delete_entity("data", dataset_id)
diff --git a/openml/exceptions.py b/openml/exceptions.py
index a5f132128..fe2138e76 100644
--- a/openml/exceptions.py
+++ b/openml/exceptions.py
@@ -11,15 +11,14 @@ class OpenMLServerError(PyOpenMLError):
"""class for when something is really wrong on the server
(result did not parse to dict), contains unparsed error."""
- def __init__(self, message: str):
- super().__init__(message)
+ pass
class OpenMLServerException(OpenMLServerError):
"""exception for when the result of the server was
not 200 (e.g., listing call w/o results)."""
- # Code needs to be optional to allow the exceptino to be picklable:
+ # Code needs to be optional to allow the exception to be picklable:
# https://stackoverflow.com/questions/16244923/how-to-make-a-custom-exception-class-with-multiple-init-args-pickleable # noqa: E501
def __init__(self, message: str, code: int = None, url: str = None):
self.message = message
@@ -28,15 +27,11 @@ def __init__(self, message: str, code: int = None, url: str = None):
super().__init__(message)
def __str__(self):
- return "%s returned code %s: %s" % (
- self.url,
- self.code,
- self.message,
- )
+ return f"{self.url} returned code {self.code}: {self.message}"
class OpenMLServerNoResult(OpenMLServerException):
- """exception for when the result of the server is empty."""
+ """Exception for when the result of the server is empty."""
pass
@@ -44,8 +39,7 @@ class OpenMLServerNoResult(OpenMLServerException):
class OpenMLCacheException(PyOpenMLError):
"""Dataset / task etc not found in cache"""
- def __init__(self, message: str):
- super().__init__(message)
+ pass
class OpenMLHashException(PyOpenMLError):
@@ -57,8 +51,7 @@ class OpenMLHashException(PyOpenMLError):
class OpenMLPrivateDatasetError(PyOpenMLError):
"""Exception thrown when the user has no rights to access the dataset."""
- def __init__(self, message: str):
- super().__init__(message)
+ pass
class OpenMLRunsExistError(PyOpenMLError):
@@ -69,3 +62,9 @@ def __init__(self, run_ids: set, message: str):
raise ValueError("Set of run ids must be non-empty.")
self.run_ids = run_ids
super().__init__(message)
+
+
+class OpenMLNotAuthorizedError(OpenMLServerError):
+ """Indicates an authenticated user is not authorized to execute the requested action."""
+
+ pass
diff --git a/openml/flows/__init__.py b/openml/flows/__init__.py
index 3642b9c56..f8d35c3f5 100644
--- a/openml/flows/__init__.py
+++ b/openml/flows/__init__.py
@@ -2,7 +2,14 @@
from .flow import OpenMLFlow
-from .functions import get_flow, list_flows, flow_exists, get_flow_id, assert_flows_equal
+from .functions import (
+ get_flow,
+ list_flows,
+ flow_exists,
+ get_flow_id,
+ assert_flows_equal,
+ delete_flow,
+)
__all__ = [
"OpenMLFlow",
@@ -11,4 +18,5 @@
"get_flow_id",
"flow_exists",
"assert_flows_equal",
+ "delete_flow",
]
diff --git a/openml/flows/functions.py b/openml/flows/functions.py
index 99525c3e4..aea5cae6d 100644
--- a/openml/flows/functions.py
+++ b/openml/flows/functions.py
@@ -544,3 +544,22 @@ def _create_flow_from_xml(flow_xml: str) -> OpenMLFlow:
"""
return OpenMLFlow._from_dict(xmltodict.parse(flow_xml))
+
+
+def delete_flow(flow_id: int) -> bool:
+ """Delete flow with id `flow_id` from the OpenML server.
+
+ You can only delete flows which you uploaded and which
+ which are not linked to runs.
+
+ Parameters
+ ----------
+ flow_id : int
+ OpenML id of the flow
+
+ Returns
+ -------
+ bool
+ True if the deletion was successful. False otherwise.
+ """
+ return openml.utils._delete_entity("flow", flow_id)
diff --git a/openml/runs/__init__.py b/openml/runs/__init__.py
index e917a57a5..2abbd8f29 100644
--- a/openml/runs/__init__.py
+++ b/openml/runs/__init__.py
@@ -12,6 +12,7 @@
run_exists,
initialize_model_from_run,
initialize_model_from_trace,
+ delete_run,
)
__all__ = [
@@ -27,4 +28,5 @@
"run_exists",
"initialize_model_from_run",
"initialize_model_from_trace",
+ "delete_run",
]
diff --git a/openml/runs/functions.py b/openml/runs/functions.py
index ff1f07c06..d52b43add 100644
--- a/openml/runs/functions.py
+++ b/openml/runs/functions.py
@@ -1209,3 +1209,21 @@ def format_prediction(
return [repeat, fold, index, prediction, truth]
else:
raise NotImplementedError(f"Formatting for {type(task)} is not supported.")
+
+
+def delete_run(run_id: int) -> bool:
+ """Delete run with id `run_id` from the OpenML server.
+
+ You can only delete runs which you uploaded.
+
+ Parameters
+ ----------
+ run_id : int
+ OpenML id of the run
+
+ Returns
+ -------
+ bool
+ True if the deletion was successful. False otherwise.
+ """
+ return openml.utils._delete_entity("run", run_id)
diff --git a/openml/tasks/__init__.py b/openml/tasks/__init__.py
index cba0aa14f..a5d578d2d 100644
--- a/openml/tasks/__init__.py
+++ b/openml/tasks/__init__.py
@@ -15,6 +15,7 @@
get_task,
get_tasks,
list_tasks,
+ delete_task,
)
__all__ = [
@@ -30,4 +31,5 @@
"list_tasks",
"OpenMLSplit",
"TaskType",
+ "delete_task",
]
diff --git a/openml/tasks/functions.py b/openml/tasks/functions.py
index c44d55ea7..964277760 100644
--- a/openml/tasks/functions.py
+++ b/openml/tasks/functions.py
@@ -545,3 +545,22 @@ def create_task(
evaluation_measure=evaluation_measure,
**kwargs,
)
+
+
+def delete_task(task_id: int) -> bool:
+ """Delete task with id `task_id` from the OpenML server.
+
+ You can only delete tasks which you created and have
+ no runs associated with them.
+
+ Parameters
+ ----------
+ task_id : int
+ OpenML id of the task
+
+ Returns
+ -------
+ bool
+ True if the deletion was successful. False otherwise.
+ """
+ return openml.utils._delete_entity("task", task_id)
diff --git a/openml/testing.py b/openml/testing.py
index 56445a253..4e2f0c006 100644
--- a/openml/testing.py
+++ b/openml/testing.py
@@ -3,12 +3,14 @@
import hashlib
import inspect
import os
+import pathlib
import shutil
import sys
import time
from typing import Dict, Union, cast
import unittest
import pandas as pd
+import requests
import openml
from openml.tasks import TaskType
@@ -306,4 +308,22 @@ class CustomImputer(SimpleImputer):
pass
-__all__ = ["TestBase", "SimpleImputer", "CustomImputer", "check_task_existence"]
+def create_request_response(
+ *, status_code: int, content_filepath: pathlib.Path
+) -> requests.Response:
+ with open(content_filepath, "r") as xml_response:
+ response_body = xml_response.read()
+
+ response = requests.Response()
+ response.status_code = status_code
+ response._content = response_body.encode()
+ return response
+
+
+__all__ = [
+ "TestBase",
+ "SimpleImputer",
+ "CustomImputer",
+ "check_task_existence",
+ "create_request_response",
+]
diff --git a/openml/utils.py b/openml/utils.py
index 0f60f2bb8..3c2fa876f 100644
--- a/openml/utils.py
+++ b/openml/utils.py
@@ -172,9 +172,42 @@ def _delete_entity(entity_type, entity_id):
raise ValueError("Can't delete a %s" % entity_type)
url_suffix = "%s/%d" % (entity_type, entity_id)
- result_xml = openml._api_calls._perform_api_call(url_suffix, "delete")
- result = xmltodict.parse(result_xml)
- return "oml:%s_delete" % entity_type in result
+ try:
+ result_xml = openml._api_calls._perform_api_call(url_suffix, "delete")
+ result = xmltodict.parse(result_xml)
+ return f"oml:{entity_type}_delete" in result
+ except openml.exceptions.OpenMLServerException as e:
+ # https://github.com/openml/OpenML/blob/21f6188d08ac24fcd2df06ab94cf421c946971b0/openml_OS/views/pages/api_new/v1/xml/pre.php
+ # Most exceptions are descriptive enough to be raised as their standard
+ # OpenMLServerException, however there are two cases where we add information:
+ # - a generic "failed" message, we direct them to the right issue board
+ # - when the user successfully authenticates with the server,
+ # but user is not allowed to take the requested action,
+ # in which case we specify a OpenMLNotAuthorizedError.
+ by_other_user = [323, 353, 393, 453, 594]
+ has_dependent_entities = [324, 326, 327, 328, 354, 454, 464, 595]
+ unknown_reason = [325, 355, 394, 455, 593]
+ if e.code in by_other_user:
+ raise openml.exceptions.OpenMLNotAuthorizedError(
+ message=(
+ f"The {entity_type} can not be deleted because it was not uploaded by you."
+ ),
+ ) from e
+ if e.code in has_dependent_entities:
+ raise openml.exceptions.OpenMLNotAuthorizedError(
+ message=(
+ f"The {entity_type} can not be deleted because "
+ f"it still has associated entities: {e.message}"
+ )
+ ) from e
+ if e.code in unknown_reason:
+ raise openml.exceptions.OpenMLServerError(
+ message=(
+ f"The {entity_type} can not be deleted for unknown reason,"
+ " please open an issue at: https://github.com/openml/openml/issues/new"
+ ),
+ ) from e
+ raise
def _list_all(listing_call, output_format="dict", *args, **filters):
diff --git a/tests/conftest.py b/tests/conftest.py
index d727bb537..43e2cc3ee 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -185,3 +185,13 @@ def pytest_addoption(parser):
@pytest.fixture(scope="class")
def long_version(request):
request.cls.long_version = request.config.getoption("--long")
+
+
+@pytest.fixture
+def test_files_directory() -> pathlib.Path:
+ return pathlib.Path(__file__).parent / "files"
+
+
+@pytest.fixture()
+def test_api_key() -> str:
+ return "c0c42819af31e706efe1f4b88c23c6c1"
diff --git a/tests/files/mock_responses/datasets/data_delete_has_tasks.xml b/tests/files/mock_responses/datasets/data_delete_has_tasks.xml
new file mode 100644
index 000000000..fc866047c
--- /dev/null
+++ b/tests/files/mock_responses/datasets/data_delete_has_tasks.xml
@@ -0,0 +1,4 @@
+
+ 354
+ Dataset is in use by other content. Can not be deleted
+
diff --git a/tests/files/mock_responses/datasets/data_delete_not_exist.xml b/tests/files/mock_responses/datasets/data_delete_not_exist.xml
new file mode 100644
index 000000000..b3b212fbe
--- /dev/null
+++ b/tests/files/mock_responses/datasets/data_delete_not_exist.xml
@@ -0,0 +1,4 @@
+
+ 352
+ Dataset does not exist
+
diff --git a/tests/files/mock_responses/datasets/data_delete_not_owned.xml b/tests/files/mock_responses/datasets/data_delete_not_owned.xml
new file mode 100644
index 000000000..7d412d48e
--- /dev/null
+++ b/tests/files/mock_responses/datasets/data_delete_not_owned.xml
@@ -0,0 +1,4 @@
+
+ 353
+ Dataset is not owned by you
+
\ No newline at end of file
diff --git a/tests/files/mock_responses/datasets/data_delete_successful.xml b/tests/files/mock_responses/datasets/data_delete_successful.xml
new file mode 100644
index 000000000..9df47c1a2
--- /dev/null
+++ b/tests/files/mock_responses/datasets/data_delete_successful.xml
@@ -0,0 +1,3 @@
+
+ 40000
+
diff --git a/tests/files/mock_responses/flows/flow_delete_has_runs.xml b/tests/files/mock_responses/flows/flow_delete_has_runs.xml
new file mode 100644
index 000000000..5c8530e75
--- /dev/null
+++ b/tests/files/mock_responses/flows/flow_delete_has_runs.xml
@@ -0,0 +1,5 @@
+
+ 324
+ flow is in use by other content (runs). Can not be deleted
+ {10716, 10707} ()
+
diff --git a/tests/files/mock_responses/flows/flow_delete_is_subflow.xml b/tests/files/mock_responses/flows/flow_delete_is_subflow.xml
new file mode 100644
index 000000000..ddc314ae4
--- /dev/null
+++ b/tests/files/mock_responses/flows/flow_delete_is_subflow.xml
@@ -0,0 +1,5 @@
+
+ 328
+ flow is in use by other content (it is a subflow). Can not be deleted
+ {37661}
+
diff --git a/tests/files/mock_responses/flows/flow_delete_not_exist.xml b/tests/files/mock_responses/flows/flow_delete_not_exist.xml
new file mode 100644
index 000000000..4df49149f
--- /dev/null
+++ b/tests/files/mock_responses/flows/flow_delete_not_exist.xml
@@ -0,0 +1,4 @@
+
+ 322
+ flow does not exist
+
diff --git a/tests/files/mock_responses/flows/flow_delete_not_owned.xml b/tests/files/mock_responses/flows/flow_delete_not_owned.xml
new file mode 100644
index 000000000..3aa9a9ef2
--- /dev/null
+++ b/tests/files/mock_responses/flows/flow_delete_not_owned.xml
@@ -0,0 +1,4 @@
+
+ 323
+ flow is not owned by you
+
diff --git a/tests/files/mock_responses/flows/flow_delete_successful.xml b/tests/files/mock_responses/flows/flow_delete_successful.xml
new file mode 100644
index 000000000..7638e942d
--- /dev/null
+++ b/tests/files/mock_responses/flows/flow_delete_successful.xml
@@ -0,0 +1,3 @@
+
+ 33364
+
diff --git a/tests/files/mock_responses/runs/run_delete_not_exist.xml b/tests/files/mock_responses/runs/run_delete_not_exist.xml
new file mode 100644
index 000000000..855c223fa
--- /dev/null
+++ b/tests/files/mock_responses/runs/run_delete_not_exist.xml
@@ -0,0 +1,4 @@
+
+ 392
+ Run does not exist
+
diff --git a/tests/files/mock_responses/runs/run_delete_not_owned.xml b/tests/files/mock_responses/runs/run_delete_not_owned.xml
new file mode 100644
index 000000000..551252e22
--- /dev/null
+++ b/tests/files/mock_responses/runs/run_delete_not_owned.xml
@@ -0,0 +1,4 @@
+
+ 393
+ Run is not owned by you
+
diff --git a/tests/files/mock_responses/runs/run_delete_successful.xml b/tests/files/mock_responses/runs/run_delete_successful.xml
new file mode 100644
index 000000000..fe4233afa
--- /dev/null
+++ b/tests/files/mock_responses/runs/run_delete_successful.xml
@@ -0,0 +1,3 @@
+
+ 10591880
+
diff --git a/tests/files/mock_responses/tasks/task_delete_has_runs.xml b/tests/files/mock_responses/tasks/task_delete_has_runs.xml
new file mode 100644
index 000000000..87a92540d
--- /dev/null
+++ b/tests/files/mock_responses/tasks/task_delete_has_runs.xml
@@ -0,0 +1,4 @@
+
+ 454
+ Task is executed in some runs. Delete these first
+
diff --git a/tests/files/mock_responses/tasks/task_delete_not_exist.xml b/tests/files/mock_responses/tasks/task_delete_not_exist.xml
new file mode 100644
index 000000000..8a262af29
--- /dev/null
+++ b/tests/files/mock_responses/tasks/task_delete_not_exist.xml
@@ -0,0 +1,4 @@
+
+ 452
+ Task does not exist
+
diff --git a/tests/files/mock_responses/tasks/task_delete_not_owned.xml b/tests/files/mock_responses/tasks/task_delete_not_owned.xml
new file mode 100644
index 000000000..3d504772b
--- /dev/null
+++ b/tests/files/mock_responses/tasks/task_delete_not_owned.xml
@@ -0,0 +1,4 @@
+
+ 453
+ Task is not owned by you
+
diff --git a/tests/files/mock_responses/tasks/task_delete_successful.xml b/tests/files/mock_responses/tasks/task_delete_successful.xml
new file mode 100644
index 000000000..594b6e992
--- /dev/null
+++ b/tests/files/mock_responses/tasks/task_delete_successful.xml
@@ -0,0 +1,3 @@
+
+ 361323
+
diff --git a/tests/test_datasets/test_dataset_functions.py b/tests/test_datasets/test_dataset_functions.py
index e6c4fe3ec..45a64ab8a 100644
--- a/tests/test_datasets/test_dataset_functions.py
+++ b/tests/test_datasets/test_dataset_functions.py
@@ -13,6 +13,7 @@
import pytest
import numpy as np
import pandas as pd
+import requests
import scipy.sparse
from oslo_concurrency import lockutils
@@ -23,8 +24,9 @@
OpenMLHashException,
OpenMLPrivateDatasetError,
OpenMLServerException,
+ OpenMLNotAuthorizedError,
)
-from openml.testing import TestBase
+from openml.testing import TestBase, create_request_response
from openml.utils import _tag_entity, _create_cache_directory_for_id
from openml.datasets.functions import (
create_dataset,
@@ -1672,3 +1674,138 @@ def test_valid_attribute_validations(default_target_attribute, row_id_attribute,
original_data_url=original_data_url,
paper_url=paper_url,
)
+
+ def test_delete_dataset(self):
+ data = [
+ ["a", "sunny", 85.0, 85.0, "FALSE", "no"],
+ ["b", "sunny", 80.0, 90.0, "TRUE", "no"],
+ ["c", "overcast", 83.0, 86.0, "FALSE", "yes"],
+ ["d", "rainy", 70.0, 96.0, "FALSE", "yes"],
+ ["e", "rainy", 68.0, 80.0, "FALSE", "yes"],
+ ]
+ column_names = ["rnd_str", "outlook", "temperature", "humidity", "windy", "play"]
+ df = pd.DataFrame(data, columns=column_names)
+ # enforce the type of each column
+ df["outlook"] = df["outlook"].astype("category")
+ df["windy"] = df["windy"].astype("bool")
+ df["play"] = df["play"].astype("category")
+ # meta-information
+ name = "%s-pandas_testing_dataset" % self._get_sentinel()
+ description = "Synthetic dataset created from a Pandas DataFrame"
+ creator = "OpenML tester"
+ collection_date = "01-01-2018"
+ language = "English"
+ licence = "MIT"
+ citation = "None"
+ original_data_url = "http://openml.github.io/openml-python"
+ paper_url = "http://openml.github.io/openml-python"
+ dataset = openml.datasets.functions.create_dataset(
+ name=name,
+ description=description,
+ creator=creator,
+ contributor=None,
+ collection_date=collection_date,
+ language=language,
+ licence=licence,
+ default_target_attribute="play",
+ row_id_attribute=None,
+ ignore_attribute=None,
+ citation=citation,
+ attributes="auto",
+ data=df,
+ version_label="test",
+ original_data_url=original_data_url,
+ paper_url=paper_url,
+ )
+ dataset.publish()
+ _dataset_id = dataset.id
+ self.assertTrue(openml.datasets.delete_dataset(_dataset_id))
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_dataset_not_owned(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = (
+ test_files_directory / "mock_responses" / "datasets" / "data_delete_not_owned.xml"
+ )
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The data can not be deleted because it was not uploaded by you.",
+ ):
+ openml.datasets.delete_dataset(40_000)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/data/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_dataset_with_run(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = (
+ test_files_directory / "mock_responses" / "datasets" / "data_delete_has_tasks.xml"
+ )
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The data can not be deleted because it still has associated entities:",
+ ):
+ openml.datasets.delete_dataset(40_000)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/data/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_dataset_success(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = (
+ test_files_directory / "mock_responses" / "datasets" / "data_delete_successful.xml"
+ )
+ mock_delete.return_value = create_request_response(
+ status_code=200, content_filepath=content_file
+ )
+
+ success = openml.datasets.delete_dataset(40000)
+ assert success
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/data/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_unknown_dataset(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = (
+ test_files_directory / "mock_responses" / "datasets" / "data_delete_not_exist.xml"
+ )
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLServerException,
+ match="Dataset does not exist",
+ ):
+ openml.datasets.delete_dataset(9_999_999)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/data/9999999",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
diff --git a/tests/test_flows/test_flow_functions.py b/tests/test_flows/test_flow_functions.py
index 532fb1d1b..f2520cb36 100644
--- a/tests/test_flows/test_flow_functions.py
+++ b/tests/test_flows/test_flow_functions.py
@@ -4,16 +4,20 @@
import copy
import functools
import unittest
+from unittest import mock
from unittest.mock import patch
from distutils.version import LooseVersion
+
+import requests
import sklearn
from sklearn import ensemble
import pandas as pd
import pytest
import openml
-from openml.testing import TestBase
+from openml.exceptions import OpenMLNotAuthorizedError, OpenMLServerException
+from openml.testing import TestBase, create_request_response
import openml.extensions.sklearn
@@ -410,3 +414,126 @@ def test_get_flow_id(self):
)
self.assertEqual(flow_ids_exact_version_True, flow_ids_exact_version_False)
self.assertIn(flow.flow_id, flow_ids_exact_version_True)
+
+ def test_delete_flow(self):
+ flow = openml.OpenMLFlow(
+ name="sklearn.dummy.DummyClassifier",
+ class_name="sklearn.dummy.DummyClassifier",
+ description="test description",
+ model=sklearn.dummy.DummyClassifier(),
+ components=OrderedDict(),
+ parameters=OrderedDict(),
+ parameters_meta_info=OrderedDict(),
+ external_version="1",
+ tags=[],
+ language="English",
+ dependencies=None,
+ )
+
+ flow, _ = self._add_sentinel_to_flow_name(flow, None)
+
+ flow.publish()
+ _flow_id = flow.flow_id
+ self.assertTrue(openml.flows.delete_flow(_flow_id))
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_flow_not_owned(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "flows" / "flow_delete_not_owned.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The flow can not be deleted because it was not uploaded by you.",
+ ):
+ openml.flows.delete_flow(40_000)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/flow/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_flow_with_run(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "flows" / "flow_delete_has_runs.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The flow can not be deleted because it still has associated entities:",
+ ):
+ openml.flows.delete_flow(40_000)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/flow/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_subflow(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "flows" / "flow_delete_is_subflow.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The flow can not be deleted because it still has associated entities:",
+ ):
+ openml.flows.delete_flow(40_000)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/flow/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_flow_success(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "flows" / "flow_delete_successful.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=200, content_filepath=content_file
+ )
+
+ success = openml.flows.delete_flow(33364)
+ assert success
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/flow/33364",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_unknown_flow(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "flows" / "flow_delete_not_exist.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLServerException,
+ match="flow does not exist",
+ ):
+ openml.flows.delete_flow(9_999_999)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/flow/9999999",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
diff --git a/tests/test_runs/test_run_functions.py b/tests/test_runs/test_run_functions.py
index 520b7c0bc..91dd4ce5e 100644
--- a/tests/test_runs/test_run_functions.py
+++ b/tests/test_runs/test_run_functions.py
@@ -1,5 +1,4 @@
# License: BSD 3-Clause
-
import arff
from distutils.version import LooseVersion
import os
@@ -7,10 +6,11 @@
import time
import sys
import ast
-import unittest.mock
+from unittest import mock
import numpy as np
import joblib
+import requests
from joblib import parallel_backend
import openml
@@ -23,13 +23,21 @@
import pytest
import openml.extensions.sklearn
-from openml.testing import TestBase, SimpleImputer, CustomImputer
+from openml.testing import TestBase, SimpleImputer, CustomImputer, create_request_response
from openml.extensions.sklearn import cat, cont
-from openml.runs.functions import _run_task_get_arffcontent, run_exists, format_prediction
+from openml.runs.functions import (
+ _run_task_get_arffcontent,
+ run_exists,
+ format_prediction,
+ delete_run,
+)
from openml.runs.trace import OpenMLRunTrace
from openml.tasks import TaskType
from openml.testing import check_task_existence
-from openml.exceptions import OpenMLServerException
+from openml.exceptions import (
+ OpenMLServerException,
+ OpenMLNotAuthorizedError,
+)
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection._search import BaseSearchCV
@@ -708,7 +716,7 @@ def get_ct_cf(nominal_indices, numeric_indices):
LooseVersion(sklearn.__version__) < "0.20",
reason="columntransformer introduction in 0.20.0",
)
- @unittest.mock.patch("warnings.warn")
+ @mock.patch("warnings.warn")
def test_run_and_upload_knn_pipeline(self, warnings_mock):
cat_imp = make_pipeline(
@@ -1672,7 +1680,7 @@ def test_format_prediction_task_regression(self):
LooseVersion(sklearn.__version__) < "0.21",
reason="couldn't perform local tests successfully w/o bloating RAM",
)
- @unittest.mock.patch("openml.extensions.sklearn.SklearnExtension._prevent_optimize_n_jobs")
+ @mock.patch("openml.extensions.sklearn.SklearnExtension._prevent_optimize_n_jobs")
def test__run_task_get_arffcontent_2(self, parallel_mock):
"""Tests if a run executed in parallel is collated correctly."""
task = openml.tasks.get_task(7) # Supervised Classification on kr-vs-kp
@@ -1726,7 +1734,7 @@ def test__run_task_get_arffcontent_2(self, parallel_mock):
LooseVersion(sklearn.__version__) < "0.21",
reason="couldn't perform local tests successfully w/o bloating RAM",
)
- @unittest.mock.patch("openml.extensions.sklearn.SklearnExtension._prevent_optimize_n_jobs")
+ @mock.patch("openml.extensions.sklearn.SklearnExtension._prevent_optimize_n_jobs")
def test_joblib_backends(self, parallel_mock):
"""Tests evaluation of a run using various joblib backends and n_jobs."""
task = openml.tasks.get_task(7) # Supervised Classification on kr-vs-kp
@@ -1777,3 +1785,82 @@ def test_joblib_backends(self, parallel_mock):
self.assertEqual(len(res[2]["predictive_accuracy"][0]), 10)
self.assertEqual(len(res[3]["predictive_accuracy"][0]), 10)
self.assertEqual(parallel_mock.call_count, call_count)
+
+ @unittest.skipIf(
+ LooseVersion(sklearn.__version__) < "0.20",
+ reason="SimpleImputer doesn't handle mixed type DataFrame as input",
+ )
+ def test_delete_run(self):
+ rs = 1
+ clf = sklearn.pipeline.Pipeline(
+ steps=[("imputer", SimpleImputer()), ("estimator", DecisionTreeClassifier())]
+ )
+ task = openml.tasks.get_task(32) # diabetes; crossvalidation
+
+ run = openml.runs.run_model_on_task(model=clf, task=task, seed=rs)
+ run.publish()
+ TestBase._mark_entity_for_removal("run", run.run_id)
+ TestBase.logger.info("collected from test_run_functions: {}".format(run.run_id))
+
+ _run_id = run.run_id
+ self.assertTrue(delete_run(_run_id))
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_run_not_owned(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "runs" / "run_delete_not_owned.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The run can not be deleted because it was not uploaded by you.",
+ ):
+ openml.runs.delete_run(40_000)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/run/40000",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_run_success(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "runs" / "run_delete_successful.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=200, content_filepath=content_file
+ )
+
+ success = openml.runs.delete_run(10591880)
+ assert success
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/run/10591880",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_unknown_run(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "runs" / "run_delete_not_exist.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLServerException,
+ match="Run does not exist",
+ ):
+ openml.runs.delete_run(9_999_999)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/run/9999999",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
diff --git a/tests/test_tasks/test_task_functions.py b/tests/test_tasks/test_task_functions.py
index be5b0c9bd..dde3561f4 100644
--- a/tests/test_tasks/test_task_functions.py
+++ b/tests/test_tasks/test_task_functions.py
@@ -3,10 +3,13 @@
import os
from unittest import mock
+import pytest
+import requests
+
from openml.tasks import TaskType
-from openml.testing import TestBase
+from openml.testing import TestBase, create_request_response
from openml import OpenMLSplit, OpenMLTask
-from openml.exceptions import OpenMLCacheException
+from openml.exceptions import OpenMLCacheException, OpenMLNotAuthorizedError, OpenMLServerException
import openml
import unittest
import pandas as pd
@@ -253,3 +256,84 @@ def test_deletion_of_cache_dir(self):
self.assertTrue(os.path.exists(tid_cache_dir))
openml.utils._remove_cache_dir_for_id("tasks", tid_cache_dir)
self.assertFalse(os.path.exists(tid_cache_dir))
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_task_not_owned(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "tasks" / "task_delete_not_owned.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The task can not be deleted because it was not uploaded by you.",
+ ):
+ openml.tasks.delete_task(1)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/task/1",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_task_with_run(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "tasks" / "task_delete_has_runs.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLNotAuthorizedError,
+ match="The task can not be deleted because it still has associated entities:",
+ ):
+ openml.tasks.delete_task(3496)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/task/3496",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_success(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "tasks" / "task_delete_successful.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=200, content_filepath=content_file
+ )
+
+ success = openml.tasks.delete_task(361323)
+ assert success
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/task/361323",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)
+
+
+@mock.patch.object(requests.Session, "delete")
+def test_delete_unknown_task(mock_delete, test_files_directory, test_api_key):
+ openml.config.start_using_configuration_for_example()
+ content_file = test_files_directory / "mock_responses" / "tasks" / "task_delete_not_exist.xml"
+ mock_delete.return_value = create_request_response(
+ status_code=412, content_filepath=content_file
+ )
+
+ with pytest.raises(
+ OpenMLServerException,
+ match="Task does not exist",
+ ):
+ openml.tasks.delete_task(9_999_999)
+
+ expected_call_args = [
+ ("https://test.openml.org/api/v1/xml/task/9999999",),
+ {"params": {"api_key": test_api_key}},
+ ]
+ assert expected_call_args == list(mock_delete.call_args)