From 9ced1346346bef0cabf9e51e8cbc20988b326cfb Mon Sep 17 00:00:00 2001 From: "M. Bilgehan Ertan" <52638733+bilgehanertan@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:50:45 +0100 Subject: [PATCH] User API endpoints (#2147) * add: user overview api * add: organization members endpoint * refactor user class * add: user followers and following endpoints * refact: fix reviews * add: is_pro added back --- src/huggingface_hub/hf_api.py | 147 ++++++++++++++++++++++++++++++++++ tests/test_hf_api.py | 25 ++++++ 2 files changed, 172 insertions(+) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index 1d9c8b9ce9..91f909d15e 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -1192,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: @@ -8273,6 +8322,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], @@ -8440,3 +8581,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 diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py index 89e427ead3..fc05aff386 100644 --- a/tests/test_hf_api.py +++ b/tests/test_hf_api.py @@ -3663,3 +3663,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)