Skip to content

Commit 337351d

Browse files
authored
Add and endpoints (#1181)
1 parent 8f7d0d0 commit 337351d

File tree

3 files changed

+177
-5
lines changed

3 files changed

+177
-5
lines changed

src/huggingface_hub/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,14 @@
105105
"ModelSearchArguments",
106106
"change_discussion_status",
107107
"comment_discussion",
108+
"create_branch",
108109
"create_commit",
109110
"create_discussion",
110111
"create_pull_request",
111112
"create_repo",
112113
"create_tag",
113114
"dataset_info",
115+
"delete_branch",
114116
"delete_file",
115117
"delete_folder",
116118
"delete_repo",
@@ -329,12 +331,14 @@ def __dir__():
329331
from .hf_api import ModelSearchArguments # noqa: F401
330332
from .hf_api import change_discussion_status # noqa: F401
331333
from .hf_api import comment_discussion # noqa: F401
334+
from .hf_api import create_branch # noqa: F401
332335
from .hf_api import create_commit # noqa: F401
333336
from .hf_api import create_discussion # noqa: F401
334337
from .hf_api import create_pull_request # noqa: F401
335338
from .hf_api import create_repo # noqa: F401
336339
from .hf_api import create_tag # noqa: F401
337340
from .hf_api import dataset_info # noqa: F401
341+
from .hf_api import delete_branch # noqa: F401
338342
from .hf_api import delete_file # noqa: F401
339343
from .hf_api import delete_folder # noqa: F401
340344
from .hf_api import delete_repo # noqa: F401

src/huggingface_hub/hf_api.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,6 +2425,103 @@ def delete_folder(
24252425
parent_commit=parent_commit,
24262426
)
24272427

2428+
@validate_hf_hub_args
2429+
def create_branch(
2430+
self,
2431+
repo_id: str,
2432+
*,
2433+
branch: str,
2434+
token: Optional[str] = None,
2435+
repo_type: Optional[str] = None,
2436+
) -> None:
2437+
"""
2438+
Create a new branch from `main` on a repo on the Hub.
2439+
2440+
Args:
2441+
repo_id (`str`):
2442+
The repository in which the branch will be created.
2443+
Example: `"user/my-cool-model"`.
2444+
2445+
branch (`str`):
2446+
The name of the branch to create.
2447+
2448+
token (`str`, *optional*):
2449+
Authentication token. Will default to the stored token.
2450+
2451+
repo_type (`str`, *optional*):
2452+
Set to `"dataset"` or `"space"` if creating a branch on a dataset or
2453+
space, `None` or `"model"` if tagging a model. Default is `None`.
2454+
2455+
Raises:
2456+
[`~utils.RepositoryNotFoundError`]:
2457+
If repository is not found (error 404): wrong repo_id/repo_type, private
2458+
but not authenticated or repo does not exist.
2459+
[`~utils.BadRequestError`]:
2460+
If invalid reference for a branch. Ex: `refs/pr/5` or 'refs/foo/bar'.
2461+
[`~utils.HfHubHTTPError`]:
2462+
If the branch already exists on the repo (error 409).
2463+
"""
2464+
if repo_type is None:
2465+
repo_type = REPO_TYPE_MODEL
2466+
branch = quote(branch, safe="")
2467+
2468+
# Prepare request
2469+
branch_url = f"{self.endpoint}/api/{repo_type}s/{repo_id}/branch/{branch}"
2470+
headers = self._build_hf_headers(token=token, is_write_action=True)
2471+
2472+
# Create branch
2473+
response = requests.post(url=branch_url, headers=headers)
2474+
hf_raise_for_status(response)
2475+
2476+
@validate_hf_hub_args
2477+
def delete_branch(
2478+
self,
2479+
repo_id: str,
2480+
*,
2481+
branch: str,
2482+
token: Optional[str] = None,
2483+
repo_type: Optional[str] = None,
2484+
) -> None:
2485+
"""
2486+
Delete a branch from a repo on the Hub.
2487+
2488+
Args:
2489+
repo_id (`str`):
2490+
The repository in which a branch will be deleted.
2491+
Example: `"user/my-cool-model"`.
2492+
2493+
branch (`str`):
2494+
The name of the branch to delete.
2495+
2496+
token (`str`, *optional*):
2497+
Authentication token. Will default to the stored token.
2498+
2499+
repo_type (`str`, *optional*):
2500+
Set to `"dataset"` or `"space"` if creating a branch on a dataset or
2501+
space, `None` or `"model"` if tagging a model. Default is `None`.
2502+
2503+
Raises:
2504+
[`~utils.RepositoryNotFoundError`]:
2505+
If repository is not found (error 404): wrong repo_id/repo_type, private
2506+
but not authenticated or repo does not exist.
2507+
[`~utils.HfHubHTTPError`]:
2508+
If trying to delete a protected branch. Ex: `main` cannot be deleted.
2509+
[`~utils.HfHubHTTPError`]:
2510+
If trying to delete a branch that does not exist.
2511+
2512+
"""
2513+
if repo_type is None:
2514+
repo_type = REPO_TYPE_MODEL
2515+
branch = quote(branch, safe="")
2516+
2517+
# Prepare request
2518+
branch_url = f"{self.endpoint}/api/{repo_type}s/{repo_id}/branch/{branch}"
2519+
headers = self._build_hf_headers(token=token, is_write_action=True)
2520+
2521+
# Delete branch
2522+
response = requests.delete(url=branch_url, headers=headers)
2523+
hf_raise_for_status(response)
2524+
24282525
@validate_hf_hub_args
24292526
def create_tag(
24302527
self,
@@ -2501,7 +2598,7 @@ def delete_tag(
25012598
25022599
Args:
25032600
repo_id (`str`):
2504-
The repository in which a commit will be deleted.
2601+
The repository in which a tag will be deleted.
25052602
Example: `"user/my-cool-model"`.
25062603
25072604
tag (`str`):
@@ -2511,20 +2608,19 @@ def delete_tag(
25112608
Authentication token. Will default to the stored token.
25122609
25132610
repo_type (`str`, *optional*):
2514-
Set to `"dataset"` or `"space"` if tagging a dataset or
2515-
space, `None` or `"model"` if tagging a model. Default is
2516-
`None`.
2611+
Set to `"dataset"` or `"space"` if tagging a dataset or space, `None` or
2612+
`"model"` if tagging a model. Default is `None`.
25172613
25182614
Raises:
25192615
[`~utils.RepositoryNotFoundError`]:
25202616
If repository is not found (error 404): wrong repo_id/repo_type, private
25212617
but not authenticated or repo does not exist.
25222618
[`~utils.RevisionNotFoundError`]:
25232619
If tag is not found.
2524-
25252620
"""
25262621
if repo_type is None:
25272622
repo_type = REPO_TYPE_MODEL
2623+
tag = quote(tag, safe="")
25282624

25292625
# Prepare request
25302626
tag_url = f"{self.endpoint}/api/{repo_type}s/{repo_id}/tag/{tag}"
@@ -3443,6 +3539,8 @@ def _warn_if_truncated(
34433539
upload_folder = api.upload_folder
34443540
delete_file = api.delete_file
34453541
delete_folder = api.delete_folder
3542+
create_branch = api.create_branch
3543+
delete_branch = api.delete_branch
34463544
create_tag = api.create_tag
34473545
delete_tag = api.delete_tag
34483546
get_full_repo_name = api.get_full_repo_name

tests/test_hf_api.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
repo_type_and_id_from_hf_id,
6262
)
6363
from huggingface_hub.utils import (
64+
BadRequestError,
6465
EntryNotFoundError,
6566
HfFolder,
6667
HfHubHTTPError,
@@ -1329,6 +1330,75 @@ def test_delete_tag_with_branch_name(self) -> None:
13291330
self._api.delete_tag(self._repo_id, tag="main")
13301331

13311332

1333+
class HfApiBranchEndpointTest(HfApiCommonTestWithLogin):
1334+
_user = USER
1335+
_repo_id: str
1336+
1337+
@retry_endpoint
1338+
@use_tmp_repo()
1339+
def test_create_and_delete_branch(self) -> None:
1340+
"""Test `create_branch` from main branch."""
1341+
self._api.create_branch(self._repo_id, branch="cool-branch")
1342+
1343+
# Check `cool-branch` branch exists
1344+
self._api.model_info(self._repo_id, revision="cool-branch")
1345+
1346+
# Delete it
1347+
self._api.delete_branch(self._repo_id, branch="cool-branch")
1348+
1349+
# Check doesn't exist anymore
1350+
with self.assertRaises(RevisionNotFoundError):
1351+
self._api.model_info(self._repo_id, revision="cool-branch")
1352+
1353+
@retry_endpoint
1354+
@use_tmp_repo()
1355+
def test_create_branch_existing_branch_fails(self) -> None:
1356+
"""Test `create_branch` on existing branch."""
1357+
self._api.create_branch(self._repo_id, branch="cool-branch")
1358+
1359+
with self.assertRaisesRegex(HfHubHTTPError, "Reference already exists"):
1360+
self._api.create_branch(self._repo_id, branch="cool-branch")
1361+
1362+
with self.assertRaisesRegex(HfHubHTTPError, "Reference already exists"):
1363+
self._api.create_branch(self._repo_id, branch="main")
1364+
1365+
@retry_endpoint
1366+
@use_tmp_repo()
1367+
def test_create_branch_existing_tag_does_not_fail(self) -> None:
1368+
"""Test `create_branch` on existing tag."""
1369+
self._api.create_tag(self._repo_id, tag="tag")
1370+
self._api.create_branch(self._repo_id, branch="tag")
1371+
1372+
@retry_endpoint
1373+
@use_tmp_repo()
1374+
def test_create_branch_forbidden_ref_branch_fails(self) -> None:
1375+
"""Test `create_branch` on forbidden ref branch."""
1376+
with self.assertRaisesRegex(BadRequestError, "Invalid reference for a branch"):
1377+
self._api.create_branch(self._repo_id, branch="refs/pr/5")
1378+
1379+
with self.assertRaisesRegex(BadRequestError, "Invalid reference for a branch"):
1380+
self._api.create_branch(self._repo_id, branch="refs/something/random")
1381+
1382+
@retry_endpoint
1383+
@use_tmp_repo()
1384+
def test_delete_branch_on_protected_branch_fails(self) -> None:
1385+
"""Test `delete_branch` fails on protected branch."""
1386+
with self.assertRaisesRegex(HfHubHTTPError, "Cannot delete refs/heads/main"):
1387+
self._api.delete_branch(self._repo_id, branch="main")
1388+
1389+
@retry_endpoint
1390+
@use_tmp_repo()
1391+
def test_delete_branch_on_missing_branch_fails(self) -> None:
1392+
"""Test `delete_branch` fails on missing branch."""
1393+
with self.assertRaisesRegex(HfHubHTTPError, "Reference does not exist"):
1394+
self._api.delete_branch(self._repo_id, branch="cool-branch")
1395+
1396+
# Using a tag instead of branch -> fails
1397+
self._api.create_tag(self._repo_id, tag="cool-tag")
1398+
with self.assertRaisesRegex(HfHubHTTPError, "Reference does not exist"):
1399+
self._api.delete_branch(self._repo_id, branch="cool-tag")
1400+
1401+
13321402
class HfApiPublicStagingTest(unittest.TestCase):
13331403
def setUp(self) -> None:
13341404
self._api = HfApi()

0 commit comments

Comments
 (0)