Repository: huggingface/huggingface_hub

Issue:
Add support for Space Persistent Storage
The Hub recently introduced Persistent Storage as a way to plug a hard drive that will persist between reruns of your Space. This is in addition to the existing Space storage that is free but ephemeral and the classic repositories (e.g. a dataset) that are free, versioned and unlimited but not meant to be used as database. More details can be found [here](https://huggingface.co/docs/hub/spaces-storage).

Let's support Space Persistent Storage in `huggingface_hub`. To do so, we should have:
- a new `request_space_storage` method that takes as input a repo_id and desired storage. Implementation is very similar to `request_space_hardware`. Requested storage must be sent with key `"tier"` in the json body. Signature would look like this:
```py
def request_space_storage(self, repo_id: str, storage: SpaceStorage, *, token: Optional[str] = None) -> SpaceRuntime:
    # POST /api/spaces/{repo_id}/storage
    # json={"tier": storage}
    ...
```
- a new `delete_space_storage` method
```py
def delete_space_storage(self, repo_id: str, *, token: Optional[str] = None) -> SpaceRuntime:
    # DELETE /api/spaces/{repo_id}/storage
    ...
```
- `SpaceStorage` should be defined as a StrEnum the same way `SpaceStorage` is. Possible values are `"small"`, `"medium"` and `"large"`.
- `storage` key must be added to `SpaceRuntime` dataclass
- It must be documented that a storage cannot be decreased in size. To do so, a user would have to delete it first -hence loosing all the data.

---
**Note:** a separate issue has been created to support persistent storage when creation/duplicating a Space. See https://github.com/huggingface/huggingface_hub/issues/1457.


Discussion:
would be an awesome first issue if someone wants to work on it!

Resolving patch:
diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py
--- a/src/huggingface_hub/__init__.py
+++ b/src/huggingface_hub/__init__.py
@@ -72,6 +72,7 @@
         "SpaceHardware",
         "SpaceRuntime",
         "SpaceStage",
+        "SpaceStorage",
     ],
     "_tensorboard_logger": [
         "HFSummaryWriter",
@@ -156,6 +157,7 @@
         "delete_folder",
         "delete_repo",
         "delete_space_secret",
+        "delete_space_storage",
         "delete_tag",
         "duplicate_space",
         "edit_discussion_comment",
@@ -184,6 +186,7 @@
         "repo_info",
         "repo_type_and_id_from_hf_id",
         "request_space_hardware",
+        "request_space_storage",
         "restart_space",
         "run_as_future",
         "set_space_sleep_time",
@@ -373,6 +376,7 @@ def __dir__():
         SpaceHardware,  # noqa: F401
         SpaceRuntime,  # noqa: F401
         SpaceStage,  # noqa: F401
+        SpaceStorage,  # noqa: F401
     )
     from ._tensorboard_logger import HFSummaryWriter  # noqa: F401
     from ._webhooks_payload import (
@@ -455,6 +459,7 @@ def __dir__():
         delete_folder,  # noqa: F401
         delete_repo,  # noqa: F401
         delete_space_secret,  # noqa: F401
+        delete_space_storage,  # noqa: F401
         delete_tag,  # noqa: F401
         duplicate_space,  # noqa: F401
         edit_discussion_comment,  # noqa: F401
@@ -483,6 +488,7 @@ def __dir__():
         repo_info,  # noqa: F401
         repo_type_and_id_from_hf_id,  # noqa: F401
         request_space_hardware,  # noqa: F401
+        request_space_storage,  # noqa: F401
         restart_space,  # noqa: F401
         run_as_future,  # noqa: F401
         set_space_sleep_time,  # noqa: F401
diff --git a/src/huggingface_hub/_space_api.py b/src/huggingface_hub/_space_api.py
--- a/src/huggingface_hub/_space_api.py
+++ b/src/huggingface_hub/_space_api.py
@@ -63,6 +63,23 @@ class SpaceHardware(str, Enum):
     A100_LARGE = "a100-large"
 
 
+class SpaceStorage(str, Enum):
+    """
+    Enumeration of persistent storage available for your Space on the Hub.
+
+    Value can be compared to a string:
+    ```py
+    assert SpaceStorage.SMALL == "small"
+    ```
+
+    Taken from https://github.com/huggingface/moon-landing/blob/main/server/repo_types/SpaceHardwareFlavor.ts#L24 (private url).
+    """
+
+    SMALL = "small"
+    MEDIUM = "medium"
+    LARGE = "large"
+
+
 @dataclass
 class SpaceRuntime:
     """
@@ -91,6 +108,7 @@ class SpaceRuntime:
     hardware: Optional[SpaceHardware]
     requested_hardware: Optional[SpaceHardware]
     sleep_time: Optional[int]
+    storage: Optional[SpaceStorage]
     raw: Dict
 
     def __init__(self, data: Dict) -> None:
@@ -98,4 +116,5 @@ def __init__(self, data: Dict) -> None:
         self.hardware = data["hardware"]["current"]
         self.requested_hardware = data["hardware"]["requested"]
         self.sleep_time = data["gcTimeout"]
+        self.storage = data["storage"]
         self.raw = data
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -80,7 +80,7 @@
     multi_commit_parse_pr_description,
     plan_multi_commits,
 )
-from ._space_api import SpaceHardware, SpaceRuntime
+from ._space_api import SpaceHardware, SpaceRuntime, SpaceStorage
 from .community import (
     Discussion,
     DiscussionComment,
@@ -5001,6 +5001,70 @@ def duplicate_space(
 
         return RepoUrl(r.json()["url"], endpoint=self.endpoint)
 
+    @validate_hf_hub_args
+    def request_space_storage(
+        self,
+        repo_id: str,
+        storage: SpaceStorage,
+        *,
+        token: Optional[str] = None,
+    ) -> SpaceRuntime:
+        """Request persistent storage for a Space.
+
+        Args:
+            repo_id (`str`):
+                ID of the Space to update. Example: `"HuggingFaceH4/open_llm_leaderboard"`.
+            storage (`str` or [`SpaceStorage`]):
+               Storage tier. Either 'small', 'medium', or 'large'.
+            token (`str`, *optional*):
+                Hugging Face token. Will default to the locally saved token if not provided.
+        Returns:
+            [`SpaceRuntime`]: Runtime information about a Space including Space stage and hardware.
+
+        <Tip>
+
+        It is not possible to decrease persistent storage after its granted. To do so, you must delete it
+        via [`delete_space_storage`].
+
+        </Tip>
+        """
+        payload: Dict[str, SpaceStorage] = {"tier": storage}
+        r = get_session().post(
+            f"{self.endpoint}/api/spaces/{repo_id}/storage",
+            headers=self._build_hf_headers(token=token),
+            json=payload,
+        )
+        hf_raise_for_status(r)
+        return SpaceRuntime(r.json())
+
+    @validate_hf_hub_args
+    def delete_space_storage(
+        self,
+        repo_id: str,
+        *,
+        token: Optional[str] = None,
+    ) -> SpaceRuntime:
+        """Delete persistent storage for a Space.
+
+        Args:
+            repo_id (`str`):
+                ID of the Space to update. Example: `"HuggingFaceH4/open_llm_leaderboard"`.
+            token (`str`, *optional*):
+                Hugging Face token. Will default to the locally saved token if not provided.
+        Returns:
+            [`SpaceRuntime`]: Runtime information about a Space including Space stage and hardware.
+        Raises:
+            [`BadRequestError`]
+                If space has no persistent storage.
+
+        """
+        r = get_session().delete(
+            f"{self.endpoint}/api/spaces/{repo_id}/storage",
+            headers=self._build_hf_headers(token=token),
+        )
+        hf_raise_for_status(r)
+        return SpaceRuntime(r.json())
+
     def _build_hf_headers(
         self,
         token: Optional[Union[bool, str]] = None,
@@ -5185,3 +5249,5 @@ def _parse_revision_from_pr_url(pr_url: str) -> str:
 pause_space = api.pause_space
 restart_space = api.restart_space
 duplicate_space = api.duplicate_space
+request_space_storage = api.request_space_storage
+delete_space_storage = api.delete_space_storage


Test patch:
diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py
--- a/tests/test_hf_api.py
+++ b/tests/test_hf_api.py
@@ -33,7 +33,7 @@
 from requests.exceptions import HTTPError
 
 import huggingface_hub.lfs
-from huggingface_hub import SpaceHardware, SpaceStage
+from huggingface_hub import SpaceHardware, SpaceStage, SpaceStorage
 from huggingface_hub._commit_api import (
     CommitOperationAdd,
     CommitOperationCopy,
@@ -2533,6 +2533,20 @@ def setUp(self) -> None:
                 "current": "t4-medium",
                 "requested": "t4-medium",
             },
+            "storage": "large",
+            "gcTimeout": None,
+        }
+        self.delete_mock = get_session_mock().delete
+        self.delete_mock.return_value.json.return_value = {
+            "url": f"{self.api.endpoint}/spaces/user/repo_id",
+            "stage": "RUNNING",
+            "sdk": "gradio",
+            "sdkVersion": "3.17.0",
+            "hardware": {
+                "current": "t4-medium",
+                "requested": "t4-medium",
+            },
+            "storage": None,
             "gcTimeout": None,
         }
         self.patcher = patch("huggingface_hub.hf_api.get_session", get_session_mock)
@@ -2590,6 +2604,23 @@ def test_set_space_sleep_time_cpu_basic(self) -> None:
         with self.assertWarns(UserWarning):
             self.api.set_space_sleep_time(self.repo_id, sleep_time=123)
 
+    def test_request_space_storage(self) -> None:
+        runtime = self.api.request_space_storage(self.repo_id, SpaceStorage.LARGE)
+        self.post_mock.assert_called_once_with(
+            f"{self.api.endpoint}/api/spaces/{self.repo_id}/storage",
+            headers=self.api._build_hf_headers(),
+            json={"tier": "large"},
+        )
+        assert runtime.storage == SpaceStorage.LARGE
+
+    def test_delete_space_storage(self) -> None:
+        runtime = self.api.delete_space_storage(self.repo_id)
+        self.delete_mock.assert_called_once_with(
+            f"{self.api.endpoint}/api/spaces/{self.repo_id}/storage",
+            headers=self.api._build_hf_headers(),
+        )
+        assert runtime.storage is None
+
 
 class ListGitRefsTest(unittest.TestCase):
     @classmethod