From c68271761157001134d2648514b384066485199f Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Thu, 21 Sep 2023 17:20:43 +0200 Subject: [PATCH 01/15] Add Collection API --- src/huggingface_hub/__init__.py | 14 + src/huggingface_hub/hf_api.py | 487 +++++++++++++++++++++++++++++++- 2 files changed, 489 insertions(+), 12 deletions(-) diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py index 9d75889f3b..839b9e06c5 100644 --- a/src/huggingface_hub/__init__.py +++ b/src/huggingface_hub/__init__.py @@ -142,11 +142,13 @@ "ModelSearchArguments", "RepoUrl", "UserLikes", + "add_collection_item", "add_space_secret", "add_space_variable", "change_discussion_status", "comment_discussion", "create_branch", + "create_collection", "create_commit", "create_commits_on_pr", "create_discussion", @@ -155,6 +157,8 @@ "create_tag", "dataset_info", "delete_branch", + "delete_collection", + "delete_collection_item", "delete_file", "delete_folder", "delete_repo", @@ -165,6 +169,7 @@ "duplicate_space", "edit_discussion_comment", "file_exists", + "get_collection", "get_dataset_tags", "get_discussion_details", "get_full_repo_name", @@ -199,6 +204,8 @@ "space_info", "super_squash_history", "unlike", + "update_collection_item", + "update_collection_metadata", "update_repo_visibility", "upload_file", "upload_folder", @@ -451,11 +458,13 @@ def __dir__(): ModelSearchArguments, # noqa: F401 RepoUrl, # noqa: F401 UserLikes, # noqa: F401 + add_collection_item, # noqa: F401 add_space_secret, # noqa: F401 add_space_variable, # noqa: F401 change_discussion_status, # noqa: F401 comment_discussion, # noqa: F401 create_branch, # noqa: F401 + create_collection, # noqa: F401 create_commit, # noqa: F401 create_commits_on_pr, # noqa: F401 create_discussion, # noqa: F401 @@ -464,6 +473,8 @@ def __dir__(): create_tag, # noqa: F401 dataset_info, # noqa: F401 delete_branch, # noqa: F401 + delete_collection, # noqa: F401 + delete_collection_item, # noqa: F401 delete_file, # noqa: F401 delete_folder, # noqa: F401 delete_repo, # noqa: F401 @@ -474,6 +485,7 @@ def __dir__(): duplicate_space, # noqa: F401 edit_discussion_comment, # noqa: F401 file_exists, # noqa: F401 + get_collection, # noqa: F401 get_dataset_tags, # noqa: F401 get_discussion_details, # noqa: F401 get_full_repo_name, # noqa: F401 @@ -508,6 +520,8 @@ def __dir__(): space_info, # noqa: F401 super_squash_history, # noqa: F401 unlike, # noqa: F401 + update_collection_item, # noqa: F401 + update_collection_metadata, # noqa: F401 update_repo_visibility, # noqa: F401 upload_file, # noqa: F401 upload_folder, # noqa: F401 diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index f4eb4b5961..6d5853c795 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -132,6 +132,7 @@ R = TypeVar("R") # Return type +CollectionItemType_T = Literal["model", "dataset", "space", "paper"] USERNAME_PLACEHOLDER = "hf_user" _REGEX_DISCUSSION_URL = re.compile(r".*/discussions/(\d+)$") @@ -143,6 +144,13 @@ class ReprMixin: """Mixin to create the __repr__ for a class""" + def __init__(self, **kwargs) -> None: + # Store all the other fields returned by the API + # Hack to ensure backward compatibility with future versions of the API. + # See discussion in https://github.com/huggingface/huggingface_hub/pull/951#discussion_r926460408 + for k, v in kwargs.items(): + setattr(self, k, v) + def __repr__(self): formatted_value = pprint.pformat(self.__dict__, width=119, compact=True) if "\n" in formatted_value: @@ -392,10 +400,8 @@ def __init__( self.blob_id = blobId self.lfs = lfs - # Hack to ensure backward compatibility with future versions of the API. - # See discussion in https://github.com/huggingface/huggingface_hub/pull/951#discussion_r926460408 - for k, v in kwargs.items(): - setattr(self, k, v) + # Store all the other fields returned by the API + super().__init__(**kwargs) class ModelInfo(ReprMixin): @@ -453,8 +459,9 @@ def __init__( self.author = author self.config = config self.securityStatus = securityStatus - for k, v in kwargs.items(): - setattr(self, k, v) + + # Store all the other fields returned by the API + super().__init__(**kwargs) def __str__(self): r = f"Model Name: {self.modelId}, Tags: {self.tags}" @@ -521,8 +528,7 @@ def __init__( # because of old versions of the datasets lib that need this field kwargs.pop("key", None) # Store all the other fields returned by the API - for k, v in kwargs.items(): - setattr(self, k, v) + super().__init__(**kwargs) def __str__(self): r = f"Dataset Name: {self.id}, Tags: {self.tags}" @@ -570,8 +576,8 @@ def __init__( self.siblings = [RepoFile(**x) for x in siblings] if siblings is not None else [] self.private = private self.author = author - for k, v in kwargs.items(): - setattr(self, k, v) + # Store all the other fields returned by the API + super().__init__(**kwargs) class MetricInfo(ReprMixin): @@ -594,14 +600,96 @@ def __init__( # because of old versions of the datasets lib that need this field kwargs.pop("key", None) # Store all the other fields returned by the API - for k, v in kwargs.items(): - setattr(self, k, v) + super().__init__(**kwargs) def __str__(self): r = f"Metric Name: {self.id}" return r +class CollectionItem(ReprMixin): + """Contains information about an item of a Collection (model, dataset, Space or paper). + + Args: + item_type (`str`): + Type of the item. Can be one of `"model"`, `"dataset"`, `"space"` or `"paper"`. + id (`str`): + ID of the item. Can be either the repo_id on the Hub or the paper Arxiv id + e.g. `"jbilcke-hf/ai-comic-factory"`, `"2307.09288"`. + position (`int`): + Position of the item in the collection. + note (`str`, *optional*): + Note associated with the item, as plain text. + kwargs (`Dict`, *optional*): + Any other attribute returned by the server. Those attributes depend on the `item_type`: "author", "private", + "lastModified", "gated", "title", "likes", "upvotes", etc. + + + """ + + def __init__( + self, type: CollectionItemType_T, id: str, position: int, note: Optional[str] = None, **kwargs + ) -> None: + self.item_type = type + self.id = id + self.position = position + self.note = note + + # Store all the other fields returned by the API + super().__init__(**kwargs) + + +class Collection(ReprMixin): + """ + Contains information about a Collection on the Hub. + + Args: + slug (`str`): + Slug of the collection. E.g. `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + title (`str`): + Title of the collection. E.g. `"Recent models"`. + owner (`str`): + Owner of the collection. E.g. `"TheBloke"`. + description (`str`, *optional*): + Description of the collection, as plain text. + items (`List[CollectionItem]`): + List of items in the collection. + last_updated (`datetime`): + Date of the last update of the collection. + position (`int`): + Position of the collection in the list of collections of the owner. + private (`bool`): + Whether the collection is private or not. + theme (`str`): + Theme of the collection. E.g. `"green"`. + """ + + slug: str + title: str + owner: str + description: Optional[str] + items: List[CollectionItem] + + last_updated: datetime + position: int + private: bool + theme: str + + def __init__(self, data: Dict) -> None: + # Collection info + self.slug = data["slug"] + self.title = data["title"] + self.owner = data["owner"]["name"] + self.description = data.get("description") + self.items = [CollectionItem(**item) for item in data["items"]] + + # Metadata + self.last_updated = parse_datetime(data["lastUpdated"]) + self.private = data["private"] + self.position = data["position"] + self.theme = data["theme"] + + class ModelSearchArguments(AttributeDictionary): """ A nested namespace object holding all possible values for properties of @@ -5710,6 +5798,372 @@ def delete_space_storage( hf_raise_for_status(r) return SpaceRuntime(r.json()) + ######################## + # Collection Endpoints # + ######################## + + def get_collection(self, collection_slug: str, *, token: Optional[str] = None) -> Collection: + """Gets information about a Collection on the Hub. + + Args: + collection_slug (`str`): + Slug of the collection of the Hub. Example: `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + Returns: + [`Collection`]: the collection content. + + Example: + ```py + >>> from huggingface_hub import get_collection + >>> collection = get_collection("TheBloke/recent-models-64f9a55bb3115b4f513ec026") + >>> collection.title + 'Recent models' + >>> len(collection.items) + 37 + >>> collection.items[0] + CollectionItem: { + {'_id': '6507f6d5423b46492ee1413e', + 'id': 'TheBloke/TigerBot-70B-Chat-GPTQ', + 'author': 'TheBloke', + 'item_type': 'model', + 'lastModified': '2023-09-19T12:55:21.000Z', + (...) + }} + ``` + """ + r = get_session().get( + f"{self.endpoint}/api/collections/{collection_slug}", headers=self._build_hf_headers(token=token) + ) + hf_raise_for_status(r) + return Collection(r.json()) + + def create_collection( + self, + title: str, + *, + namespace: Optional[str] = None, + description: Optional[str] = None, + private: bool = False, + exists_ok: bool = False, + token: Optional[str] = None, + ) -> Collection: + """Create a new Collection on the Hub. + + Args: + title (`str`): + Title of the collection to create. Example: `"Recent models"`. + namespace (`str`, *optional*): + Namespace of the collection to create (username or org). Will default to the owner name. + description (`str`, *optional*): + Description of the collection to create. + private (`bool`, *optional*): + Whether the collection should be private or not. Defaults to `False` (i.e. public collection). + exists_ok (`bool`, *optional*): + If `True`, do not raise an error if collection already exists. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + Returns: + [`Collection`]: the newly created collection. + + Example: + ```py + >>> from huggingface_hub import create_collection + >>> collection = create_collection( + ... title="ICCV 2023", + ... description="Portfolio of models, papers and demos I presented at ICCV 2023", + ... ) + >>> collection.slug + "username/iccv-2023-64f9a55bb3115b4f513ec026" + ``` + """ + if namespace is None: + namespace = self.whoami(token)["name"] + + payload = { + "title": title, + "namespace": namespace, + "private": private, + } + if description is not None: + payload["description"] = description + + r = get_session().post( + f"{self.endpoint}/api/collections", headers=self._build_hf_headers(token=token), json=payload + ) + try: + hf_raise_for_status(r) + except HTTPError as err: + if exists_ok and err.response.status_code == 409: + # Collection already exists and `exists_ok=True` + slug = r.json()["slug"] + return self.get_collection(slug, token=token) + else: + raise + return Collection(r.json()) + + def update_collection_metadata( + self, + collection_slug: str, + *, + title: Optional[str] = None, + description: Optional[str] = None, + position: Optional[int] = None, + private: Optional[bool] = None, + theme: Optional[str] = None, + token: Optional[str] = None, + ) -> Collection: + """Update metadata of a collection on the Hub. + + All arguments are optional. Only provided metadata will be updated. + + Args: + collection_slug (`str`): + Slug of the collection to update. Example: `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + title (`str`): + Title of the collection to update. + description (`str`, *optional*): + Description of the collection to update. + position (`int`, *optional*): + New position of the collection in the list of collections of the user. + private (`bool`, *optional*): + Whether the collection should be private or not. + theme (`str`, *optional*): + Theme of the collection on the Hub. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + Returns: + [`Collection`]: the updated collection. + + Example: + ```py + >>> from huggingface_hub import update_collection_metadata + >>> collection = update_collection_metadata( + ... collection_slug="username/iccv-2023-64f9a55bb3115b4f513ec026", + ... title="ICCV Oct. 2023" + ... description="Portfolio of models, datasets, papers and demos I presented at ICCV Oct. 2023", + ... private=False, + ... theme="pink", + ... ) + >>> collection.slug + "username/iccv-oct-2023-64f9a55bb3115b4f513ec026" + # ^collection slug got updated but not the trailing ID + ``` + """ + payload = { + "position": position, + "private": private, + "theme": theme, + "title": title, + "description": description, + } + r = get_session().patch( + f"{self.endpoint}/api/collections/{collection_slug}", + headers=self._build_hf_headers(token=token), + # Only send not-none values to the API + json={key: value for key, value in payload.items() if value is not None}, + ) + hf_raise_for_status(r) + return Collection(r.json()) + + def delete_collection( + self, collection_slug: str, *, missing_ok: bool = False, token: Optional[str] = None + ) -> None: + """Delete a collection on the Hub. + + Args: + collection_slug (`str`): + Slug of the collection to delete. Example: `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + missing_ok (`bool`, *optional*): + If `True`, do not raise an error if collection doesn't exists. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + Example: + ```py + >>> from huggingface_hub import delete_collection + >>> collection = delete_collection("username/useless-collection-64f9a55bb3115b4f513ec026", missing_ok=True) + ``` + """ + r = get_session().delete( + f"{self.endpoint}/api/collections/{collection_slug}", headers=self._build_hf_headers(token=token) + ) + try: + hf_raise_for_status(r) + except HTTPError as err: + if missing_ok and err.response.status_code == 404: + # Collection doesn't exists and `missing_ok=True` + return + else: + raise + + def add_collection_item( + self, + collection_slug: str, + item_id: str, + item_type: CollectionItemType_T, + *, + note: Optional[str] = None, + exists_ok: bool = False, + token: Optional[str] = None, + ) -> Collection: + """Add an item to a collection on the Hub. + + Args: + collection_slug (`str`): + Slug of the collection to update. Example: `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + item_id (`str`): + ID of the item to add to the collection. It can be the ID of a repo on the Hub (e.g. `"facebook/bart-large-mnli"`) + or a paper id (e.g. `"2307.09288"`). + item_type (`str`): + Type of the item to add. Can be one of `"model"`, `"dataset"`, `"space"` or `"paper"`. + note (`str`, *optional*): + A note to attach to the item in the collection. + exists_ok (`bool`, *optional*): + If `True`, do not raise an error if item already exists. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + Returns: + [`Collection`]: the updated collection. + + ```py + >>> from huggingface_hub import add_collection_item + >>> collection = add_collection_item( + ... collection_slug="davanstrien/climate-64f99dc2a5067f6b65531bab", + ... item_id="pierre-loic/climate-news-articles", + ... item_type="dataset" + ... ) + >>> collection.items[-1].id + "pierre-loic/climate-news-articles" + # ^item got added to the collection on last position + + # Add collection with a note + >>> add_collection_item( + ... collection_slug="davanstrien/climate-64f99dc2a5067f6b65531bab", + ... item_id="datasets/climate_fever", + ... item_type="dataset" + ... note="This dataset adopts the FEVER methodology that consists of 1,535 real-world claims regarding climate-change collected on the internet." + ... ) + (...) + ``` + """ + payload: Dict[str, Any] = {"item": {"id": item_id, "type": item_type}} + if note is not None: + payload["note"] = note + r = get_session().post( + f"{self.endpoint}/api/collections/{collection_slug}", + headers=self._build_hf_headers(token=token), + json=payload, + ) + try: + hf_raise_for_status(r) + except HTTPError as err: + if exists_ok and err.response.status_code == 409: + # Item already exists and `exists_ok=True` + return self.get_collection(collection_slug, token=token) + else: + raise + return Collection(r.json()) + + def update_collection_item( + self, + collection_slug: str, + item_object_id: str, + *, + note: Optional[str] = None, + position: Optional[int] = None, + token: Optional[str] = None, + ) -> None: + """Update an item in a collection. + + Args: + collection_slug (`str`): + Slug of the collection to update. Example: `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + item_object_id (`str`): + ID of the item in the collection. This is not the id of the item on the Hub (repo_id or paper id). + It must be retrieved from a [`CollectionItem`] object. Example: `collection.items[0]._id`. + note (`str`, *optional*): + A note to attach to the item in the collection. + position (`int`, *optional*): + New position of the item in the collection. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + ```py + >>> from huggingface_hub import get_collection, update_collection_item + + # Get collection first + >>> collection = get_collection("TheBloke/recent-models-64f9a55bb3115b4f513ec026") + + # Update item based on its ID (add note + update position) + >>> update_collection_item( + ... collection_slug="TheBloke/recent-models-64f9a55bb3115b4f513ec026", + ... item_object_id=collection.items[-1], + ... note="Newly update model!" + ... position=0, + ... ) + ``` + """ + payload = {"position": position, "note": note} + r = get_session().patch( + f"{self.endpoint}/api/collections/{collection_slug}/{item_object_id}", + headers=self._build_hf_headers(token=token), + # Only send not-none values to the API + json={key: value for key, value in payload.items() if value is not None}, + ) + hf_raise_for_status(r) + + def delete_collection_item( + self, + collection_slug: str, + item_object_id: str, + *, + missing_ok: bool = False, + token: Optional[str] = None, + ) -> None: + """Delete an item from a collection. + + Args: + collection_slug (`str`): + Slug of the collection to update. Example: `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. + item_object_id (`str`): + ID of the item in the collection. This is not the id of the item on the Hub (repo_id or paper id). + It must be retrieved from a [`CollectionItem`] object. Example: `collection.items[0]._id`. + missing_ok (`bool`, *optional*): + If `True`, do not raise an error if item doesn't exists. + token (`str`, *optional*): + Hugging Face token. Will default to the locally saved token if not provided. + + ```py + >>> from huggingface_hub import get_collection, delete_collection_item + + # Get collection first + >>> collection = get_collection("TheBloke/recent-models-64f9a55bb3115b4f513ec026") + + # Delete item based on its ID + >>> delete_collection_item( + ... collection_slug="TheBloke/recent-models-64f9a55bb3115b4f513ec026", + ... item_object_id=collection.items[-1], + ... ) + ``` + """ + r = get_session().delete( + f"{self.endpoint}/api/collections/{collection_slug}/{item_object_id}", + headers=self._build_hf_headers(token=token), + ) + try: + hf_raise_for_status(r) + except HTTPError as err: + if missing_ok and err.response.status_code == 409: + # Item already deleted and `missing_ok=True` + return + else: + raise + def _build_hf_headers( self, token: Optional[Union[bool, str]] = None, @@ -5902,3 +6356,12 @@ def _parse_revision_from_pr_url(pr_url: str) -> str: duplicate_space = api.duplicate_space request_space_storage = api.request_space_storage delete_space_storage = api.delete_space_storage + +# Collections API +get_collection = api.get_collection +create_collection = api.create_collection +update_collection_metadata = api.update_collection_metadata +delete_collection = api.delete_collection +add_collection_item = api.add_collection_item +update_collection_item = api.update_collection_item +delete_collection_item = api.delete_collection_item From abc2de874062fdd22055fe561cec8b6a9b1f69be Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Thu, 21 Sep 2023 18:24:14 +0200 Subject: [PATCH 02/15] add unit tests --- tests/test_hf_api.py | 144 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py index f37e992170..02096664e9 100644 --- a/tests/test_hf_api.py +++ b/tests/test_hf_api.py @@ -19,6 +19,7 @@ import time import types import unittest +import uuid import warnings from concurrent.futures import Future from functools import partial @@ -49,6 +50,7 @@ ) from huggingface_hub.file_download import hf_hub_download from huggingface_hub.hf_api import ( + Collection, CommitInfo, DatasetInfo, HfApi, @@ -3199,3 +3201,145 @@ def __init__(self, **kwargs: Dict[str, Any]) -> None: repr(MyClass(foo="foo", bar="bar")), "MyClass: {'bar': 'bar', 'foo': 'foo'}", # keys are sorted ) + + +class CollectionAPITest(HfApiCommonTest): + def setUp(self) -> None: + id = uuid.uuid4() + self.title = f"My cool stuff {id}" + self.slug_prefix = f"my-cool-stuff-{id}" + return super().setUp() + + def test_create_collection_with_description(self) -> None: + collection = self._api.create_collection(self.title, description="Contains a lot of cool stuff") + + self.assertIsInstance(collection, Collection) + self.assertEqual(collection.title, self.title) + self.assertEqual(collection.description, "Contains a lot of cool stuff") + self.assertEqual(collection.items, []) + self.assertEqual(collection.slug.startswith(self.slug_prefix)) + + self._api.delete_collection(collection.slug) + + def test_create_collection_exists_ok(self) -> None: + # Create collection once without description + collection_1 = self._api.create_collection(self.title) + + # Cannot create twice with same title + with self.assertRaises(HTTPError): # already exists + self._api.create_collection(self.title) + + # Can ignore error + collection_2 = self._api.create_collection(self.title, description="description", exists_ok=True) + + self.assertEqual(collection_1.slug, collection_2.slug) + self.assertIsNone(collection_1.description) + self.assertIsNone(collection_2.description) # Did not got updated! + + self._api.delete_collection(collection_1.slug) + + def test_create_private_collection(self) -> None: + collection = self._api.create_collection(self.title, private=True) + + # Get private collection + self._api.get_collection(collection.slug) # no error + with self.assertRaises(HTTPError): + self._api.get_collection(collection.slug, token=False) # not authorized + + # Get public collection + self._api.update_collection_metadata(collection.slug, private=False) + self._api.get_collection(collection.slug) # no error + self._api.get_collection(collection.slug, token=False) # no error + + self._api.delete_collection(collection.slug) + + def test_update_collection(self) -> None: + collection_1 = self._api.create_collection(self.title) + collection_2 = self._api.update_collection_metadata( + collection_slug=collection_1.slug, + title="New title", + description="New description", + private=True, + theme="pink", + ) + + self.assertEqual(collection_2.title, "New title") + self.assertEqual(collection_2.description, "New description") + self.assertEqual(collection_2.private, True) + self.assertEqual(collection_2.theme, "pink") + self.assertNotEqual(collection_1.slug, collection_2.slug) + + # Different slug, same id + self.assertEqual(collection_1.slug.split("-")[-1], collection_2.slug.split("-")[-1]) + + # Works with both slugs, same collection returned + self.assertEqual(self._api.get_collection(collection_1.slug).slug, collection_2.slug) + self.assertEqual(self._api.get_collection(collection_2.slug).slug, collection_2.slug) + + self._api.delete_collection(collection_2.slug) + + def test_delete_collection(self) -> None: + collection = self._api.create_collection(self.title) + + self._api.delete_collection(collection.slug) + + # Cannot delete twice the same collection + with self.assertRaises(HTTPError): # already exists + self._api.delete_collection(collection.slug) + + # Possible to ignore error + self._api.delete_collection(collection.slug, missing_ok=True) + + def test_collection_items(self) -> None: + # Create some repos + model_id = self._api.create_repo(repo_name()).repo_id + dataset_id = self._api.create_repo(repo_name(), repo_type="dataset").repo_id + space_id = self._api.create_repo(repo_name(), repo_type="Space", space_sdk="gradio").repo_id + + # Create collection + add items to it + collection = self._api.create_collection(self.title) + self._api.add_collection_item(collection.slug, model_id, "model", note="This is my model") + self._api.add_collection_item(collection.slug, dataset_id, "dataset") # note is optional + self._api.add_collection_item(collection.slug, space_id, "space") # note is optional + + # Check consistency + collection = self._api.get_collection(collection.slug) + self.assertEqual(len(collection.items), 3) + self.assertEqual(collection.items[0].id, model_id) + self.assertEqual(collection.items[0].item_type, "model") + self.assertEqual(collection.items[0].note, "This is my model") + + self.assertEqual(collection.items[1].id, dataset_id) + self.assertEqual(collection.items[1].item_type, "dataset") + self.assertIsNone(collection.items[1].note) + + self.assertEqual(collection.items[2].id, space_id) + self.assertEqual(collection.items[2].item_type, "space") + self.assertIsNone(collection.items[2].note) + + # Add existing item fails (except if ignore error) + with self.assertRaises(HTTPError): + self._api.add_collection_item(collection.slug, model_id, "model") + self._api.add_collection_item(collection.slug, model_id, "model", exists_ok=True) + + # Add inexistent item fails + with self.assertRaises(HTTPError): + self._api.add_collection_item(collection.slug, model_id, "dataset") + + # Update first item + delete last item + self._api.update_collection_item(collection.slug, collection.items[0].id, note="New note", position=1) + self._api.delete_collection_item(collection.slug, collection.items[2].id) + self._api.delete_collection_item(collection.slug, collection.items[2].id, missing_ok=True) + + # Check consistency + collection = self._api.get_collection(collection.slug) + self.assertEqual(len(collection.items), 2) # item got removed + self.assertEqual(collection.items[0].id, dataset_id) # position got updated + self.assertEqual(collection.items[1].id, model_id) + self.assertEqual(collection.items[1].note, "New note") # note got updated + + # Delete everything + self._api.delete_repo(model_id) + self._api.delete_repo(dataset_id, repo_type="dataset") + self._api.delete_repo(space_id, repo_type="space") + self._api.delete_collection(collection.slug) From 7f8ffc2b85b46ad2c458c1b700c193806444155c Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Thu, 21 Sep 2023 18:35:26 +0200 Subject: [PATCH 03/15] collections --- docs/source/en/_toctree.yml | 2 ++ .../en/package_reference/collections.md | 25 +++++++++++++++++++ src/huggingface_hub/__init__.py | 4 +++ 3 files changed, 31 insertions(+) create mode 100644 docs/source/en/package_reference/collections.md diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index e8ab4a1241..6db69bef39 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -68,6 +68,8 @@ title: Repo Cards and Repo Card Data - local: package_reference/space_runtime title: Space runtime + - local: package_reference/collections + title: Collections - local: package_reference/tensorboard title: TensorBoard logger - local: package_reference/webhooks_server diff --git a/docs/source/en/package_reference/collections.md b/docs/source/en/package_reference/collections.md new file mode 100644 index 0000000000..c4f2f2a994 --- /dev/null +++ b/docs/source/en/package_reference/collections.md @@ -0,0 +1,25 @@ + + +# Managing collections + +Check out the [`HfApi`] documentation page for the reference of methods to manage your Space on the Hub. + +- Get collection content: [`get_collection`] +- Create new collection: [`create_collection] +- Update a collection: [`update_collection_metadata`] +- Delete a collection: [`delete_collection`] +- Add an item to a collection: [`add_collection_item`] +- Update an item in a collection: [`update_collection_item`] +- Remove an item from a collection: [`delete_collection_item`] + +## Data structures + +### Collection + +[[autodoc]] Collection + +### CollectionItem + +[[autodoc]] CollectionItem \ No newline at end of file diff --git a/src/huggingface_hub/__init__.py b/src/huggingface_hub/__init__.py index 839b9e06c5..bf137fcf13 100644 --- a/src/huggingface_hub/__init__.py +++ b/src/huggingface_hub/__init__.py @@ -129,6 +129,8 @@ "try_to_load_from_cache", ], "hf_api": [ + "Collection", + "CollectionItem", "CommitInfo", "CommitOperation", "CommitOperationAdd", @@ -445,6 +447,8 @@ def __dir__(): try_to_load_from_cache, # noqa: F401 ) from .hf_api import ( + Collection, # noqa: F401 + CollectionItem, # noqa: F401 CommitInfo, # noqa: F401 CommitOperation, # noqa: F401 CommitOperationAdd, # noqa: F401 From e9b95f7b371f184a8aabed0c9e7e95aaa99bf801 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Thu, 21 Sep 2023 19:05:27 +0200 Subject: [PATCH 04/15] add guide (wip) --- docs/source/en/_toctree.yml | 2 + docs/source/en/guides/collections.md | 72 ++++++++++++++++++++++++++++ tests/test_collection_api.py | 50 +++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 docs/source/en/guides/collections.md create mode 100644 tests/test_collection_api.py diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index 6db69bef39..fb4139a577 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -30,6 +30,8 @@ title: Model Cards - local: guides/manage-spaces title: Manage your Space + - local: guides/collections + title: Manage your Collections - local: guides/integrations title: Integrate a library - local: guides/webhooks_server diff --git a/docs/source/en/guides/collections.md b/docs/source/en/guides/collections.md new file mode 100644 index 0000000000..c6aa9837ba --- /dev/null +++ b/docs/source/en/guides/collections.md @@ -0,0 +1,72 @@ + + +# Manage your Collections + +A Collection is a group of related items from the Hub (models, datasets, Spaces, papers) that are organized together on a same page. They can be useful in many use cases such as creating your own portfolio, bookmarking content in categories or presenting a curated list of items your want to share. Check out this [guide](https://huggingface.co/docs/hub/collections) to understand in more details what are Collections and how they look like on the Hub, + +Managing Collections can be done in the browser directly. In this guide, we will focus on how to it programmatically using `huggingface_hub`. + +## Get Collection content + +To get the content of a collection, use [`get_collection`]. You can use it either on your own Collections or any public Collection. You retrieve a Collection, you must have its collection `slug`. A slug is a unique identifier for a collection based on the title and an ID. You can find it in the URL of the Collection page. + +
+ +
+ + +```py +>>> from huggingface_hub import get_collection +>>> collection = get_collection("TheBloke/recent-models-64f9a55bb3115b4f513ec026") +>>> collection +Collection: { + {'description': "Models I've recently quantized.', + 'items': [...], + 'last_updated': datetime.datetime(2023, 9, 21, 7, 26, 28, 57000, tzinfo=datetime.timezone.utc), + 'owner': 'TheBloke', + 'position': 1, + 'private': False, + 'slug': 'TheBloke/recent-models-64f9a55bb3115b4f513ec026', + 'theme': 'green', + 'title': 'Recent models'} +} +>>> collection.items[0] +CollectionItem: { + {'_id': '6507f6d5423b46492ee1413e', + 'author': 'TheBloke', + 'id': 'TheBloke/TigerBot-70B-Chat-GPTQ', + 'item_type': 'model', + 'lastModified': '2023-09-19T12:55:21.000Z', + 'position': 0, + 'private': False, + 'repoType': 'model' + (...) + } +} +``` + +## Create a new Collection + +To create a collection, use [`create_collection`] with title and optionally a description. + +```py +>>> from huggingface_hub import create_collection + +>>> collection = create_collection( +... title="ICCV 2023", +... description="Portfolio of models, papers and demos I presented at ICCV 2023", +... ) +``` + +It will return a [`Collection`] object with some high-level metadata (title, description, owner, etc.) and a list of items (currently empty). You will now be able to refer to this collection using it's `slug`. + +```py +>>> collection.slug +'iccv-2023-15ecb98efca45' +>>> collection.title +"ICCV 2023" +``` + +## Add items to your Collection diff --git a/tests/test_collection_api.py b/tests/test_collection_api.py new file mode 100644 index 0000000000..e3a85cae3d --- /dev/null +++ b/tests/test_collection_api.py @@ -0,0 +1,50 @@ +# Copyright 2020 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import unittest +from functools import partial + +from huggingface_hub.hf_api import ( + HfApi, +) +from huggingface_hub.utils import ( + logging, +) + +from .testing_constants import ( + ENDPOINT_STAGING, + TOKEN, +) +from .testing_utils import ( + repo_name, +) + + +logger = logging.get_logger(__name__) + +dataset_repo_name = partial(repo_name, prefix="my-dataset") +space_repo_name = partial(repo_name, prefix="my-space") +large_file_repo_name = partial(repo_name, prefix="my-model-largefiles") + +WORKING_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures/working_repo") +LARGE_FILE_14MB = "https://cdn-media.huggingface.co/lfs-largefiles/progit.epub" +LARGE_FILE_18MB = "https://cdn-media.huggingface.co/lfs-largefiles/progit.pdf" + + +class HfApiCommonTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + """Share the valid token in all tests below.""" + cls._token = TOKEN + cls._api = HfApi(endpoint=ENDPOINT_STAGING, token=TOKEN) From 76fa6d47824f6f38307569a0da1a963edd84f1f0 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 10:22:20 +0200 Subject: [PATCH 05/15] completed guide --- docs/source/en/_toctree.yml | 4 +- docs/source/en/guides/collections.md | 139 ++++++++++++++++++++++++--- src/huggingface_hub/hf_api.py | 35 ++++--- 3 files changed, 150 insertions(+), 28 deletions(-) diff --git a/docs/source/en/_toctree.yml b/docs/source/en/_toctree.yml index fb4139a577..632bbf5086 100644 --- a/docs/source/en/_toctree.yml +++ b/docs/source/en/_toctree.yml @@ -24,14 +24,14 @@ title: Inference - local: guides/community title: Community Tab + - local: guides/collections + title: Collections - local: guides/manage-cache title: Cache - local: guides/model-cards title: Model Cards - local: guides/manage-spaces title: Manage your Space - - local: guides/collections - title: Manage your Collections - local: guides/integrations title: Integrate a library - local: guides/webhooks_server diff --git a/docs/source/en/guides/collections.md b/docs/source/en/guides/collections.md index c6aa9837ba..ddefa7024a 100644 --- a/docs/source/en/guides/collections.md +++ b/docs/source/en/guides/collections.md @@ -2,20 +2,21 @@ rendered properly in your Markdown viewer. --> -# Manage your Collections +# Manage your collections -A Collection is a group of related items from the Hub (models, datasets, Spaces, papers) that are organized together on a same page. They can be useful in many use cases such as creating your own portfolio, bookmarking content in categories or presenting a curated list of items your want to share. Check out this [guide](https://huggingface.co/docs/hub/collections) to understand in more details what are Collections and how they look like on the Hub, +A collection is a group of related items on the Hub (models, datasets, Spaces, papers) that are organized together on a same page. Collections can be useful in many use cases such as creating your own portfolio, bookmarking content in categories or presenting a curated list of items your want to share. Check out this [guide](https://huggingface.co/docs/hub/collections) to understand in more details what are Collections and how they look like on the Hub, -Managing Collections can be done in the browser directly. In this guide, we will focus on how to it programmatically using `huggingface_hub`. +Managing collections can be done in the browser directly. In this guide, we will focus on how to do it programmatically using `huggingface_hub`. -## Get Collection content +## Fetch a collection -To get the content of a collection, use [`get_collection`]. You can use it either on your own Collections or any public Collection. You retrieve a Collection, you must have its collection `slug`. A slug is a unique identifier for a collection based on the title and an ID. You can find it in the URL of the Collection page. +To fetch a collection, use [`get_collection`]. You can use it either on your own collections or any public one. To retrieve a collection, you must have its collection's `slug`. A slug is an identifier for a collection based on the title and a unique ID. You can find it in the URL of the collection page.
+Here the slug is `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. Let's fetch the collection: ```py >>> from huggingface_hub import get_collection @@ -34,9 +35,9 @@ Collection: { } >>> collection.items[0] CollectionItem: { - {'_id': '6507f6d5423b46492ee1413e', + {'item_object_id': '6507f6d5423b46492ee1413e', 'author': 'TheBloke', - 'id': 'TheBloke/TigerBot-70B-Chat-GPTQ', + 'item_id': 'TheBloke/TigerBot-70B-Chat-GPTQ', 'item_type': 'model', 'lastModified': '2023-09-19T12:55:21.000Z', 'position': 0, @@ -47,9 +48,23 @@ CollectionItem: { } ``` -## Create a new Collection +The [`Collection`] object returned by [`get_collection`] contains: +- high-level metadata: `slug`, `owner`, `title`, `description`, etc. +- a list of [`CollectionItem`] objects. Each item represents a model, a dataset, a Space or a paper. -To create a collection, use [`create_collection`] with title and optionally a description. +All items of a collection are guaranteed to have: +- a unique `item_object_id`: this is the id of the collection item in the database +- an `item_id`: this is the id on the Hub of the underlying item (model, dataset, Space, paper). It is not necessarily unique! only the `item_id`/`item_type` pair is unique. +- an `item_type`: model, dataset, Space, paper. +- the `position` of the item in the collection. Position can be updated to re-organize your collection (see [`update_collection_item`] below) + +A `note` can also be attached to the item. This is useful to add additional information about the item (e.g. a comment, a link to a blog post, etc.). If an item doesn't have a note, the attribute still exists with a `None` value. + +In addition to these base attributes, returned items can have additional attributes depending on their type: `author`, `private`, `lastModified`, `gated`, `title`, `likes`, `upvotes`, etc. None of these attributes are guaranteed to be returned. + +## Create a new collection + +Now that we know how to get a [`Collection`], let's create our own! Use [`create_collection`] with a title and optionally a description. ```py >>> from huggingface_hub import create_collection @@ -60,13 +75,113 @@ To create a collection, use [`create_collection`] with title and optionally a de ... ) ``` -It will return a [`Collection`] object with some high-level metadata (title, description, owner, etc.) and a list of items (currently empty). You will now be able to refer to this collection using it's `slug`. +It will return a [`Collection`] object with the high-level metadata (title, description, owner, etc.) and an empty list of items. You will now be able to refer to this collection using it's `slug`. ```py >>> collection.slug -'iccv-2023-15ecb98efca45' +'iccv-2023-15e23b46cb98efca45' >>> collection.title "ICCV 2023" +>>> collection.owner +"username" +``` + +To create a collection on an organization page, pass `namespace="my-cool-org"` when creating the collection. Finally, you can also create private collections by passing `private=True`. + +## Manage items in a collection + +Now that we have a [`Collection`], we want to add items to it and organize them. + +### Add items + +Items have to be added one by one using [`add_collection_item`]. You only need to know the `collection_slug`, `item_id` and `item_type`. Optionally, you can also add a `note` to the item. + +```py +>>> from huggingface_hub import create_collection, add_collection_item + +>>> collection = create_collection(title="OS Week Highlights - Sept 18 - 24", namespace="osanseviero") +>>> collection.slug +"osanseviero/os-week-highlights-sept-18-24-650bfed7f795a59f491afb80" + +>>> add_collection_item(collection.slug, item_id="coqui/xtts", item_type="space") +>>> add_collection_item( +... collection.slug, +... item_id="warp-ai/wuerstchen", +... item_type="model", +... note="Würstchen is a new fast and efficient high resolution text-to-image architecture and model" +... ) +>>> add_collection_item(collection.slug, item_id="lmsys/lmsys-chat-1m", item_type="dataset") +>>> add_collection_item(collection.slug, item_id="warp-ai/wuerstchen", item_type="space") # same item_id, different item_type ``` -## Add items to your Collection +If an item already exists in a collection (i.e. same `item_id`/`item_type` pair), an HTTP 409 error will be raised. You can choose to ignore this error by setting `exists_ok=True`. + +### Add a note to an existing item + +You can modify an existing item to add or modify the note attached to it using [`update_collection_item`]. Let's reuse the example above: + +```py +>>> from huggingface_hub import get_collection, update_collection_item + +# Fetch collection with newly added items +>>> collection_slug = "osanseviero/os-week-highlights-sept-18-24-650bfed7f795a59f491afb80" +>>> collection = get_collection(collection_slug) + +# Add note the `lmsys-chat-1m` dataset +>>> update_collection_item( +... collection_slug=collection_slug, +... item_object_id=collection.items[2].item_object_id, +... note="This dataset contains one million real-world conversations with 25 state-of-the-art LLMs.", +... ) +``` + +### Re-order items + +Items in a collection are ordered. The order is determined by the `position` attribute of each item. By default, items are ordered by appending new items at the end of the collection. You can update the ordering using [`update_collection_item`] the same way you would add a note. + +Let's reuse our example above: + +```py +>>> from huggingface_hub import get_collection, update_collection_item + +# Fetch collection +>>> collection_slug = "osanseviero/os-week-highlights-sept-18-24-650bfed7f795a59f491afb80" +>>> collection = get_collection(collection_slug) + +# Reorder to place the two `Wuerstchen` items together +>>> update_collection_item( +... collection_slug=collection_slug, +... item_object_id=collection.items[3].item_object_id, +... position=2, +... ) +``` + +### Remove items + +Finally you can also remove an item using [`delete_collection_item`]. + +```py +>>> from huggingface_hub import get_collection, update_collection_item + +# Fetch collection +>>> collection_slug = "osanseviero/os-week-highlights-sept-18-24-650bfed7f795a59f491afb80" +>>> collection = get_collection(collection_slug) + +# Remove `coqui/xtts` Space from the list +>>> delete_collection_item(collection_slug=collection_slug, item_object_id=collection.items[0].item_object_id) +``` + +## Delete collection + +A collection can be deleted using [`delete_collection`]. + + + +This is a non-revertible action. A deleted collection cannot be restored. + + + +```py +>>> from huggingface_hub import delete_collection +>>> collection = delete_collection("username/useless-collection-64f9a55bb3115b4f513ec026", missing_ok=True) +``` \ No newline at end of file diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index 6d5853c795..dd3a77ea10 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -611,11 +611,13 @@ class CollectionItem(ReprMixin): """Contains information about an item of a Collection (model, dataset, Space or paper). Args: - item_type (`str`): - Type of the item. Can be one of `"model"`, `"dataset"`, `"space"` or `"paper"`. - id (`str`): - ID of the item. Can be either the repo_id on the Hub or the paper Arxiv id + item_object_id (`str`): + Unique ID of the item in the collection. + item_id (`str`): + ID of the underlying object on the Hub. Can be either a repo_id or a paper id e.g. `"jbilcke-hf/ai-comic-factory"`, `"2307.09288"`. + item_type (`str`): + Type of the underlying object. Can be one of `"model"`, `"dataset"`, `"space"` or `"paper"`. position (`int`): Position of the item in the collection. note (`str`, *optional*): @@ -623,15 +625,14 @@ class CollectionItem(ReprMixin): kwargs (`Dict`, *optional*): Any other attribute returned by the server. Those attributes depend on the `item_type`: "author", "private", "lastModified", "gated", "title", "likes", "upvotes", etc. - - """ def __init__( - self, type: CollectionItemType_T, id: str, position: int, note: Optional[str] = None, **kwargs + self, _id: str, id: str, type: CollectionItemType_T, position: int, note: Optional[str] = None, **kwargs ) -> None: + self.item_object_id = _id # id in database + self.item_id = id # repo_id or paper id self.item_type = type - self.id = id self.position = position self.note = note @@ -5824,8 +5825,8 @@ def get_collection(self, collection_slug: str, *, token: Optional[str] = None) - 37 >>> collection.items[0] CollectionItem: { - {'_id': '6507f6d5423b46492ee1413e', - 'id': 'TheBloke/TigerBot-70B-Chat-GPTQ', + {'item_object_id': '6507f6d5423b46492ee1413e', + 'item_id': 'TheBloke/TigerBot-70B-Chat-GPTQ', 'author': 'TheBloke', 'item_type': 'model', 'lastModified': '2023-09-19T12:55:21.000Z', @@ -5987,6 +5988,12 @@ def delete_collection( >>> from huggingface_hub import delete_collection >>> collection = delete_collection("username/useless-collection-64f9a55bb3115b4f513ec026", missing_ok=True) ``` + + + + This is a non-revertible action. A deleted collection cannot be restored. + + """ r = get_session().delete( f"{self.endpoint}/api/collections/{collection_slug}", headers=self._build_hf_headers(token=token) @@ -6037,7 +6044,7 @@ def add_collection_item( ... item_id="pierre-loic/climate-news-articles", ... item_type="dataset" ... ) - >>> collection.items[-1].id + >>> collection.items[-1].item_id "pierre-loic/climate-news-articles" # ^item got added to the collection on last position @@ -6102,8 +6109,8 @@ def update_collection_item( # Update item based on its ID (add note + update position) >>> update_collection_item( ... collection_slug="TheBloke/recent-models-64f9a55bb3115b4f513ec026", - ... item_object_id=collection.items[-1], - ... note="Newly update model!" + ... item_object_id=collection.items[-1].item_object_id, + ... note="Newly updated model!" ... position=0, ... ) ``` @@ -6147,7 +6154,7 @@ def delete_collection_item( # Delete item based on its ID >>> delete_collection_item( ... collection_slug="TheBloke/recent-models-64f9a55bb3115b4f513ec026", - ... item_object_id=collection.items[-1], + ... item_object_id=collection.items[-1].item_object_id, ... ) ``` """ From 62092225ddf03570a56b9bfbdb77c5de072e8b98 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 11:35:29 +0200 Subject: [PATCH 06/15] fix collection tests --- src/huggingface_hub/hf_api.py | 23 ++++----- src/huggingface_hub/utils/_errors.py | 9 ++++ tests/test_hf_api.py | 73 +++++++++++++++------------- 3 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index dd3a77ea10..eb083b8912 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -628,13 +628,13 @@ class CollectionItem(ReprMixin): """ def __init__( - self, _id: str, id: str, type: CollectionItemType_T, position: int, note: Optional[str] = None, **kwargs + self, _id: str, id: str, type: CollectionItemType_T, position: int, note: Optional[Dict] = None, **kwargs ) -> None: - self.item_object_id = _id # id in database - self.item_id = id # repo_id or paper id - self.item_type = type - self.position = position - self.note = note + self.item_object_id: str = _id # id in database + self.item_id: str = id # repo_id or paper id + self.item_type: CollectionItemType_T = type + self.position: int = position + self.note: str = note["text"] if note is not None else None # Store all the other fields returned by the API super().__init__(**kwargs) @@ -5968,7 +5968,7 @@ def update_collection_metadata( json={key: value for key, value in payload.items() if value is not None}, ) hf_raise_for_status(r) - return Collection(r.json()) + return Collection(r.json()["data"]) def delete_collection( self, collection_slug: str, *, missing_ok: bool = False, token: Optional[str] = None @@ -6062,7 +6062,7 @@ def add_collection_item( if note is not None: payload["note"] = note r = get_session().post( - f"{self.endpoint}/api/collections/{collection_slug}", + f"{self.endpoint}/api/collections/{collection_slug}/items", headers=self._build_hf_headers(token=token), json=payload, ) @@ -6117,7 +6117,7 @@ def update_collection_item( """ payload = {"position": position, "note": note} r = get_session().patch( - f"{self.endpoint}/api/collections/{collection_slug}/{item_object_id}", + f"{self.endpoint}/api/collections/{collection_slug}/items/{item_object_id}", headers=self._build_hf_headers(token=token), # Only send not-none values to the API json={key: value for key, value in payload.items() if value is not None}, @@ -6159,13 +6159,13 @@ def delete_collection_item( ``` """ r = get_session().delete( - f"{self.endpoint}/api/collections/{collection_slug}/{item_object_id}", + f"{self.endpoint}/api/collections/{collection_slug}/items/{item_object_id}", headers=self._build_hf_headers(token=token), ) try: hf_raise_for_status(r) except HTTPError as err: - if missing_ok and err.response.status_code == 409: + if missing_ok and err.response.status_code == 404: # Item already deleted and `missing_ok=True` return else: @@ -6372,3 +6372,4 @@ def _parse_revision_from_pr_url(pr_url: str) -> str: add_collection_item = api.add_collection_item update_collection_item = api.update_collection_item delete_collection_item = api.delete_collection_item +delete_collection_item = api.delete_collection_item diff --git a/src/huggingface_hub/utils/_errors.py b/src/huggingface_hub/utils/_errors.py index 47f66d8b4c..586aa2acea 100644 --- a/src/huggingface_hub/utils/_errors.py +++ b/src/huggingface_hub/utils/_errors.py @@ -283,6 +283,15 @@ def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None) ) raise GatedRepoError(message, response) from e + elif ( + response.status_code == 401 + and response.request.url is not None + and "/api/collections" in response.request.url + ): + # Collection not found. We don't raise a custom error for this. + # This prevent from raising a misleading `RepositoryNotFoundError` (see below). + pass + elif error_code == "RepoNotFound" or response.status_code == 401: # 401 is misleading as it is returned for: # - private and gated repos if user is not authenticated diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py index 02096664e9..3c56ab0f85 100644 --- a/tests/test_hf_api.py +++ b/tests/test_hf_api.py @@ -25,7 +25,7 @@ from functools import partial from io import BytesIO from pathlib import Path -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Optional, Union from unittest.mock import Mock, patch from urllib.parse import quote @@ -3207,23 +3207,29 @@ class CollectionAPITest(HfApiCommonTest): def setUp(self) -> None: id = uuid.uuid4() self.title = f"My cool stuff {id}" - self.slug_prefix = f"my-cool-stuff-{id}" + self.slug_prefix = f"{USER}/my-cool-stuff-{id}" + self.slug: Optional[str] = None # Populated by the tests => use to delete in tearDown return super().setUp() + def tearDown(self) -> None: + if self.slug is not None: # Delete collection even if test failed + self._api.delete_collection(self.slug, missing_ok=True) + return super().tearDown() + def test_create_collection_with_description(self) -> None: collection = self._api.create_collection(self.title, description="Contains a lot of cool stuff") + self.slug = collection.slug self.assertIsInstance(collection, Collection) self.assertEqual(collection.title, self.title) self.assertEqual(collection.description, "Contains a lot of cool stuff") self.assertEqual(collection.items, []) - self.assertEqual(collection.slug.startswith(self.slug_prefix)) - - self._api.delete_collection(collection.slug) + self.assertTrue(collection.slug.startswith(self.slug_prefix)) def test_create_collection_exists_ok(self) -> None: # Create collection once without description collection_1 = self._api.create_collection(self.title) + self.slug = collection_1.slug # Cannot create twice with same title with self.assertRaises(HTTPError): # already exists @@ -3236,34 +3242,36 @@ def test_create_collection_exists_ok(self) -> None: self.assertIsNone(collection_1.description) self.assertIsNone(collection_2.description) # Did not got updated! - self._api.delete_collection(collection_1.slug) - def test_create_private_collection(self) -> None: collection = self._api.create_collection(self.title, private=True) + self.slug = collection.slug # Get private collection self._api.get_collection(collection.slug) # no error with self.assertRaises(HTTPError): - self._api.get_collection(collection.slug, token=False) # not authorized + self._api.get_collection(collection.slug, token=OTHER_TOKEN) # not authorized # Get public collection self._api.update_collection_metadata(collection.slug, private=False) self._api.get_collection(collection.slug) # no error - self._api.get_collection(collection.slug, token=False) # no error - - self._api.delete_collection(collection.slug) + self._api.get_collection(collection.slug, token=OTHER_TOKEN) # no error def test_update_collection(self) -> None: + # Create collection collection_1 = self._api.create_collection(self.title) + self.slug = collection_1.slug + + # Update metadata + new_title = f"New title {uuid.uuid4()}" collection_2 = self._api.update_collection_metadata( collection_slug=collection_1.slug, - title="New title", + title=new_title, description="New description", private=True, theme="pink", ) - self.assertEqual(collection_2.title, "New title") + self.assertEqual(collection_2.title, new_title) self.assertEqual(collection_2.description, "New description") self.assertEqual(collection_2.private, True) self.assertEqual(collection_2.theme, "pink") @@ -3276,8 +3284,6 @@ def test_update_collection(self) -> None: self.assertEqual(self._api.get_collection(collection_1.slug).slug, collection_2.slug) self.assertEqual(self._api.get_collection(collection_2.slug).slug, collection_2.slug) - self._api.delete_collection(collection_2.slug) - def test_delete_collection(self) -> None: collection = self._api.create_collection(self.title) @@ -3294,29 +3300,23 @@ def test_collection_items(self) -> None: # Create some repos model_id = self._api.create_repo(repo_name()).repo_id dataset_id = self._api.create_repo(repo_name(), repo_type="dataset").repo_id - space_id = self._api.create_repo(repo_name(), repo_type="Space", space_sdk="gradio").repo_id # Create collection + add items to it collection = self._api.create_collection(self.title) self._api.add_collection_item(collection.slug, model_id, "model", note="This is my model") self._api.add_collection_item(collection.slug, dataset_id, "dataset") # note is optional - self._api.add_collection_item(collection.slug, space_id, "space") # note is optional # Check consistency collection = self._api.get_collection(collection.slug) - self.assertEqual(len(collection.items), 3) - self.assertEqual(collection.items[0].id, model_id) + self.assertEqual(len(collection.items), 2) + self.assertEqual(collection.items[0].item_id, model_id) self.assertEqual(collection.items[0].item_type, "model") - self.assertEqual(collection.items[0].note, "This is my model") + # self.assertEqual(collection.items[0].note, "This is my model") - self.assertEqual(collection.items[1].id, dataset_id) + self.assertEqual(collection.items[1].item_id, dataset_id) self.assertEqual(collection.items[1].item_type, "dataset") self.assertIsNone(collection.items[1].note) - self.assertEqual(collection.items[2].id, space_id) - self.assertEqual(collection.items[2].item_type, "space") - self.assertIsNone(collection.items[2].note) - # Add existing item fails (except if ignore error) with self.assertRaises(HTTPError): self._api.add_collection_item(collection.slug, model_id, "model") @@ -3326,20 +3326,27 @@ def test_collection_items(self) -> None: with self.assertRaises(HTTPError): self._api.add_collection_item(collection.slug, model_id, "dataset") - # Update first item + delete last item - self._api.update_collection_item(collection.slug, collection.items[0].id, note="New note", position=1) - self._api.delete_collection_item(collection.slug, collection.items[2].id) - self._api.delete_collection_item(collection.slug, collection.items[2].id, missing_ok=True) + # Update first item + self._api.update_collection_item( + collection.slug, collection.items[0].item_object_id, note="New note", position=1 + ) # Check consistency collection = self._api.get_collection(collection.slug) - self.assertEqual(len(collection.items), 2) # item got removed - self.assertEqual(collection.items[0].id, dataset_id) # position got updated - self.assertEqual(collection.items[1].id, model_id) + self.assertEqual(collection.items[0].item_id, dataset_id) # position got updated + self.assertEqual(collection.items[1].item_id, model_id) self.assertEqual(collection.items[1].note, "New note") # note got updated + # Delete last item + self._api.delete_collection_item(collection.slug, collection.items[1].item_object_id) + self._api.delete_collection_item(collection.slug, collection.items[1].item_object_id, missing_ok=True) + + # Check consistency + collection = self._api.get_collection(collection.slug) + self.assertEqual(len(collection.items), 1) # only 1 item remaining + self.assertEqual(collection.items[0].item_id, dataset_id) # position got updated + # Delete everything self._api.delete_repo(model_id) self._api.delete_repo(dataset_id, repo_type="dataset") - self._api.delete_repo(space_id, repo_type="space") self._api.delete_collection(collection.slug) From eb4b3a6fd318c8ed4bc6fbb04b806fe8832fdd4c Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 11:36:18 +0200 Subject: [PATCH 07/15] uncomment --- tests/test_hf_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hf_api.py b/tests/test_hf_api.py index 3c56ab0f85..6fb7110fd0 100644 --- a/tests/test_hf_api.py +++ b/tests/test_hf_api.py @@ -3311,7 +3311,7 @@ def test_collection_items(self) -> None: self.assertEqual(len(collection.items), 2) self.assertEqual(collection.items[0].item_id, model_id) self.assertEqual(collection.items[0].item_type, "model") - # self.assertEqual(collection.items[0].note, "This is my model") + self.assertEqual(collection.items[0].note, "This is my model") self.assertEqual(collection.items[1].item_id, dataset_id) self.assertEqual(collection.items[1].item_type, "dataset") From 061a2ea61ea7068700a45cb3423c2067d1a46bbf Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 13:59:07 +0200 Subject: [PATCH 08/15] document 500 limit --- docs/source/en/guides/collections.md | 2 +- src/huggingface_hub/hf_api.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/en/guides/collections.md b/docs/source/en/guides/collections.md index ddefa7024a..dc94235f14 100644 --- a/docs/source/en/guides/collections.md +++ b/docs/source/en/guides/collections.md @@ -94,7 +94,7 @@ Now that we have a [`Collection`], we want to add items to it and organize them. ### Add items -Items have to be added one by one using [`add_collection_item`]. You only need to know the `collection_slug`, `item_id` and `item_type`. Optionally, you can also add a `note` to the item. +Items have to be added one by one using [`add_collection_item`]. You only need to know the `collection_slug`, `item_id` and `item_type`. Optionally, you can also add a `note` to the item (500 characters maximum). ```py >>> from huggingface_hub import create_collection, add_collection_item diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index eb083b8912..0c588a1eda 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -6028,7 +6028,7 @@ def add_collection_item( item_type (`str`): Type of the item to add. Can be one of `"model"`, `"dataset"`, `"space"` or `"paper"`. note (`str`, *optional*): - A note to attach to the item in the collection. + A note to attach to the item in the collection. The maximum size for a note is 500 characters. exists_ok (`bool`, *optional*): If `True`, do not raise an error if item already exists. token (`str`, *optional*): @@ -6094,7 +6094,7 @@ def update_collection_item( ID of the item in the collection. This is not the id of the item on the Hub (repo_id or paper id). It must be retrieved from a [`CollectionItem`] object. Example: `collection.items[0]._id`. note (`str`, *optional*): - A note to attach to the item in the collection. + A note to attach to the item in the collection. The maximum size for a note is 500 characters. position (`int`, *optional*): New position of the item in the collection. token (`str`, *optional*): From 7dc746d6301ee74b8d71b7917257752d1ec23ee3 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 14:12:17 +0200 Subject: [PATCH 09/15] delete --- tests/test_collection_api.py | 50 ------------------------------------ 1 file changed, 50 deletions(-) delete mode 100644 tests/test_collection_api.py diff --git a/tests/test_collection_api.py b/tests/test_collection_api.py deleted file mode 100644 index e3a85cae3d..0000000000 --- a/tests/test_collection_api.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2020 The HuggingFace Team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import os -import unittest -from functools import partial - -from huggingface_hub.hf_api import ( - HfApi, -) -from huggingface_hub.utils import ( - logging, -) - -from .testing_constants import ( - ENDPOINT_STAGING, - TOKEN, -) -from .testing_utils import ( - repo_name, -) - - -logger = logging.get_logger(__name__) - -dataset_repo_name = partial(repo_name, prefix="my-dataset") -space_repo_name = partial(repo_name, prefix="my-space") -large_file_repo_name = partial(repo_name, prefix="my-model-largefiles") - -WORKING_REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fixtures/working_repo") -LARGE_FILE_14MB = "https://cdn-media.huggingface.co/lfs-largefiles/progit.epub" -LARGE_FILE_18MB = "https://cdn-media.huggingface.co/lfs-largefiles/progit.pdf" - - -class HfApiCommonTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - """Share the valid token in all tests below.""" - cls._token = TOKEN - cls._api = HfApi(endpoint=ENDPOINT_STAGING, token=TOKEN) From f088585fc0e3558ffd1622cde4ef2cf2827ec263 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 14:13:23 +0200 Subject: [PATCH 10/15] tpyo --- docs/source/en/package_reference/collections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/en/package_reference/collections.md b/docs/source/en/package_reference/collections.md index c4f2f2a994..c27bfc6f57 100644 --- a/docs/source/en/package_reference/collections.md +++ b/docs/source/en/package_reference/collections.md @@ -7,7 +7,7 @@ rendered properly in your Markdown viewer. Check out the [`HfApi`] documentation page for the reference of methods to manage your Space on the Hub. - Get collection content: [`get_collection`] -- Create new collection: [`create_collection] +- Create new collection: [`create_collection`] - Update a collection: [`update_collection_metadata`] - Delete a collection: [`delete_collection`] - Add an item to a collection: [`add_collection_item`] From c80a60617c7e2663732d53505a0ec7ff73390bc9 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 14:15:28 +0200 Subject: [PATCH 11/15] better formatting --- src/huggingface_hub/hf_api.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index 0c588a1eda..7d83393003 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -5812,8 +5812,7 @@ def get_collection(self, collection_slug: str, *, token: Optional[str] = None) - token (`str`, *optional*): Hugging Face token. Will default to the locally saved token if not provided. - Returns: - [`Collection`]: the collection content. + Returns: [`Collection`] Example: ```py @@ -5866,8 +5865,7 @@ def create_collection( token (`str`, *optional*): Hugging Face token. Will default to the locally saved token if not provided. - Returns: - [`Collection`]: the newly created collection. + Returns: [`Collection`] Example: ```py @@ -5936,8 +5934,7 @@ def update_collection_metadata( token (`str`, *optional*): Hugging Face token. Will default to the locally saved token if not provided. - Returns: - [`Collection`]: the updated collection. + Returns: [`Collection`] Example: ```py @@ -6034,8 +6031,7 @@ def add_collection_item( token (`str`, *optional*): Hugging Face token. Will default to the locally saved token if not provided. - Returns: - [`Collection`]: the updated collection. + Returns: [`Collection`] ```py >>> from huggingface_hub import add_collection_item From 6cb84c40326666fb37a2dfe0f23fbd0302b850bd Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 14:17:27 +0200 Subject: [PATCH 12/15] docs --- src/huggingface_hub/hf_api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/huggingface_hub/hf_api.py b/src/huggingface_hub/hf_api.py index 7d83393003..a357cf1b61 100644 --- a/src/huggingface_hub/hf_api.py +++ b/src/huggingface_hub/hf_api.py @@ -5815,6 +5815,7 @@ def get_collection(self, collection_slug: str, *, token: Optional[str] = None) - Returns: [`Collection`] Example: + ```py >>> from huggingface_hub import get_collection >>> collection = get_collection("TheBloke/recent-models-64f9a55bb3115b4f513ec026") @@ -5868,6 +5869,7 @@ def create_collection( Returns: [`Collection`] Example: + ```py >>> from huggingface_hub import create_collection >>> collection = create_collection( @@ -5937,6 +5939,7 @@ def update_collection_metadata( Returns: [`Collection`] Example: + ```py >>> from huggingface_hub import update_collection_metadata >>> collection = update_collection_metadata( @@ -5981,6 +5984,7 @@ def delete_collection( Hugging Face token. Will default to the locally saved token if not provided. Example: + ```py >>> from huggingface_hub import delete_collection >>> collection = delete_collection("username/useless-collection-64f9a55bb3115b4f513ec026", missing_ok=True) @@ -6033,6 +6037,8 @@ def add_collection_item( Returns: [`Collection`] + Example: + ```py >>> from huggingface_hub import add_collection_item >>> collection = add_collection_item( @@ -6096,6 +6102,8 @@ def update_collection_item( token (`str`, *optional*): Hugging Face token. Will default to the locally saved token if not provided. + Example: + ```py >>> from huggingface_hub import get_collection, update_collection_item @@ -6141,6 +6149,8 @@ def delete_collection_item( token (`str`, *optional*): Hugging Face token. Will default to the locally saved token if not provided. + Example: + ```py >>> from huggingface_hub import get_collection, delete_collection_item From d7f42f49c5ba62d742362a17c3025bcd7407780f Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Fri, 22 Sep 2023 18:30:40 +0200 Subject: [PATCH 13/15] fix test --- tests/test_utils_errors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_utils_errors.py b/tests/test_utils_errors.py index af469f9c29..b0362a1998 100644 --- a/tests/test_utils_errors.py +++ b/tests/test_utils_errors.py @@ -1,6 +1,6 @@ import unittest -from requests.models import Response +from requests.models import PreparedRequest, Response from huggingface_hub.utils._errors import ( BadRequestError, @@ -27,6 +27,7 @@ def test_hf_raise_for_status_repo_not_found_without_error_code(self) -> None: response = Response() response.headers = {"X-Request-Id": 123} response.status_code = 401 + response.request = PreparedRequest() with self.assertRaisesRegex(RepositoryNotFoundError, "Repository Not Found") as context: hf_raise_for_status(response) From 89cef2a900da6d55709807e7e294fc74c5c08c74 Mon Sep 17 00:00:00 2001 From: Lucain Date: Mon, 25 Sep 2023 08:42:23 +0200 Subject: [PATCH 14/15] Apply suggestions from code review Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com> --- docs/source/en/guides/collections.md | 32 +++++++++---------- .../en/package_reference/collections.md | 1 - 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/docs/source/en/guides/collections.md b/docs/source/en/guides/collections.md index dc94235f14..c800c32b43 100644 --- a/docs/source/en/guides/collections.md +++ b/docs/source/en/guides/collections.md @@ -2,21 +2,21 @@ rendered properly in your Markdown viewer. --> -# Manage your collections +# Collections -A collection is a group of related items on the Hub (models, datasets, Spaces, papers) that are organized together on a same page. Collections can be useful in many use cases such as creating your own portfolio, bookmarking content in categories or presenting a curated list of items your want to share. Check out this [guide](https://huggingface.co/docs/hub/collections) to understand in more details what are Collections and how they look like on the Hub, +A collection is a group of related items on the Hub (models, datasets, Spaces, papers) that are organized together on the same page. Collections are useful for creating your own portfolio, bookmarking content in categories, or presenting a curated list of items you want to share. Check out this [guide](https://huggingface.co/docs/hub/collections) to understand in more detail what collections are and how they look on the Hub. -Managing collections can be done in the browser directly. In this guide, we will focus on how to do it programmatically using `huggingface_hub`. +You can directly manage collections in the browser, but in this guide, we will focus on how to manage it programmatically. ## Fetch a collection -To fetch a collection, use [`get_collection`]. You can use it either on your own collections or any public one. To retrieve a collection, you must have its collection's `slug`. A slug is an identifier for a collection based on the title and a unique ID. You can find it in the URL of the collection page. +Use [`get_collection`] to fetch your collections or any public ones. You must have the collection's *slug* to retrieve a collection. A slug is an identifier for a collection based on the title and a unique ID. You can find the slug in the URL of the collection page.
-Here the slug is `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`. Let's fetch the collection: +Let's fetch the collection with, `"TheBloke/recent-models-64f9a55bb3115b4f513ec026"`: ```py >>> from huggingface_hub import get_collection @@ -50,21 +50,21 @@ CollectionItem: { The [`Collection`] object returned by [`get_collection`] contains: - high-level metadata: `slug`, `owner`, `title`, `description`, etc. -- a list of [`CollectionItem`] objects. Each item represents a model, a dataset, a Space or a paper. +- a list of [`CollectionItem`] objects; each item represents a model, a dataset, a Space, or a paper. -All items of a collection are guaranteed to have: +All collection items are guaranteed to have: - a unique `item_object_id`: this is the id of the collection item in the database -- an `item_id`: this is the id on the Hub of the underlying item (model, dataset, Space, paper). It is not necessarily unique! only the `item_id`/`item_type` pair is unique. -- an `item_type`: model, dataset, Space, paper. -- the `position` of the item in the collection. Position can be updated to re-organize your collection (see [`update_collection_item`] below) +- an `item_id`: this is the id on the Hub of the underlying item (model, dataset, Space, paper); it is not necessarily unique, and only the `item_id`/`item_type` pair is unique +- an `item_type`: model, dataset, Space, paper +- the `position` of the item in the collection, which can be updated to reorganize your collection (see [`update_collection_item`] below) -A `note` can also be attached to the item. This is useful to add additional information about the item (e.g. a comment, a link to a blog post, etc.). If an item doesn't have a note, the attribute still exists with a `None` value. +A `note` can also be attached to the item. This is useful to add additional information about the item (a comment, a link to a blog post, etc.). The attribute still has a `None` value if an item doesn't have a note. In addition to these base attributes, returned items can have additional attributes depending on their type: `author`, `private`, `lastModified`, `gated`, `title`, `likes`, `upvotes`, etc. None of these attributes are guaranteed to be returned. ## Create a new collection -Now that we know how to get a [`Collection`], let's create our own! Use [`create_collection`] with a title and optionally a description. +Now that we know how to get a [`Collection`], let's create our own! Use [`create_collection`] with a title and description. ```py >>> from huggingface_hub import create_collection @@ -114,7 +114,7 @@ Items have to be added one by one using [`add_collection_item`]. You only need t >>> add_collection_item(collection.slug, item_id="warp-ai/wuerstchen", item_type="space") # same item_id, different item_type ``` -If an item already exists in a collection (i.e. same `item_id`/`item_type` pair), an HTTP 409 error will be raised. You can choose to ignore this error by setting `exists_ok=True`. +If an item already exists in a collection (same `item_id`/`item_type` pair), an HTTP 409 error will be raised. You can choose to ignore this error by setting `exists_ok=True`. ### Add a note to an existing item @@ -135,9 +135,9 @@ You can modify an existing item to add or modify the note attached to it using [ ... ) ``` -### Re-order items +### Reorder items -Items in a collection are ordered. The order is determined by the `position` attribute of each item. By default, items are ordered by appending new items at the end of the collection. You can update the ordering using [`update_collection_item`] the same way you would add a note. +Items in a collection are ordered. The order is determined by the `position` attribute of each item. By default, items are ordered by appending new items at the end of the collection. You can update the order using [`update_collection_item`] the same way you would add a note. Let's reuse our example above: @@ -158,7 +158,7 @@ Let's reuse our example above: ### Remove items -Finally you can also remove an item using [`delete_collection_item`]. +Finally, you can also remove an item using [`delete_collection_item`]. ```py >>> from huggingface_hub import get_collection, update_collection_item diff --git a/docs/source/en/package_reference/collections.md b/docs/source/en/package_reference/collections.md index c27bfc6f57..37fe25039c 100644 --- a/docs/source/en/package_reference/collections.md +++ b/docs/source/en/package_reference/collections.md @@ -14,7 +14,6 @@ Check out the [`HfApi`] documentation page for the reference of methods to manag - Update an item in a collection: [`update_collection_item`] - Remove an item from a collection: [`delete_collection_item`] -## Data structures ### Collection From b12443b550a395dc88cb60ec009b22d2cb3e01d9 Mon Sep 17 00:00:00 2001 From: Lucain Pouget Date: Mon, 25 Sep 2023 08:44:57 +0200 Subject: [PATCH 15/15] requested changes --- docs/source/en/guides/collections.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/source/en/guides/collections.md b/docs/source/en/guides/collections.md index c800c32b43..9421b7a50f 100644 --- a/docs/source/en/guides/collections.md +++ b/docs/source/en/guides/collections.md @@ -64,7 +64,7 @@ In addition to these base attributes, returned items can have additional attribu ## Create a new collection -Now that we know how to get a [`Collection`], let's create our own! Use [`create_collection`] with a title and description. +Now that we know how to get a [`Collection`], let's create our own! Use [`create_collection`] with a title and description. To create a collection on an organization page, pass `namespace="my-cool-org"` when creating the collection. Finally, you can also create private collections by passing `private=True`. ```py >>> from huggingface_hub import create_collection @@ -86,8 +86,6 @@ It will return a [`Collection`] object with the high-level metadata (title, desc "username" ``` -To create a collection on an organization page, pass `namespace="my-cool-org"` when creating the collection. Finally, you can also create private collections by passing `private=True`. - ## Manage items in a collection Now that we have a [`Collection`], we want to add items to it and organize them.