Repository: huggingface/dataset-viewer

Issue:
Delete the cache entries when a dataset does not exist anymore
See https://kibana.elastic.huggingface.tech/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time:(from:'2024-01-10T19:38:28.257Z',to:'2024-01-10T21:30:00.000Z'))&_a=(columns:!(message,kubernetes.namespace),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:de38ff80-ac19-11ec-bb45-ad141ad1c5f8,key:kubernetes.namespace,negate:!f,params:(query:datasets-server),type:phrase),query:(match_phrase:(kubernetes.namespace:datasets-server)))),hideChart:!f,index:de38ff80-ac19-11ec-bb45-ad141ad1c5f8,interval:auto,query:(language:kuery,query:'kubernetes.container.name%20:%20%22prod-datasets-server-backfill%22%20and%20message:%20%22failed%20to%20update_dataset%22'),sort:!(!('@timestamp',desc)))

```
WARNING: 2024-01-10 21:23:20,852 - root - failed to update_dataset feazer/nva-WRSPGR: 404 Client Error. (Request ID: Root=1-659f0ac8-663f6be5385419927dda9a27;a6f7acd2-20cc-4482-b887-58ae4d6e6144)
```

The dataset does not exist anymore. hfh raises an exception but we only catch NotSupportedError: https://github.com/huggingface/datasets-server/blob/3ee12ad19a7a4a503394a25308038d6bff5c002b/libs/libcommon/src/libcommon/operations.py#L91-L102

We should catch 404 error and delete the dataset here.


Discussion:

Resolving patch:
diff --git a/libs/libcommon/src/libcommon/exceptions.py b/libs/libcommon/src/libcommon/exceptions.py
--- a/libs/libcommon/src/libcommon/exceptions.py
+++ b/libs/libcommon/src/libcommon/exceptions.py
@@ -105,6 +105,7 @@ def as_response(self) -> ErrorResponse:
     "NotSupportedDisabledRepositoryError",
     "NotSupportedDisabledViewerError",
     "NotSupportedPrivateRepositoryError",
+    "NotSupportedRepositoryNotFoundError",
     "NormalRowsError",
     "ParameterMissingError",
     "ParquetResponseEmptyError",
@@ -542,6 +543,13 @@ def __init__(self, message: str, cause: Optional[BaseException] = None):
         super().__init__(message, HTTPStatus.NOT_IMPLEMENTED, "NotSupportedDisabledRepositoryError", cause, False)
 
 
+class NotSupportedRepositoryNotFoundError(NotSupportedError):
+    """The repository has not been found."""
+
+    def __init__(self, message: str, cause: Optional[BaseException] = None):
+        super().__init__(message, HTTPStatus.NOT_IMPLEMENTED, "NotSupportedRepositoryNotFoundError", cause, False)
+
+
 class NotSupportedDisabledViewerError(NotSupportedError):
     """The dataset viewer is disabled in the dataset configuration."""
 
diff --git a/libs/libcommon/src/libcommon/operations.py b/libs/libcommon/src/libcommon/operations.py
--- a/libs/libcommon/src/libcommon/operations.py
+++ b/libs/libcommon/src/libcommon/operations.py
@@ -5,12 +5,14 @@
 from typing import Optional
 
 from huggingface_hub.hf_api import DatasetInfo, HfApi
+from huggingface_hub.utils import HfHubHTTPError, RepositoryNotFoundError
 
 from libcommon.exceptions import (
     NotSupportedDisabledRepositoryError,
     NotSupportedDisabledViewerError,
     NotSupportedError,
     NotSupportedPrivateRepositoryError,
+    NotSupportedRepositoryNotFoundError,
 )
 from libcommon.orchestrator import get_revision, remove_dataset, set_revision
 from libcommon.storage_client import StorageClient
@@ -36,13 +38,25 @@ def get_latest_dataset_revision_if_supported_or_raise(
     hf_timeout_seconds: Optional[float] = None,
     blocked_datasets: Optional[list[str]] = None,
 ) -> str:
-    dataset_info = get_dataset_info(
-        dataset=dataset, hf_endpoint=hf_endpoint, hf_token=hf_token, hf_timeout_seconds=hf_timeout_seconds
-    )
+    try:
+        dataset_info = get_dataset_info(
+            dataset=dataset, hf_endpoint=hf_endpoint, hf_token=hf_token, hf_timeout_seconds=hf_timeout_seconds
+        )
+    except RepositoryNotFoundError as e:
+        raise NotSupportedRepositoryNotFoundError(f"Repository {dataset} is not found.", e) from e
+    except HfHubHTTPError as e:
+        response = e.response
+        if response.headers.get("X-Error-Message") == "Access to this resource is disabled.":
+            # ^ huggingface_hub does not yet provide a specific exception for this error
+            # Let's catch DisabledRepoError instead once https://github.com/huggingface/huggingface_hub/pull/1965
+            # is released.
+            raise NotSupportedDisabledRepositoryError(f"Repository {dataset} is disabled.", e) from e
+        raise
     revision = dataset_info.sha
     if not revision:
         raise ValueError(f"Cannot get the git revision of dataset {dataset}.")
     if dataset_info.disabled:
