Repository: huggingface/huggingface_hub (python)

Issue:
Support User API endpoints
A few user endpoints have been adding server-side over the past few months. Would be good to support them natively in `huggingface_hub`, especially for Spaces that rely on social feats.

Here is a list of them:
- [ ] `/overview` (e.g. https://huggingface.co/api/users/julien-c/overview). Returns details about a user on the Hub: user id, name, profile, avatar url, is_pro, nb_models, nb_datasets, ..., nb_likes, etc.
- [ ] `/likes` (e.g. https://huggingface.co/api/users/julien-c/likes). Returns all repos liked by a user.
- [ ] `/followers` (e.g. https://huggingface.co/api/users/julien-c/followers). Returns all followers of a user.
- [ ] `/following` (e.g. https://huggingface.co/api/users/julien-c/following). Returns all users followed by a user.
- [ ] `/api/organizations/:name/members` (e.g. https://huggingface.co/api/organizations/huggingface/members). List of member of an org.
  
Each endpoint can be added as a separate PR. Return type should always be a dataclass. Might also be good to take the opportunity to document those endpoints in [api.md](https://github.com/huggingface/hub-docs/blob/main/docs/hub/api.md) (separate repo).

More priority on the `/overview` and `/members` endpoints. Lower prio for the others.

Pull request description:
User API endpoints
User API endpoints are implemented. 

`/likes` endpoint was already implemented on `hf_api.py:list_liked_repos:2078`. 

Resolves #2077 


Patch:
diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py
index 56378b05..456ad71d 100644
--- a/src/huggingface_hub/__init__.py
+++ b/src/huggingface_hub/__init__.py
@@ -46,7 +46,7 @@ import sys
 from typing import TYPE_CHECKING
 
 
-__version__ = "0.22.0.dev0"
+__version__ = "0.23.0.dev0"
 
 # Alphabetical order of definitions is ensured in tests
 # WARNING: any comment added in this dictionary definition will be lost when
@@ -198,7 +198,6 @@ _SUBMOD_ATTRS = {
         "list_accepted_access_requests",
         "list_collections",
         "list_datasets",
-        "list_files_info",
         "list_inference_endpoints",
         "list_liked_repos",
         "list_metrics",
@@ -662,7 +661,6 @@ if TYPE_CHECKING:  # pragma: no cover
         list_accepted_access_requests,  # noqa: F401
         list_collections,  # noqa: F401
         list_datasets,  # noqa: F401
-        list_files_info,  # noqa: F401
         list_inference_endpoints,  # noqa: F401
         list_liked_repos,  # noqa: F401
         list_metrics,  # noqa: F401
diff --git a/src/huggingface_hub/_commit_api.py b/src/huggingface_hub/_commit_api.py
index c7333e0e..b807106b 100644
--- a/src/huggingface_hub/_commit_api.py
+++ b/src/huggingface_hub/_commit_api.py
@@ -399,7 +399,7 @@ def _upload_lfs_files(
     def _wrapped_lfs_upload(batch_action) -> None:
         try:
             operation = oid2addop[batch_action["oid"]]
-            lfs_upload(operation=operation, lfs_batch_action=batch_action, headers=headers)
+            lfs_upload(operation=operation, lfs_batch_action=batch_action, headers=headers, endpoint=endpoint)
         except Exception as exc:
             raise RuntimeError(f"Error while uploading '{operation.path_in_repo}' to the Hub.") from exc
 
diff --git a/src/huggingface_hub/_login.py b/src/huggingface_hub/_login.py
index 0f4fa025..84814056 100644
--- a/src/huggingface_hub/_login.py
+++ b/src/huggingface_hub/_login.py
@@ -103,9 +103,10 @@ def login(
     if token is not None:
         if not add_to_git_credential:
             print(
-                "Token has not been saved to git credential helper. Pass"
-                " `add_to_git_credential=True` if you want to set the git"
-                " credential as well."
+                "The token has not been saved to the git credentials helper. Pass "
+                "`add_to_git_credential=True` in this function directly or "
+                "`--add-to-git-credential` if using via `huggingface-cli` if "
+                "you want to set the git credential as well."
             )
         _login(token, add_to_git_credential=add_to_git_credential, write_permission=write_permission)
     elif is_notebook():
diff --git a/src/huggingface_hub/_webhooks_server.py b/src/huggingface_hub/_webhooks_server.py
index fefb0388..222f0962 100644
--- a/src/huggingface_hub/_webhooks_server.py
+++ b/src/huggingface_hub/_webhooks_server.py
@@ -21,7 +21,6 @@ from functools import wraps
 from typing import TYPE_CHECKING, Any, Callable, Dict, Optional
 
 from .utils import experimental, is_gradio_available
-from .utils._deprecation import _deprecate_method
 
 
 if TYPE_CHECKING:
@@ -41,7 +40,7 @@ class WebhooksServer:
     """
     The [`WebhooksServer`] class lets you create an instance of a Gradio app that can receive Huggingface webhooks.
     These webhooks can be registered using the [`~WebhooksServer.add_webhook`] decorator. Webhook endpoints are added to
-    the app as a POST endpoint to the FastAPI router. Once all the webhooks are registered, the `run` method has to be
+    the app as a POST endpoint to the FastAPI router. Once all the webhooks are registered, the `launch` method has to be
     called to start the app.
 
     It is recommended to accept [`WebhookPayload`] as the first argument of the webhook function. It is a Pydantic
@@ -87,7 +86,7 @@ class WebhooksServer:
         async def hello(payload: WebhookPayload):
             return {"message": "hello"}
 
-        app.run()
+        app.launch()
         ```
     """
 
@@ -134,7 +133,7 @@ class WebhooksServer:
                     # Trigger a training job if a dataset is updated
                     ...
 
-            app.run()
+            app.launch()
         ```
         """
         # Usage: directly as decorator. Example: `@app.add_webhook`
@@ -185,10 +184,6 @@ class WebhooksServer:
         if not prevent_thread_lock:
             ui.block_thread()
 
-    @_deprecate_method(version="0.23", message="Use `WebhooksServer.launch` instead.")
-    def run(self) -> None:
-        return self.launch()
-
     def _get_default_ui(self) -> "gr.Blocks":
         """Default UI if not provided (lists webhooks and provides basic instructions)."""
         import gradio as gr
@@ -278,7 +273,7 @@ def webhook_endpoint(path: Optional[str] = None) -> Callable:
                 ...
 
         # Start the server manually
-        trigger_training.run()
+        trigger_training.launch()
         ```
     """
     if callable(path):
@@ -290,16 +285,16 @@ def webhook_endpoint(path: Optional[str] = None) -> Callable:
         app = _get_global_app()
         app.add_webhook(path)(func)
         if len(app.registered_webhooks) == 1:
-            # Register `app.run` to run at exit (only once)
-            atexit.register(app.run)
+            # Register `app.launch` to run at exit (only once)
+            atexit.register(app.launch)
 
-        @wraps(app.run)
-        def _run_now():
+        @wraps(app.launch)
+        def _launch_now():
             # Run the app directly (without waiting atexit)
-            atexit.unregister(app.run)
-            app.run()
+            atexit.unregister(app.launch)
+            app.launch()
 
-        func.run = _run_now  # type: ignore
+        func.launch = _launch_now  # type: ignore
         return func
 
     return _inner
diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py
index 5b00c489..91f909d1 100644
--- a/src/huggingface_hub/hf_api.py
+++ b/src/huggingface_hub/hf_api.py
@@ -82,8 +82,6 @@ from .community import (
     deserialize_event,
 )
 from .constants import (
-    _HF_DEFAULT_ENDPOINT,
-    _HF_DEFAULT_STAGING_ENDPOINT,
     DEFAULT_ETAG_TIMEOUT,
     DEFAULT_REQUEST_TIMEOUT,
     DEFAULT_REVISION,
@@ -123,6 +121,7 @@ from .utils import (  # noqa: F401 # imported for backward compatibility
     build_hf_headers,
     experimental,
     filter_repo_objects,
+    fix_hf_endpoint_in_url,
     get_session,
     hf_raise_for_status,
     logging,
@@ -131,7 +130,7 @@ from .utils import (  # noqa: F401 # imported for backward compatibility
     validate_hf_hub_args,
 )
 from .utils import tqdm as hf_tqdm
-from .utils._deprecation import _deprecate_arguments, _deprecate_method
+from .utils._deprecation import _deprecate_arguments
 from .utils._typing import CallableT
 from .utils.endpoint_helpers import (
     DatasetFilter,
@@ -185,11 +184,6 @@ def repo_type_and_id_from_hf_id(hf_id: str, hub_url: Optional[str] = None) -> Tu
     """
     input_hf_id = hf_id
 
-    # check if a proxy has been set => if yes, update the returned URL to use the proxy
-    if ENDPOINT not in (_HF_DEFAULT_ENDPOINT, _HF_DEFAULT_STAGING_ENDPOINT):
-        hf_id = hf_id.replace(_HF_DEFAULT_ENDPOINT, ENDPOINT)
-        hf_id = hf_id.replace(_HF_DEFAULT_STAGING_ENDPOINT, ENDPOINT)
-
     hub_url = re.sub(r"https?://", "", hub_url if hub_url is not None else ENDPOINT)
     is_hf_url = hub_url in hf_id and "@" not in hf_id
 
@@ -436,6 +430,7 @@ class RepoUrl(str):
     """
 
     def __new__(cls, url: Any, endpoint: Optional[str] = None):
+        url = fix_hf_endpoint_in_url(url, endpoint=endpoint)
         return super(RepoUrl, cls).__new__(cls, url)
 
     def __init__(self, url: Any, endpoint: Optional[str] = None) -> None:
@@ -505,10 +500,10 @@ class RepoFile:
         lfs (`BlobLfsInfo`):
             The file's LFS metadata.
         last_commit (`LastCommitInfo`, *optional*):
-            The file's last commit metadata. Only defined if [`list_files_info`], [`list_repo_tree`] and [`get_paths_info`]
+            The file's last commit metadata. Only defined if [`list_repo_tree`] and [`get_paths_info`]
             are called with `expand=True`.
         security (`BlobSecurityInfo`, *optional*):
-            The file's security scan metadata. Only defined if [`list_files_info`], [`list_repo_tree`] and [`get_paths_info`]
+            The file's security scan metadata. Only defined if [`list_repo_tree`] and [`get_paths_info`]
             are called with `expand=True`.
     """
 
@@ -1197,12 +1192,61 @@ class User:
             Name of the user on the Hub (unique).
         fullname (`str`):
             User's full name.
+        is_pro (`bool`, *optional*):
+            Whether the user is a pro user.
+        num_models (`int`, *optional*):
+            Number of models created by the user.
+        num_datasets (`int`, *optional*):
+            Number of datasets created by the user.
+        num_spaces (`int`, *optional*):
+            Number of spaces created by the user.
+        num_discussions (`int`, *optional*):
+            Number of discussions initiated by the user.
+        num_papers (`int`, *optional*):
+            Number of papers authored by the user.
+        num_upvotes (`int`, *optional*):
+            Number of upvotes received by the user.
+        num_likes (`int`, *optional*):
+            Number of likes given by the user.
+        is_following (`bool`, *optional*):
+            Whether the authenticated user is following this user.
+        details (`str`, *optional*):
+            User's details.
     """
 
     # Metadata
     avatar_url: str
     username: str
     fullname: str
+    is_pro: Optional[bool] = None
+    num_models: Optional[int] = None
+    num_datasets: Optional[int] = None
+    num_spaces: Optional[int] = None
+    num_discussions: Optional[int] = None
+    num_papers: Optional[int] = None
+    num_upvotes: Optional[int] = None
+    num_likes: Optional[int] = None
+    is_following: Optional[bool] = None
+    details: Optional[str] = None
+
+    def __init__(self, **kwargs) -> None:
+        self.avatar_url = kwargs.get("avatarUrl", "")
+        self.username = kwargs.get("user", "")
+        self.fullname = kwargs.get("fullname", "")
+        self.is_pro = kwargs.get("isPro")
+        self.num_models = kwargs.get("numModels")
+        self.num_datasets = kwargs.get("numDatasets")
+        self.num_spaces = kwargs.get("numSpaces")
+        self.num_discussions = kwargs.get("numDiscussions")
+        self.num_papers = kwargs.get("numPapers")
+        self.num_upvotes = kwargs.get("numUpvotes")
+        self.num_likes = kwargs.get("numLikes")
+        self.user_type = kwargs.get("type")
+        self.is_following = kwargs.get("isFollowing")
+        self.details = kwargs.get("details")
+
+        # forward compatibility
+        self.__dict__.update(**kwargs)
 
 
 def future_compatible(fn: CallableT) -> CallableT:
@@ -2577,190 +2621,6 @@ class HfApi:
         except (RepositoryNotFoundError, EntryNotFoundError, RevisionNotFoundError):
             return False
 
-    @validate_hf_hub_args
-    @_deprecate_method(version="0.23", message="Use `list_repo_tree` and `get_paths_info` instead.")
-    def list_files_info(
-        self,
-        repo_id: str,
-        paths: Union[List[str], str, None] = None,
-        *,
-        expand: bool = False,
-        revision: Optional[str] = None,
-        repo_type: Optional[str] = None,
-        token: Optional[Union[bool, str]] = None,
-    ) -> Iterable[RepoFile]:
-        """
-        List files on a repo and get information about them.
-
-        Takes as input a list of paths. Those paths can be either files or folders. Two server endpoints are called:
-        1. POST "/paths-info" to get information about the provided paths. Called once.
-        2. GET  "/tree?recursive=True" to paginate over the input folders. Called only if a folder path is provided as
-           input. Will be called multiple times to follow pagination.
-        If no path is provided as input, step 1. is ignored and all files from the repo are listed.
-
-        Args:
-            repo_id (`str`):
-                A namespace (user or an organization) and a repo name separated by a `/`.
-            paths (`Union[List[str], str, None]`, *optional*):
-                The paths to get information about. Paths to files are directly resolved. Paths to folders are resolved
-                recursively which means that information is returned about all files in the folder and its subfolders.
-                If `None`, all files are returned (the default). If a path do not exist, it is ignored without raising
-                an exception.
-            expand (`bool`, *optional*, defaults to `False`):
-                Whether to fetch more information about the files (e.g. last commit and security scan results). This
-                operation is more expensive for the server so only 50 results are returned per page (instead of 1000).
-                As pagination is implemented in `huggingface_hub`, this is transparent for you except for the time it
-                takes to get the results.
-            revision (`str`, *optional*):
-                The revision of the repository from which to get the information. Defaults to `"main"` branch.
-            repo_type (`str`, *optional*):
-                The type of the repository from which to get the information (`"model"`, `"dataset"` or `"space"`.
-                Defaults to `"model"`.
-            token (`bool` or `str`, *optional*):
-                A valid authentication token (see https://huggingface.co/settings/token). If `None` or `True` and
-                machine is logged in (through `huggingface-cli login` or [`~huggingface_hub.login`]), token will be
-                retrieved from the cache. If `False`, token is not sent in the request header.
-
-        Returns:
-            `Iterable[RepoFile]`:
-                The information about the files, as an iterable of [`RepoFile`] objects. The order of the files is
-                not guaranteed.
-
-        Raises:
-            [`~utils.RepositoryNotFoundError`]:
-                If repository is not found (error 404): wrong repo_id/repo_type, private but not authenticated or repo
-                does not exist.
-            [`~utils.RevisionNotFoundError`]:
-                If revision is not found (error 404) on the repo.
-
-        Examples:
-
-            Get information about files on a repo.
-            ```py
-            >>> from huggingface_hub import list_files_info
-            >>> files_info = list_files_info("lysandre/arxiv-nlp", ["README.md", "config.json"])
-            >>> files_info
-            <generator object HfApi.list_files_info at 0x7f93b848e730>
-            >>> list(files_info)
-            [
-                RepoFile(path='README.md', size=391, blob_id='43bd404b159de6fba7c2f4d3264347668d43af25', lfs=None, last_commit=None, security=None),
-                RepoFile(path='config.json', size=554, blob_id='2f9618c3a19b9a61add74f70bfb121335aeef666', lfs=None, last_commit=None, security=None)
-            ]
-            ```
-
-            Get even more information about files on a repo (last commit and security scan results)
-            ```py
-            >>> from huggingface_hub import list_files_info
-            >>> files_info = list_files_info("prompthero/openjourney-v4", expand=True)
-            >>> list(files_info)
-            [
-                RepoFile(
-                    path='safety_checker/pytorch_model.bin',
-                    size=1216064769,
-                    blob_id='c8835557a0d3af583cb06c7c154b7e54a069c41d',
-                    lfs={
-                        'size': 1216064769,
-                        'sha256': '16d28f2b37109f222cdc33620fdd262102ac32112be0352a7f77e9614b35a394',
-                        'pointer_size': 135
-                    },
-                    last_commit={
-                        'oid': '47b62b20b20e06b9de610e840282b7e6c3d51190',
-                        'title': 'Upload diffusers weights (#48)',
-                        'date': datetime.datetime(2023, 3, 21, 10, 5, 27, tzinfo=datetime.timezone.utc)
-                    },
-                    security={
-                        'safe': True,
-                        'av_scan': {
-                            'virusFound': False,
-                            'virusNames': None
-                        },
-                        'pickle_import_scan': {
-                            'highestSafetyLevel': 'innocuous',
-                            'imports': [
-                                {'module': 'torch', 'name': 'FloatStorage', 'safety': 'innocuous'},
-                                {'module': 'collections', 'name': 'OrderedDict', 'safety': 'innocuous'},
-                                {'module': 'torch', 'name': 'LongStorage', 'safety': 'innocuous'},
-                                {'module': 'torch._utils', 'name': '_rebuild_tensor_v2', 'safety': 'innocuous'}
-                            ]
-                        }
-                    }
-                ),
-                RepoFile(
-                    path='scheduler/scheduler_config.json',
-                    size=465,
-                    blob_id='70d55e3e9679f41cbc66222831b38d5abce683dd',
-                    lfs=None,
-                    last_commit={
-                        'oid': '47b62b20b20e06b9de610e840282b7e6c3d51190',
-                        'title': 'Upload diffusers weights (#48)',
-                        'date': datetime.datetime(2023, 3, 21, 10, 5, 27, tzinfo=datetime.timezone.utc)},
-                        security={
-                            'safe': True,
-                            'av_scan': {
-                                'virusFound': False,
-                                'virusNames': None
-                            },
-                            'pickle_import_scan': None
-                        }
-                ),
-                ...
-            ]
-            ```
-
-            List LFS files from the "vae/" folder in "stabilityai/stable-diffusion-2" repository.
-
-            ```py
-            >>> from huggingface_hub import list_files_info
-            >>> [info.path for info in list_files_info("stabilityai/stable-diffusion-2", "vae") if info.lfs is not None]
-            ['vae/diffusion_pytorch_model.bin', 'vae/diffusion_pytorch_model.safetensors']
-            ```
-
-            List all files on a repo.
-            ```py
-            >>> from huggingface_hub import list_files_info
-            >>> [info.path for info in list_files_info("glue", repo_type="dataset")]
-            ['.gitattributes', 'README.md', 'dataset_infos.json', 'glue.py']
-            ```
-        """
-        repo_type = repo_type or REPO_TYPE_MODEL
-        revision = quote(revision, safe="") if revision is not None else DEFAULT_REVISION
-        headers = self._build_hf_headers(token=token)
-
-        folder_paths = []
-        if paths is None:
-            # `paths` is not provided => list all files from the repo
-            folder_paths.append("")
-        elif paths == []:
-            # corner case: server would return a 400 error if `paths` is an empty list. Let's return early.
-            return
-        else:
-            # `paths` is provided => get info about those
-            response = get_session().post(
-                f"{self.endpoint}/api/{repo_type}s/{repo_id}/paths-info/{revision}",
-                data={
-                    "paths": paths if isinstance(paths, list) else [paths],
-                    "expand": expand,
-                },
-                headers=headers,
-            )
-            hf_raise_for_status(response)
-            paths_info = response.json()
-
-            # List top-level files first
-            for path_info in paths_info:
-                if path_info["type"] == "file":
-                    yield RepoFile(**path_info)
-                else:
-                    folder_paths.append(path_info["path"])
-
-        # List files in subdirectories
-        for path in folder_paths:
-            encoded_path = "/" + quote(path, safe="") if path else ""
-            tree_url = f"{self.endpoint}/api/{repo_type}s/{repo_id}/tree/{revision}{encoded_path}"
-            for subpath_info in paginate(path=tree_url, headers=headers, params={"recursive": True, "expand": expand}):
-                if subpath_info["type"] == "file":
-                    yield RepoFile(**subpath_info)
-
     @validate_hf_hub_args
     def list_repo_files(
         self,
@@ -8462,6 +8322,98 @@ class HfApi:
             if relpath_to_abspath[relpath] != ".gitattributes"
         ]
 
+    def get_user_overview(self, username: str) -> User:
+        """
+        Get an overview of a user on the Hub.
+
+        Args:
+            username (`str`):
+                Username of the user to get an overview of.
+
+        Returns:
+            `User`: A [`User`] object with the user's overview.
+
+        Raises:
+            `HTTPError`:
+                HTTP 404 If the user does not exist on the Hub.
+        """
+        r = get_session().get(f"{ENDPOINT}/api/users/{username}/overview")
+
+        hf_raise_for_status(r)
+        return User(**r.json())
+
+    def list_organization_members(self, organization: str) -> Iterable[User]:
+        """
+        List of members of an organization on the Hub.
+
+        Args:
+            organization (`str`):
+                Name of the organization to get the members of.
+
+        Returns:
+            `Iterable[User]`: A list of [`User`] objects with the members of the organization.
+
+        Raises:
+            `HTTPError`:
+                HTTP 404 If the organization does not exist on the Hub.
+
+        """
+
+        r = get_session().get(f"{ENDPOINT}/api/organizations/{organization}/members")
+
+        hf_raise_for_status(r)
+
+        for member in r.json():
+            yield User(**member)
+
+    def list_user_followers(self, username: str) -> Iterable[User]:
+        """
+        Get the list of followers of a user on the Hub.
+
+        Args:
+            username (`str`):
+                Username of the user to get the followers of.
+
+        Returns:
+            `Iterable[User]`: A list of [`User`] objects with the followers of the user.
+
+        Raises:
+            `HTTPError`:
+                HTTP 404 If the user does not exist on the Hub.
+
+        """
+
+        r = get_session().get(f"{ENDPOINT}/api/users/{username}/followers")
+
+        hf_raise_for_status(r)
+
+        for follower in r.json():
+            yield User(**follower)
+
+    def list_user_following(self, username: str) -> Iterable[User]:
+        """
+        Get the list of users followed by a user on the Hub.
+
+        Args:
+            username (`str`):
+                Username of the user to get the users followed by.
+
+        Returns:
+            `Iterable[User]`: A list of [`User`] objects with the users followed by the user.
+
+        Raises:
+            `HTTPError`:
+                HTTP 404 If the user does not exist on the Hub.
+
+        """
+
+        r = get_session().get(f"{ENDPOINT}/api/users/{username}/following")
+
+        hf_raise_for_status(r)
+
+        for followed_user in r.json():
+            yield User(**followed_user)
+
 
 def _prepare_upload_folder_additions(
     folder_path: Union[str, Path],
@@ -8535,7 +8487,6 @@ repo_info = api.repo_info
 list_repo_files = api.list_repo_files
 list_repo_refs = api.list_repo_refs
 list_repo_commits = api.list_repo_commits
-list_files_info = api.list_files_info
 list_repo_tree = api.list_repo_tree
 get_paths_info = api.get_paths_info
 
@@ -8630,3 +8581,9 @@ cancel_access_request = api.cancel_access_request
 accept_access_request = api.accept_access_request
 reject_access_request = api.reject_access_request
 grant_access = api.grant_access
+
+# User API
+get_user_overview = api.get_user_overview
+list_organization_members = api.list_organization_members
+list_user_followers = api.list_user_followers
+list_user_following = api.list_user_following
diff --git a/src/huggingface_hub/hub_mixin.py b/src/huggingface_hub/hub_mixin.py
index eb42ef4e..aa44ad16 100644
--- a/src/huggingface_hub/hub_mixin.py
+++ b/src/huggingface_hub/hub_mixin.py
@@ -19,7 +19,6 @@ from .utils import (
     logging,
     validate_hf_hub_args,
 )
-from .utils._deprecation import _deprecate_arguments
 
 
 if TYPE_CHECKING:
@@ -485,11 +484,6 @@ class ModelHubMixin:
         """
         raise NotImplementedError
 
-    @_deprecate_arguments(
-        version="0.23.0",
-        deprecated_args=["api_endpoint"],
-        custom_message="Use `HF_ENDPOINT` environment variable instead.",
-    )
     @validate_hf_hub_args
     def push_to_hub(
         self,
@@ -504,8 +498,6 @@ class ModelHubMixin:
         allow_patterns: Optional[Union[List[str], str]] = None,
         ignore_patterns: Optional[Union[List[str], str]] = None,
         delete_patterns: Optional[Union[List[str], str]] = None,
-        # TODO: remove once deprecated
-        api_endpoint: Optional[str] = None,
     ) -> str:
         """
         Upload model checkpoint to the Hub.
@@ -523,8 +515,6 @@ class ModelHubMixin:
                 Message to commit while pushing.
             private (`bool`, *optional*, defaults to `False`):
                 Whether the repository created should be private.
-            api_endpoint (`str`, *optional*):
-                The API endpoint to use when pushing the model to the hub.
             token (`str`, *optional*):
                 The token to use as HTTP bearer authorization for remote files. By default, it will use the token
                 cached when running `huggingface-cli login`.
@@ -542,7 +532,7 @@ class ModelHubMixin:
         Returns:
             The url of the commit of your model in the given repository.
         """
-        api = HfApi(endpoint=api_endpoint, token=token)
+        api = HfApi(token=token)
         repo_id = api.create_repo(repo_id=repo_id, private=private, exist_ok=True).repo_id
 
         # Push the files to the repo in a single commit
diff --git a/src/huggingface_hub/lfs.py b/src/huggingface_hub/lfs.py
index a6be3eb1..d6fb9e21 100644
--- a/src/huggingface_hub/lfs.py
+++ b/src/huggingface_hub/lfs.py
@@ -31,6 +31,7 @@ from huggingface_hub.constants import ENDPOINT, HF_HUB_ENABLE_HF_TRANSFER, REPO_
 
 from .utils import (
     build_hf_headers,
+    fix_hf_endpoint_in_url,
     get_session,
     hf_raise_for_status,
     http_backoff,
@@ -193,6 +194,7 @@ def lfs_upload(
     lfs_batch_action: Dict,
     token: Optional[str] = None,
     headers: Optional[Dict[str, str]] = None,
+    endpoint: Optional[str] = None,
 ) -> None:
     """
     Handles uploading a given object to the Hub with the LFS protocol.
@@ -230,6 +232,7 @@ def lfs_upload(
     # 2. Upload file (either single part or multi-part)
     header = upload_action.get("header", {})
     chunk_size = header.get("chunk_size")
+    upload_url = fix_hf_endpoint_in_url(upload_action["href"], endpoint=endpoint)
     if chunk_size is not None:
         try:
             chunk_size = int(chunk_size)
@@ -237,15 +240,16 @@ def lfs_upload(
             raise ValueError(
                 f"Malformed response from LFS batch endpoint: `chunk_size` should be an integer. Got '{chunk_size}'."
             )
-        _upload_multi_part(operation=operation, header=header, chunk_size=chunk_size, upload_url=upload_action["href"])
+        _upload_multi_part(operation=operation, header=header, chunk_size=chunk_size, upload_url=upload_url)
     else:
-        _upload_single_part(operation=operation, upload_url=upload_action["href"])
+        _upload_single_part(operation=operation, upload_url=upload_url)
 
     # 3. Verify upload went well
     if verify_action is not None:
         _validate_lfs_action(verify_action)
+        verify_url = fix_hf_endpoint_in_url(verify_action["href"], endpoint)
         verify_resp = get_session().post(
-            verify_action["href"],
+            verify_url,
             headers=build_hf_headers(token=token, headers=headers),
             json={"oid": operation.upload_info.sha256.hex(), "size": operation.upload_info.size},
         )
diff --git a/src/huggingface_hub/utils/__init__.py b/src/huggingface_hub/utils/__init__.py
index 0a3a0a32..97c236ec 100644
--- a/src/huggingface_hub/utils/__init__.py
+++ b/src/huggingface_hub/utils/__init__.py
@@ -47,7 +47,14 @@ from ._fixes import SoftTemporaryDirectory, WeakFileLock, yaml_dump
 from ._git_credential import list_credential_helpers, set_git_credential, unset_git_credential
 from ._headers import LocalTokenNotFoundError, build_hf_headers, get_token_to_send
 from ._hf_folder import HfFolder
-from ._http import OfflineModeIsEnabled, configure_http_backend, get_session, http_backoff, reset_sessions
+from ._http import (
+    OfflineModeIsEnabled,
+    configure_http_backend,
+    fix_hf_endpoint_in_url,
+    get_session,
+    http_backoff,
+    reset_sessions,
+)
 from ._pagination import paginate
 from ._paths import IGNORE_GIT_FOLDER_PATTERNS, filter_repo_objects
 from ._runtime import (
diff --git a/src/huggingface_hub/utils/_http.py b/src/huggingface_hub/utils/_http.py
index 591bd837..081d84a4 100644
--- a/src/huggingface_hub/utils/_http.py
+++ b/src/huggingface_hub/utils/_http.py
@@ -21,7 +21,7 @@ import time
 import uuid
 from functools import lru_cache
 from http import HTTPStatus
-from typing import Callable, Tuple, Type, Union
+from typing import Callable, Optional, Tuple, Type, Union
 
 import requests
 from requests import Response
@@ -306,3 +306,16 @@ def http_backoff(
 
         # Update sleep time for next retry
         sleep_time = min(max_wait_time, sleep_time * 2)  # Exponential backoff
+
+
+def fix_hf_endpoint_in_url(url: str, endpoint: Optional[str]) -> str:
+    """Replace the default endpoint in a URL by a custom one.
+
+    This is useful when using a proxy and the Hugging Face Hub returns a URL with the default endpoint.
+    """
+    endpoint = endpoint or constants.ENDPOINT
+    # check if a proxy has been set => if yes, update the returned URL to use the proxy
+    if endpoint not in (None, constants._HF_DEFAULT_ENDPOINT, constants._HF_DEFAULT_STAGING_ENDPOINT):
+        url = url.replace(constants._HF_DEFAULT_ENDPOINT, endpoint)
+        url = url.replace(constants._HF_DEFAULT_STAGING_ENDPOINT, endpoint)
+    return url


Test patch:
diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py
index baabd476..fc05aff3 100644
--- a/tests/test_hf_api.py
+++ b/tests/test_hf_api.py
@@ -1120,134 +1120,6 @@ class HfApiListFilesInfoTest(HfApiCommonTest):
     def tearDownClass(cls):
         cls._api.delete_repo(repo_id=cls.repo_id)
 
-    @expect_deprecation("list_files_info")
-    def test_get_regular_file_info(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths="file.md"))
-        self.assertEqual(len(files), 1)
-        file = files[0]
-
-        self.assertEqual(file.path, "file.md")
-        self.assertIsNone(file.lfs)
-        self.assertEqual(file.size, 4)
-        self.assertEqual(file.blob_id, "6320cd248dd8aeaab759d5871f8781b5c0505172")
-
-    @expect_deprecation("list_files_info")
-    def test_get_lfs_file_info(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths="lfs.bin"))
-        self.assertEqual(len(files), 1)
-        file = files[0]
-
-        self.assertEqual(file.path, "lfs.bin")
-        self.assertEqual(
-            file.lfs,
-            {
-                "size": 4,
-                "sha256": "3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7",
-                "pointer_size": 126,
-            },
-        )
-        self.assertEqual(file.size, 4)
-        self.assertEqual(file.blob_id, "0a828055346279420bd02a4221c177bbcdc045d8")
-
-    @expect_deprecation("list_files_info")
-    def test_list_files(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths=["file.md", "lfs.bin", "2/file_2.md"]))
-        self.assertEqual(len(files), 3)
-        self.assertEqual({f.path for f in files}, {"file.md", "lfs.bin", "2/file_2.md"})
-
-    @expect_deprecation("list_files_info")
-    def test_list_files_and_folder(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths=["file.md", "lfs.bin", "2"]))
-        self.assertEqual(len(files), 3)
-        self.assertEqual({f.path for f in files}, {"file.md", "lfs.bin", "2/file_2.md"})
-
-    @expect_deprecation("list_files_info")
-    def test_list_unknown_path_among_other(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths=["file.md", "unknown"]))
-        self.assertEqual(len(files), 1)
-
-    @expect_deprecation("list_files_info")
-    def test_list_unknown_path_alone(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths="unknown"))
-        self.assertEqual(len(files), 0)
-
-    @expect_deprecation("list_files_info")
-    def test_list_folder_flat(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths=["2"]))
-        self.assertEqual(len(files), 1)
-        self.assertEqual(files[0].path, "2/file_2.md")
-
-    @expect_deprecation("list_files_info")
-    def test_list_folder_recursively(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths=["1"]))
-        self.assertEqual(len(files), 2)
-        self.assertEqual({f.path for f in files}, {"1/2/file_1_2.md", "1/file_1.md"})
-
-    @expect_deprecation("list_files_info")
-    def test_list_repo_files_manually(self):
-        files = list(self._api.list_files_info(repo_id=self.repo_id))
-        self.assertEqual(len(files), 7)
-        self.assertEqual(
-            {f.path for f in files},
-            {".gitattributes", "1/2/file_1_2.md", "1/file_1.md", "2/file_2.md", "3/file_3.md", "file.md", "lfs.bin"},
-        )
-
-    @expect_deprecation("list_files_info")
-    def test_list_repo_files_alias(self):
-        self.assertEqual(
-            set(f.path for f in self._api.list_files_info(repo_id=self.repo_id)),
-            {".gitattributes", "1/2/file_1_2.md", "1/file_1.md", "2/file_2.md", "3/file_3.md", "file.md", "lfs.bin"},
-        )
-
-    @expect_deprecation("list_files_info")
-    def test_list_with_root_path_is_ignored(self):
-        # must use `paths=None`
-        files = list(self._api.list_files_info(repo_id=self.repo_id, paths="/"))
-        self.assertEqual(len(files), 0)
-
-    @expect_deprecation("list_files_info")
-    def test_list_with_empty_path_is_invalid(self):
-        # must use `paths=None`
-        with self.assertRaises(BadRequestError):
-            list(self._api.list_files_info(repo_id=self.repo_id, paths=""))
-
-    @expect_deprecation("list_files_info")
-    @with_production_testing
-    def test_list_files_with_expand(self):
-        files = list(
-            HfApi().list_files_info(
-                repo_id="prompthero/openjourney-v4",
-                expand=True,
-                revision="c9211c53404dd6f4cfac5f04f33535892260668e",
-            )
-        )
-        self.assertEqual(len(files), 22)
-
-        # check last_commit and security are present
-        vae_model = next(file for file in files if file.path == "vae/diffusion_pytorch_model.bin")
-        self.assertIsNotNone(vae_model.last_commit)
-        self.assertEqual(vae_model.last_commit["oid"], "47b62b20b20e06b9de610e840282b7e6c3d51190")
-        self.assertIsNotNone(vae_model.security)
-        self.assertTrue(vae_model.security["safe"])
-        self.assertTrue(isinstance(vae_model.security["av_scan"], dict))  # all details in here
-
-    @expect_deprecation("list_files_info")
-    @with_production_testing
-    def test_list_files_without_expand(self):
-        files = list(
-            HfApi().list_files_info(
-                repo_id="prompthero/openjourney-v4",
-                expand=False,
-                revision="c9211c53404dd6f4cfac5f04f33535892260668e",
-            )
-        )
-        self.assertEqual(len(files), 22)
-
-        # check last_commit and security are missing
-        vae_model = next(file for file in files if file.path == "vae/diffusion_pytorch_model.bin")
-        self.assertIsNone(vae_model.last_commit)
-        self.assertIsNone(vae_model.security)
-
 
 class HfApiListRepoTreeTest(HfApiCommonTest):
     @classmethod
@@ -3791,3 +3663,28 @@ class AccessRequestAPITest(HfApiCommonTest):
         self._api.cancel_access_request(self.repo_id, OTHER_USER)
         with self.assertRaises(HTTPError):
             self._api.cancel_access_request(self.repo_id, OTHER_USER)
+
+
+@with_production_testing
+class UserApiTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.api = HfApi()  # no auth!
+
+    def test_user_overview(self) -> None:
+        overview = self.api.get_user_overview("julien-c")
+        self.assertEqual(overview.user_type, "user")
+        self.assertGreater(overview.num_likes, 10)
+        self.assertGreater(overview.num_upvotes, 10)
+
+    def test_organization_members(self) -> None:
+        members = self.api.list_organization_members("huggingface")
+        self.assertGreater(len(list(members)), 1)
+
+    def test_user_followers(self) -> None:
+        followers = self.api.list_user_followers("julien-c")
+        self.assertGreater(len(list(followers)), 10)
+
+    def test_user_following(self) -> None:
+        following = self.api.list_user_following("julien-c")
+        self.assertGreater(len(list(following)), 10)
diff --git a/tests/test_utils_http.py b/tests/test_utils_http.py
index f82a7a17..be5ceaea 100644
--- a/tests/test_utils_http.py
+++ b/tests/test_utils_http.py
@@ -3,10 +3,11 @@ import threading
 import time
 import unittest
 from multiprocessing import Process, Queue
-from typing import Generator
+from typing import Generator, Optional
 from unittest.mock import Mock, call, patch
 from uuid import UUID
 
+import pytest
 import requests
 from requests import ConnectTimeout, HTTPError
 
@@ -14,6 +15,7 @@ from huggingface_hub.constants import ENDPOINT
 from huggingface_hub.utils._http import (
     OfflineModeIsEnabled,
     configure_http_backend,
+    fix_hf_endpoint_in_url,
     get_session,
     http_backoff,
     reset_sessions,
@@ -292,3 +294,20 @@ def _is_uuid(string: str) -> bool:
     except ValueError:
         return False
     return str(uuid_obj) == string
+
+
+@pytest.mark.parametrize(
+    ("base_url", "endpoint", "expected_url"),
+    [
+        # Staging url => unchanged
+        ("https://hub-ci.huggingface.co/resolve/...", None, "https://hub-ci.huggingface.co/resolve/..."),
+        # Prod url => unchanged
+        ("https://huggingface.co/resolve/...", None, "https://huggingface.co/resolve/..."),
+        # Custom endpoint + staging url => fixed
+        ("https://hub-ci.huggingface.co/api/models", "https://mirror.co", "https://mirror.co/api/models"),
+        # Custom endpoint + prod url => fixed
+        ("https://huggingface.co/api/models", "https://mirror.co", "https://mirror.co/api/models"),
+    ],
+)
+def test_fix_hf_endpoint_in_url(base_url: str, endpoint: Optional[str], expected_url: str) -> None:
+    assert fix_hf_endpoint_in_url(base_url, endpoint) == expected_url