Repository: huggingface/huggingface_hub

Issue:
`rename_repo` function for `huggingface_hub` library
A new feature to update a repository's name with `huggingface_hub`. The behavious could be similar to `update_repo_visibility`:

```Python
from huggingface_hub import rename_repo

rename_repo(name=REPO_NAME, org=ORG,
            new_name=NEW_REPO_NAME, new_org=NEW_ORG)
```

This would allow to programmatically change the name of several repositories on the hub with ease.

cc @osanseviero


Discussion:

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
@@ -50,6 +50,7 @@
     login,
     logout,
     model_info,
+    move_repo,
     repo_type_and_id_from_hf_id,
     update_repo_visibility,
     upload_file,
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
@@ -459,6 +459,31 @@ def whoami(self, token: Optional[str] = None) -> Dict:
             ) from e
         return r.json()
 
+    def _is_valid_token(self, token: str):
+        """
+        Determines whether `token` is a valid token or not.
+        """
+        try:
+            self.whoami(token=token)
+            return True
+        except HTTPError:
+            return False
+
+    def _validate_or_retrieve_token(self, token: Optional[Union[str, bool]] = None):
+        """
+        Either retrieves stored token or validates passed token.
+        """
+        if token is None or token is True:
+            token = HfFolder.get_token()
+            if token is None:
+                raise EnvironmentError(
+                    "You need to provide a `token` or be logged in to Hugging Face with "
+                    "`huggingface-cli login`."
+                )
+        elif not self._is_valid_token(token):
+            raise ValueError("Invalid token passed!")
+        return token
+
     def logout(self, token: Optional[str] = None) -> None:
         """
         Call HF API to log out.
@@ -612,17 +637,7 @@ def list_models(
         """
         path = f"{self.endpoint}/api/models"
         if use_auth_token:
-            if isinstance(use_auth_token, str):
-                if not self._is_valid_token(use_auth_token):
-                    raise ValueError("Invalid token passed!")
-                token = use_auth_token
-            else:
-                token = HfFolder.get_token()
-                if token is None:
-                    raise EnvironmentError(
-                        "You need to provide a `token` to `use_auth_token` or be logged in to Hugging Face with "
-                        "`huggingface-cli login`."
-                    )
+            token = self._validate_or_retrieve_token(use_auth_token)
         headers = {"authorization": f"Bearer {token}"} if use_auth_token else None
         params = {}
         if filter is not None:
