Repository: huggingface/huggingface_hub

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:
- [x] `/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.
- [x] `/likes` (e.g. https://huggingface.co/api/users/julien-c/likes). Returns all repos liked by a user.
- [x] `/followers` (e.g. https://huggingface.co/api/users/julien-c/followers). Returns all followers of a user.
- [x] `/following` (e.g. https://huggingface.co/api/users/julien-c/following). Returns all users followed by a user.
- [x] `/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.


Discussion:
Hi, I would like to work on this issue, could you clarify what you mean by supporting these endpoints natively? Would this mean to create an endpoint in the hf_api.py file that mimics the output of the aforementioned endpoints?
Hi @op8mnl, thanks for proposing to contribute! Yes that's exactly it :) It would mean having a method for each of those endpoints in the `HfApi` client object. Would you like to start with `/overview` for instance? Please let me know if you have any questions! 

Resolving patch:
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
@@ -1197,12 +1197,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:
@@ -8462,6 +8511,98 @@ def _prepare_upload_folder_deletions(
             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],
@@ -8630,3 +8771,9 @@ def _parse_revision_from_pr_url(pr_url: str) -> str:
 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


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
@@ -3791,3 +3791,28 @@ def test_access_request_error(self):
         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)