+        # ^ in most cases, get_dataset_info should already have raised. Anyway, we double-check here.
         raise NotSupportedDisabledRepositoryError(f"Not supported: dataset repository {dataset} is disabled.")
     if dataset_info.private:
         raise NotSupportedPrivateRepositoryError(f"Not supported: dataset repository {dataset} is private.")
@@ -96,8 +110,8 @@ def update_dataset(
             hf_timeout_seconds=hf_timeout_seconds,
             blocked_datasets=blocked_datasets,
         )
-    except NotSupportedError:
-        logging.warning(f"Dataset {dataset} is not supported. Let's delete the dataset.")
+    except NotSupportedError as e:
+        logging.warning(f"Dataset {dataset} is not supported ({type(e)}). Let's delete the dataset.")
         delete_dataset(dataset=dataset, storage_clients=storage_clients)
         raise
     set_revision(


Test patch:
diff --git a/libs/libcommon/tests/test_operations.py b/libs/libcommon/tests/test_operations.py
--- a/libs/libcommon/tests/test_operations.py
+++ b/libs/libcommon/tests/test_operations.py
@@ -6,11 +6,19 @@
 from contextlib import contextmanager
 from http import HTTPStatus
 from pathlib import Path
+from unittest.mock import patch
 
 import pytest
-from huggingface_hub import HfApi
-
-from libcommon.exceptions import DatasetInBlockListError, NotSupportedPrivateRepositoryError
+from huggingface_hub.hf_api import DatasetInfo, HfApi
+from huggingface_hub.utils import HfHubHTTPError
+from requests import Response  # type: ignore
+
+from libcommon.exceptions import (
+    DatasetInBlockListError,
+    NotSupportedDisabledRepositoryError,
+    NotSupportedPrivateRepositoryError,
+    NotSupportedRepositoryNotFoundError,
+)
 from libcommon.operations import delete_dataset, get_latest_dataset_revision_if_supported_or_raise, update_dataset
 from libcommon.queue import Queue
 from libcommon.resources import CacheMongoResource, QueueMongoResource
@@ -105,6 +113,50 @@ def test_update_private_raises(
             update_dataset(dataset=dataset, hf_endpoint=CI_HUB_ENDPOINT, hf_token=CI_APP_TOKEN)
 
 
+def test_update_non_existent_raises(
+    queue_mongo_resource: QueueMongoResource,
+    cache_mongo_resource: CacheMongoResource,
+) -> None:
+    with pytest.raises(NotSupportedRepositoryNotFoundError):
+        update_dataset(dataset="this-dataset-does-not-exists", hf_endpoint=CI_HUB_ENDPOINT, hf_token=CI_APP_TOKEN)
+
+
+def test_update_disabled_dataset_raises_way_1(
+    queue_mongo_resource: QueueMongoResource,
+    cache_mongo_resource: CacheMongoResource,
+) -> None:
+    dataset = "this-dataset-is-disabled"
+    with patch(
+        "libcommon.operations.get_dataset_info",
+        return_value=DatasetInfo(
+            id=dataset, sha="not-important", private=False, downloads=0, likes=0, tags=[], disabled=True
+        ),
+    ):
+        # ^ we have no programmatical way to disable a dataset, so we mock the response of the API
+        # possibly, the API can raise a 403 error code instead of returning a disabled field
+        # see following test
+        with pytest.raises(NotSupportedDisabledRepositoryError):
+            update_dataset(dataset=dataset, hf_endpoint=CI_HUB_ENDPOINT, hf_token=CI_APP_TOKEN)
+
+
+def test_update_disabled_dataset_raises_way_2(
+    queue_mongo_resource: QueueMongoResource,
+    cache_mongo_resource: CacheMongoResource,
+) -> None:
+    dataset = "this-dataset-is-disabled"
+
+    response = Response()
+    response.status_code = 403
+    response.headers["X-Error-Message"] = "Access to this resource is disabled."
+    with patch(
+        "libcommon.operations.get_dataset_info",
+        raise_exception=HfHubHTTPError("some message", response=response),
+    ):
+        # ^ we have no programmatical way to disable a dataset, so we mock the response of the API
+        with pytest.raises(NotSupportedDisabledRepositoryError):
+            update_dataset(dataset=dataset, hf_endpoint=CI_HUB_ENDPOINT, hf_token=CI_APP_TOKEN)
+
+
 @pytest.mark.parametrize(
     "token,namespace",
     [
diff --git a/services/api/tests/routes/test_endpoint.py b/services/api/tests/routes/test_endpoint.py
--- a/services/api/tests/routes/test_endpoint.py
+++ b/services/api/tests/routes/test_endpoint.py
@@ -222,7 +222,6 @@ def test_get_cache_entry_from_steps_no_cache_disabled_viewer() -> None:
         ),
     ):
         # ^ the dataset does not exist on the Hub, we don't want to raise an issue here
-        # we set the dataset as disabled
 
         with raises(NotSupportedDisabledViewerError):
             get_cache_entry_from_steps(
@@ -278,7 +277,6 @@ def test_get_cache_entry_from_steps_no_cache_private() -> None:
         return_value=DatasetInfo(id=dataset, sha=revision, private=True, downloads=0, likes=0, tags=[]),
     ):
         # ^ the dataset does not exist on the Hub, we don't want to raise an issue here
-        # we set the dataset as disabled
 
         with raises(NotSupportedPrivateRepositoryError):
             get_cache_entry_from_steps(