@@ -813,17 +828,7 @@ def list_datasets(
         """
         path = f"{self.endpoint}/api/datasets"
         if use_auth_token:
-            if isinstance(use_auth_token, str):
-                if not self._is_valid_token(use_auth_token):
-                    raise ValueError("Invalid token passed!")
-                token = use_auth_token
-            else:
-                token = HfFolder.get_token()
-                if token is None:
-                    raise EnvironmentError(
-                        "You need to provide a `token` to `use_auth_token` or be logged in to Hugging Face with "
-                        "`huggingface-cli login`."
-                    )
+            token = self._validate_or_retrieve_token(use_auth_token)
         headers = {"authorization": f"Bearer {token}"} if use_auth_token else None
         params = {}
         if filter is not None:
@@ -970,6 +975,9 @@ def dataset_info(
 
         Dataset can be private if you pass an acceptable token.
         """
+        if token is None:
+            token = HfFolder.get_token()
+
         path = (
             f"{self.endpoint}/api/datasets/{repo_id}"
             if revision is None
@@ -982,16 +990,6 @@ def dataset_info(
         d = r.json()
         return DatasetInfo(**d)
 
-    def _is_valid_token(self, token: str):
-        """
-        Determines whether `token` is a valid token or not.
-        """
-        try:
-            self.whoami(token=token)
-            return True
-        except HTTPError:
-            return False
-
     def create_repo(
         self,
         name: str,
@@ -1024,12 +1022,7 @@ def create_repo(
         """
         path = f"{self.endpoint}/api/repos/create"
         if token is None:
-            token = HfFolder.get_token()
-            if token is None:
-                raise EnvironmentError(
-                    "You need to provide a `token` or be logged in to Hugging Face with "
-                    "`huggingface-cli login`."
-                )
+            token = self._validate_or_retrieve_token()
         elif not self._is_valid_token(token):
             if self._is_valid_token(name):
                 warnings.warn(
@@ -1130,12 +1123,7 @@ def delete_repo(
         """
         path = f"{self.endpoint}/api/repos/delete"
         if token is None:
-            token = HfFolder.get_token()
-            if token is None:
-                raise EnvironmentError(
-                    "You need to provide a `token` or be logged in to Hugging Face with "
-                    "`huggingface-cli login`."
-                )
+            token = self._validate_or_retrieve_token()
         elif not self._is_valid_token(token):
             if self._is_valid_token(name):
                 warnings.warn(
@@ -1202,12 +1190,7 @@ def update_repo_visibility(
             raise ValueError("Invalid repo type")
 
         if token is None:
-            token = HfFolder.get_token()
-            if token is None:
-                raise EnvironmentError(
-                    "You need to provide a `token` or be logged in to Hugging Face with "
-                    "`huggingface-cli login`."
-                )
+            token = self._validate_or_retrieve_token()
         elif not self._is_valid_token(token):
             if self._is_valid_token(name):
                 warnings.warn(
@@ -1240,6 +1223,52 @@ def update_repo_visibility(
         r.raise_for_status()
         return r.json()
 
+    def move_repo(
+        self,
+        from_id: str,
+        to_id: str,
+        repo_type: Optional[str] = None,
+        token: Optional[str] = None,
+    ):
+        """
+        Moving a repository from namespace1/repo_name1 to namespace2/repo_name2
+
+        Note there are certain limitations. For more information about moving repositories, please
+        see https://hf.co/docs/hub/main#how-can-i-rename-or-transfer-a-repo.
+        """
+        token = self._validate_or_retrieve_token(token)
+
+        if len(from_id.split("/")) != 2:
+            raise ValueError(
+                f"Invalid repo_id: {from_id}. It should have a namespace (:namespace:/:repo_name:)"
+            )
+
+        if len(to_id.split("/")) != 2:
+            raise ValueError(
+                f"Invalid repo_id: {to_id}. It should have a namespace (:namespace:/:repo_name:)"
+            )
+
+        json = {"fromRepo": from_id, "toRepo": to_id, "type": repo_type}
+
+        path = f"{self.endpoint}/api/repos/move"
+        r = requests.post(
+            path,
+            headers={"authorization": f"Bearer {token}"},
+            json=json,
+        )
+        try:
+            r.raise_for_status()
+        except HTTPError as e:
+            if r.text:
+                raise HTTPError(
+                    f"{r.status_code} Error Message: {r.text}. For additional documentation please see https://hf.co/docs/hub/main#how-can-i-rename-or-transfer-a-repo."
+                )
+            else:
+                raise e
+        logging.info(
+            "Accepted transfer request. You will get an email once this is successfully completed."
+        )
+
     def upload_file(
         self,
         path_or_fileobj: Union[str, bytes, IO],
@@ -1251,58 +1280,56 @@ def upload_file(
         identical_ok: bool = True,
     ) -> str:
         """
-        Upload a local file (up to 5GB) to the given repo. The upload is done through a HTTP post request, and
-        doesn't require git or git-lfs to be installed.
+                Upload a local file (up to 5GB) to the given repo. The upload is done through a HTTP post request, and
+                doesn't require git or git-lfs to be installed.
+        upload_fileth_or_fileobj (``str``, ``bytes``, or ``IO``):
+                        Path to a file on the local machine or binary data stream / fileobj / buffer.
 
-        Params:
-            path_or_fileobj (``str``, ``bytes``, or ``IO``):
-                Path to a file on the local machine or binary data stream / fileobj / buffer.
+                    path_in_repo (``str``):
+                        Relative filepath in the repo, for example: :obj:`"checkpoints/1fec34a/weights.bin"`
 
-            path_in_repo (``str``):
-                Relative filepath in the repo, for example: :obj:`"checkpoints/1fec34a/weights.bin"`
+                    repo_id (``str``):
+                        The repository to which the file will be uploaded, for example: :obj:`"username/custom_transformers"`
 
-            repo_id (``str``):
-                The repository to which the file will be uploaded, for example: :obj:`"username/custom_transformers"`
+                    token (``str``):
+                        Authentication token, obtained with :function:`HfApi.login` method. Will default to the stored token.
 
-            token (``str``):
-                Authentication token, obtained with :function:`HfApi.login` method. Will default to the stored token.
+                    repo_type (``str``, Optional):
+                        Set to :obj:`"dataset"` or :obj:`"space"` if uploading to a dataset or space, :obj:`None` or :obj:`"model"` if uploading to a model. Default is :obj:`None`.
 
-            repo_type (``str``, Optional):
-                Set to :obj:`"dataset"` or :obj:`"space"` if uploading to a dataset or space, :obj:`None` or :obj:`"model"` if uploading to a model. Default is :obj:`None`.
+                    revision (``str``, Optional):
+                        The git revision to commit from. Defaults to the :obj:`"main"` branch.
 
-            revision (``str``, Optional):
-                The git revision to commit from. Defaults to the :obj:`"main"` branch.
+                    identical_ok (``bool``, defaults to ``True``):
+                        When set to false, will raise an HTTPError when the file you're trying to upload already exists on the hub
+                        and its content did not change.
 
-            identical_ok (``bool``, defaults to ``True``):
-                When set to false, will raise an HTTPError when the file you're trying to upload already exists on the hub
-                and its content did not change.
+                Returns:
+                    ``str``: The URL to visualize the uploaded file on the hub
 
-        Returns:
-            ``str``: The URL to visualize the uploaded file on the hub
+                Raises:
+                    :class:`ValueError`: if some parameter value is invalid
 
-        Raises:
-            :class:`ValueError`: if some parameter value is invalid
+                    :class:`requests.HTTPError`: if the HuggingFace API returned an error
 
-            :class:`requests.HTTPError`: if the HuggingFace API returned an error
+                Examples:
+                    >>> with open("./local/filepath", "rb") as fobj:
+                    ...     upload_file(
+                    ...         path_or_fileobj=fileobj,
+                    ...         path_in_repo="remote/file/path.h5",
+                    ...         repo_id="username/my-dataset",
+                    ...         repo_type="datasets",
+                    ...         token="my_token",
+                    ...    )
+                    "https://huggingface.co/datasets/username/my-dataset/blob/main/remote/file/path.h5"
 
-        Examples:
-            >>> with open("./local/filepath", "rb") as fobj:
-            ...     upload_file(
-            ...         path_or_fileobj=fileobj,
-            ...         path_in_repo="remote/file/path.h5",
-            ...         repo_id="username/my-dataset",
-            ...         repo_type="datasets",
-            ...         token="my_token",
-            ...    )
-            "https://huggingface.co/datasets/username/my-dataset/blob/main/remote/file/path.h5"
-
-            >>> upload_file(
-            ...     path_or_fileobj=".\\\\local\\\\file\\\\path",
-            ...     path_in_repo="remote/file/path.h5",
-            ...     repo_id="username/my-model",
-            ...     token="my_token",
-            ... )
-            "https://huggingface.co/username/my-model/blob/main/remote/file/path.h5"
+                    >>> upload_file(
+                    ...     path_or_fileobj=".\\\\local\\\\file\\\\path",
+                    ...     path_in_repo="remote/file/path.h5",
+                    ...     repo_id="username/my-model",
+                    ...     token="my_token",
+                    ... )
+                    "https://huggingface.co/username/my-model/blob/main/remote/file/path.h5"
 
 
         """
@@ -1310,12 +1337,7 @@ def upload_file(
             raise ValueError(f"Invalid repo type, must be one of {REPO_TYPES}")
 
         if token is None:
-            token = HfFolder.get_token()
-            if token is None:
-                raise EnvironmentError(
-                    "You need to provide a `token` or be logged in to Hugging Face with "
-                    "`huggingface-cli login`."
-                )
+            token = self._validate_or_retrieve_token()
         elif not self._is_valid_token(token):
             if self._is_valid_token(path_or_fileobj):
                 warnings.warn(
@@ -1411,12 +1433,7 @@ def delete_file(
             raise ValueError(f"Invalid repo type, must be one of {REPO_TYPES}")
 
         if token is None:
-            token = HfFolder.get_token()
-            if token is None:
-                raise EnvironmentError(
-                    "You need to provide a `token` or be logged in to Hugging Face with "
-                    "`huggingface-cli login`."
-                )
+            token = self._validate_or_retrieve_token()
 
         if repo_type in REPO_TYPES_URL_PREFIXES:
             repo_id = REPO_TYPES_URL_PREFIXES[repo_type] + repo_id
@@ -1518,6 +1535,7 @@ def delete_token(cls):
 create_repo = api.create_repo
 delete_repo = api.delete_repo
 update_repo_visibility = api.update_repo_visibility
+move_repo = api.move_repo
 upload_file = api.upload_file
 delete_file = api.delete_file
 get_full_repo_name = api.get_full_repo_name


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
@@ -258,6 +258,39 @@ def test_create_update_and_delete_space_repo(self):
                 name=SPACE_REPO_NAME, token=self._token, repo_type=REPO_TYPE_SPACE
             )
 
+    def test_move_repo(self):
+        REPO_NAME = repo_name("crud")
+        repo_id = f"__DUMMY_TRANSFORMERS_USER__/{REPO_NAME}"
+        NEW_REPO_NAME = repo_name("crud2")
+        new_repo_id = f"__DUMMY_TRANSFORMERS_USER__/{NEW_REPO_NAME}"
+
+        for repo_type in [REPO_TYPE_MODEL, REPO_TYPE_DATASET, REPO_TYPE_SPACE]:
+            self._api.create_repo(
+                name=REPO_NAME,
+                token=self._token,
+                repo_type=repo_type,
+                space_sdk="static",
+            )
+
+            with pytest.raises(ValueError, match=r"Invalid repo_id*"):
+                self._api.move_repo(
+                    from_id=repo_id,
+                    to_id="invalid_repo_id",
+                    token=self._token,
+                    repo_type=repo_type,
+                )
+
+            # Should raise an error if it fails
+            self._api.move_repo(
+                from_id=repo_id,
+                to_id=new_repo_id,
+                token=self._token,
+                repo_type=repo_type,
+            )
+            self._api.delete_repo(
+                name=NEW_REPO_NAME, token=self._token, repo_type=repo_type
+            )
+
 
 class HfApiUploadFileTest(HfApiCommonTestWithLogin):
     def setUp(self) -> None: