Skip to content

Commit cbe0846

Browse files
Add new HF commands (#3384)
* [1.0] Httpx migration (#3328) * first httpx integration * more migration * some fixes * download workflow should work * Fix repocard and error utils tests * fix hf-file-system * gix http utils tests * more fixes * fix some inference tests * fix test_file_download tests * async inference client * async code should be good * Define RemoteEntryFileNotFound explicitly (+some fixes) * fix async code quality * torch ok * fix hf_file_system * fix errors tests * mock * fix test_cli mock * fix commit scheduler * add fileno test * no more requests anywhere * fix test_file_download * tmp requests * Update src/huggingface_hub/utils/_http.py Co-authored-by: célina <[email protected]> * Update src/huggingface_hub/utils/_http.py Co-authored-by: célina <[email protected]> * Update src/huggingface_hub/hf_file_system.py Co-authored-by: célina <[email protected]> * not async * fix tests --------- Co-authored-by: célina <[email protected]> * Bump minimal version to Python3.9 (#3343) * Bump minimal version to Python3.9 * use built-in generics * code quality * new batch * yet another btach * fix dataclass_with_extra * fix * keep Type for strict dataclasses * fix test * Remove `HfFolder` and `InferenceAPI` classes (#3344) * Remove HfFolder * Remove InferenceAPI * more recent gradio * bump pytest * fix python 3.9? * install gradio only on python 3.10+ * fix tests * fix tests * fix * [v1.0] Remove more deprecated stuff (#3345) * remove constants.-hf_cache_home * remove smoothly_deprecate_use_auth_token * remove get_token_permission * remove update_repo_visibility * remove is_write_action arg * remove write_permission arg from login methods * new parameter skip_if_logged_in in login methods * Remove resume_download / force_filename parameters * Remove deprecated local_dir_use_symlinks parameter * Remove deprecated language, library, task, tags from list_models * Return commit URL in upload_file/upload_folder (previously url to file/folder on the Hub) * fix upload_file/upload_folder tests * smoothly_deprecate_legacy_arguments everywhere * code quality * fix tests * fix xet tests * [v1.0] Remove `Repository` class (#3346) * Remove Repository class + adapt docs * remove fr git_vs_http * bump to 1.0.0.dev0 * Remove _deprecate_positional_args on login methods (#3349) * [v1.0] Remove imports kept only for backward compatibility (#3350) * Remove imports kept only for backward compatibility * fix tests * [v1.0] Remove keras2 utilities (#3352) * Remove keras2 utilities * remove keras from init * [v1.0] Remove anything tensorflow-related + deps (#3354) * Remove anything tensorflow-related + deps * init * fix tests * fix conflicts in tests * Release: v1.0.0.rc0 * [v1.0] Update "HTTP backend" docs + `git_vs_http` guide (#3357) * HTTP configuration docs * http configuration docs * refactored git_vs_http * fix import * fix docs? * Update docs/source/en/package_reference/utilities.md Co-authored-by: célina <[email protected]> --------- Co-authored-by: célina <[email protected]> * Refactor CLI implementation using Typer (#3372) * Refactor CLI implementation using Typer (#3365) * migrate CLI to typer * (#3364) disable rich in all cases * update tests * make typer-slim a required dep * use Annotated * fix linting issues * fix tests * refactoring * update docs * use built in types * fix mypy * call whoami directly * lint * Apply suggestions from code review Co-authored-by: Lucain <[email protected]> * import Annotated from typing * Use Enums * set verbosity globally * refactor scan cache and update version docstring * centralize where Typer is defined * no need for ... * rename enum * no need for extra param name * docstring * revert * centralize arguments and options definition * add library version when initializing HfApi * add auto-completion * sort commands alphabetically * suggestions * centralize jobs params and HfApi initialization * fix --------- Co-authored-by: Lucain <[email protected]> * update docs --------- Co-authored-by: Lucain <[email protected]> * add hf repo delete command * add repo settings, repo move, repo branch commands * fix test * Apply suggestions from code review Co-authored-by: Lucain <[email protected]> --------- Co-authored-by: Lucain <[email protected]> Co-authored-by: Lucain Pouget <[email protected]>
1 parent c727e9e commit cbe0846

File tree

2 files changed

+339
-5
lines changed

2 files changed

+339
-5
lines changed

src/huggingface_hub/cli/repo.py

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
hf repo create my-cool-model --private
2222
"""
2323

24+
import enum
2425
from typing import Annotated, Optional
2526

2627
import typer
@@ -44,8 +45,16 @@
4445
logger = logging.get_logger(__name__)
4546

4647
repo_cli = typer_factory(help="Manage repos on the Hub.")
47-
tag_app = typer_factory(help="Manage tags for a repo on the Hub.")
48-
repo_cli.add_typer(tag_app, name="tag")
48+
tag_cli = typer_factory(help="Manage tags for a repo on the Hub.")
49+
branch_cli = typer_factory(help="Manage branches for a repo on the Hub.")
50+
repo_cli.add_typer(tag_cli, name="tag")
51+
repo_cli.add_typer(branch_cli, name="branch")
52+
53+
54+
class GatedChoices(str, enum.Enum):
55+
auto = "auto"
56+
manual = "manual"
57+
false = "false"
4958

5059

5160
@repo_cli.command("create", help="Create a new repo on the Hub.")
@@ -87,7 +96,130 @@ def repo_create(
8796
print(f"Your repo is now available at {ANSI.bold(repo_url)}")
8897

8998

90-
@tag_app.command("create", help="Create a tag for a repo.")
99+
@repo_cli.command("delete", help="Delete a repo from the Hub. this is an irreversible operation.")
100+
def repo_delete(
101+
repo_id: RepoIdArg,
102+
repo_type: RepoTypeOpt = RepoType.model,
103+
token: TokenOpt = None,
104+
missing_ok: Annotated[
105+
bool,
106+
typer.Option(
107+
help="If set to True, do not raise an error if repo does not exist.",
108+
),
109+
] = False,
110+
) -> None:
111+
api = get_hf_api(token=token)
112+
api.delete_repo(
113+
repo_id=repo_id,
114+
repo_type=repo_type.value,
115+
missing_ok=missing_ok,
116+
)
117+
print(f"Successfully deleted {ANSI.bold(repo_id)} on the Hub.")
118+
119+
120+
@repo_cli.command("move", help="Move a repository from a namespace to another namespace.")
121+
def repo_move(
122+
from_id: RepoIdArg,
123+
to_id: RepoIdArg,
124+
token: TokenOpt = None,
125+
repo_type: RepoTypeOpt = RepoType.model,
126+
) -> None:
127+
api = get_hf_api(token=token)
128+
api.move_repo(
129+
from_id=from_id,
130+
to_id=to_id,
131+
repo_type=repo_type.value,
132+
)
133+
print(f"Successfully moved {ANSI.bold(from_id)} to {ANSI.bold(to_id)} on the Hub.")
134+
135+
136+
@repo_cli.command("settings", help="Update the settings of a repository.")
137+
def repo_settings(
138+
repo_id: RepoIdArg,
139+
gated: Annotated[
140+
Optional[GatedChoices],
141+
typer.Option(
142+
help="The gated status for the repository.",
143+
),
144+
] = None,
145+
private: Annotated[
146+
Optional[bool],
147+
typer.Option(
148+
help="Whether the repository should be private.",
149+
),
150+
] = None,
151+
xet_enabled: Annotated[
152+
Optional[bool],
153+
typer.Option(
154+
help=" Whether the repository should be enabled for Xet Storage.",
155+
),
156+
] = None,
157+
token: TokenOpt = None,
158+
repo_type: RepoTypeOpt = RepoType.model,
159+
) -> None:
160+
api = get_hf_api(token=token)
161+
api.update_repo_settings(
162+
repo_id=repo_id,
163+
gated=(gated.value if gated else None), # type: ignore [arg-type]
164+
private=private,
165+
xet_enabled=xet_enabled,
166+
repo_type=repo_type.value,
167+
)
168+
print(f"Successfully updated the settings of {ANSI.bold(repo_id)} on the Hub.")
169+
170+
171+
@branch_cli.command("create", help="Create a new branch for a repo on the Hub.")
172+
def branch_create(
173+
repo_id: RepoIdArg,
174+
branch: Annotated[
175+
str,
176+
typer.Argument(
177+
help="The name of the branch to create.",
178+
),
179+
],
180+
revision: RevisionOpt = None,
181+
token: TokenOpt = None,
182+
repo_type: RepoTypeOpt = RepoType.model,
183+
exist_ok: Annotated[
184+
bool,
185+
typer.Option(
186+
help="If set to True, do not raise an error if branch already exists.",
187+
),
188+
] = False,
189+
) -> None:
190+
api = get_hf_api(token=token)
191+
api.create_branch(
192+
repo_id=repo_id,
193+
branch=branch,
194+
revision=revision,
195+
repo_type=repo_type.value,
196+
exist_ok=exist_ok,
197+
)
198+
print(f"Successfully created {ANSI.bold(branch)} branch on {repo_type.value} {ANSI.bold(repo_id)}")
199+
200+
201+
@branch_cli.command("delete", help="Delete a branch from a repo on the Hub.")
202+
def branch_delete(
203+
repo_id: RepoIdArg,
204+
branch: Annotated[
205+
str,
206+
typer.Argument(
207+
help="The name of the branch to delete.",
208+
),
209+
],
210+
token: TokenOpt = None,
211+
repo_type: RepoTypeOpt = RepoType.model,
212+
) -> None:
213+
api = get_hf_api(token=token)
214+
api.delete_branch(
215+
repo_id=repo_id,
216+
branch=branch,
217+
repo_type=repo_type.value,
218+
)
219+
print(f"Successfully deleted {ANSI.bold(branch)} branch on {repo_type.value} {ANSI.bold(repo_id)}")
220+
221+
222+
@tag_cli.command("create", help="Create a tag for a repo.")
91223
def tag_create(
92224
repo_id: RepoIdArg,
93225
tag: Annotated[
@@ -127,7 +259,7 @@ def tag_create(
127259
print(f"Tag {ANSI.bold(tag)} created on {ANSI.bold(repo_id)}")
128260

129261

130-
@tag_app.command("list", help="List tags for a repo.")
262+
@tag_cli.command("list", help="List tags for a repo.")
131263
def tag_list(
132264
repo_id: RepoIdArg,
133265
token: TokenOpt = None,
@@ -152,7 +284,7 @@ def tag_list(
152284
print(t.name)
153285

154286

155-
@tag_app.command("delete", help="Delete a tag for a repo.")
287+
@tag_cli.command("delete", help="Delete a tag for a repo.")
156288
def tag_delete(
157289
repo_id: RepoIdArg,
158290
tag: Annotated[

tests/test_cli.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,208 @@ def test_tag_delete_basic(self, runner: CliRunner) -> None:
778778
api.delete_tag.assert_called_once_with(repo_id=DUMMY_MODEL_ID, tag="1.0", repo_type="model")
779779

780780

781+
class TestBranchCommands:
782+
def test_branch_create_basic(self, runner: CliRunner) -> None:
783+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
784+
api = api_cls.return_value
785+
result = runner.invoke(app, ["repo", "branch", "create", DUMMY_MODEL_ID, "dev"])
786+
assert result.exit_code == 0
787+
api_cls.assert_called_once_with(token=None)
788+
api.create_branch.assert_called_once_with(
789+
repo_id=DUMMY_MODEL_ID,
790+
branch="dev",
791+
revision=None,
792+
repo_type="model",
793+
exist_ok=False,
794+
)
795+
796+
def test_branch_create_with_all_options(self, runner: CliRunner) -> None:
797+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
798+
api = api_cls.return_value
799+
result = runner.invoke(
800+
app,
801+
[
802+
"repo",
803+
"branch",
804+
"create",
805+
DUMMY_MODEL_ID,
806+
"dev",
807+
"--repo-type",
808+
"dataset",
809+
"--revision",
810+
"v1.0.0",
811+
"--token",
812+
"my-token",
813+
"--exist-ok",
814+
],
815+
)
816+
assert result.exit_code == 0
817+
api_cls.assert_called_once_with(token="my-token")
818+
api.create_branch.assert_called_once_with(
819+
repo_id=DUMMY_MODEL_ID,
820+
branch="dev",
821+
revision="v1.0.0",
822+
repo_type="dataset",
823+
exist_ok=True,
824+
)
825+
826+
def test_branch_delete_basic(self, runner: CliRunner) -> None:
827+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
828+
api = api_cls.return_value
829+
result = runner.invoke(app, ["repo", "branch", "delete", DUMMY_MODEL_ID, "dev"])
830+
assert result.exit_code == 0
831+
api_cls.assert_called_once_with(token=None)
832+
api.delete_branch.assert_called_once_with(
833+
repo_id=DUMMY_MODEL_ID,
834+
branch="dev",
835+
repo_type="model",
836+
)
837+
838+
def test_branch_delete_with_all_options(self, runner: CliRunner) -> None:
839+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
840+
api = api_cls.return_value
841+
result = runner.invoke(
842+
app,
843+
[
844+
"repo",
845+
"branch",
846+
"delete",
847+
DUMMY_MODEL_ID,
848+
"dev",
849+
"--repo-type",
850+
"dataset",
851+
"--token",
852+
"my-token",
853+
],
854+
)
855+
assert result.exit_code == 0
856+
api_cls.assert_called_once_with(token="my-token")
857+
api.delete_branch.assert_called_once_with(
858+
repo_id=DUMMY_MODEL_ID,
859+
branch="dev",
860+
repo_type="dataset",
861+
)
862+
863+
864+
class TestRepoMoveCommand:
865+
def test_repo_move_basic(self, runner: CliRunner) -> None:
866+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
867+
api = api_cls.return_value
868+
result = runner.invoke(app, ["repo", "move", DUMMY_MODEL_ID, "new-id"])
869+
assert result.exit_code == 0
870+
api_cls.assert_called_once_with(token=None)
871+
api.move_repo.assert_called_once_with(
872+
from_id=DUMMY_MODEL_ID,
873+
to_id="new-id",
874+
repo_type="model",
875+
)
876+
877+
def test_repo_move_with_all_options(self, runner: CliRunner) -> None:
878+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
879+
api = api_cls.return_value
880+
result = runner.invoke(
881+
app,
882+
[
883+
"repo",
884+
"move",
885+
DUMMY_MODEL_ID,
886+
"new-id",
887+
"--repo-type",
888+
"dataset",
889+
"--token",
890+
"my-token",
891+
],
892+
)
893+
assert result.exit_code == 0
894+
api_cls.assert_called_once_with(token="my-token")
895+
api.move_repo.assert_called_once_with(
896+
from_id=DUMMY_MODEL_ID,
897+
to_id="new-id",
898+
repo_type="dataset",
899+
)
900+
901+
902+
class TestRepoSettingsCommand:
903+
def test_repo_settings_basic(self, runner: CliRunner) -> None:
904+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
905+
api = api_cls.return_value
906+
result = runner.invoke(app, ["repo", "settings", DUMMY_MODEL_ID])
907+
assert result.exit_code == 0
908+
api_cls.assert_called_once_with(token=None)
909+
api.update_repo_settings.assert_called_once_with(
910+
repo_id=DUMMY_MODEL_ID,
911+
gated=None,
912+
private=None,
913+
xet_enabled=None,
914+
repo_type="model",
915+
)
916+
917+
def test_repo_settings_with_all_options(self, runner: CliRunner) -> None:
918+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
919+
api = api_cls.return_value
920+
result = runner.invoke(
921+
app,
922+
[
923+
"repo",
924+
"settings",
925+
DUMMY_MODEL_ID,
926+
"--gated",
927+
"manual",
928+
"--private",
929+
"--repo-type",
930+
"dataset",
931+
"--token",
932+
"my-token",
933+
],
934+
)
935+
assert result.exit_code == 0
936+
api_cls.assert_called_once_with(token="my-token")
937+
kwargs = api.update_repo_settings.call_args.kwargs
938+
assert kwargs["repo_id"] == DUMMY_MODEL_ID
939+
assert kwargs["repo_type"] == "dataset"
940+
assert kwargs["private"] is True
941+
assert kwargs["xet_enabled"] is None
942+
assert kwargs["gated"] == "manual"
943+
944+
945+
class TestRepoDeleteCommand:
946+
def test_repo_delete_basic(self, runner: CliRunner) -> None:
947+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
948+
api = api_cls.return_value
949+
result = runner.invoke(app, ["repo", "delete", DUMMY_MODEL_ID])
950+
assert result.exit_code == 0
951+
api_cls.assert_called_once_with(token=None)
952+
api.delete_repo.assert_called_once_with(
953+
repo_id=DUMMY_MODEL_ID,
954+
repo_type="model",
955+
missing_ok=False,
956+
)
957+
958+
def test_repo_delete_with_all_options(self, runner: CliRunner) -> None:
959+
with patch("huggingface_hub.cli.repo.get_hf_api") as api_cls:
960+
api = api_cls.return_value
961+
result = runner.invoke(
962+
app,
963+
[
964+
"repo",
965+
"delete",
966+
DUMMY_MODEL_ID,
967+
"--repo-type",
968+
"dataset",
969+
"--token",
970+
"my-token",
971+
"--missing-ok",
972+
],
973+
)
974+
assert result.exit_code == 0
975+
api_cls.assert_called_once_with(token="my-token")
976+
api.delete_repo.assert_called_once_with(
977+
repo_id=DUMMY_MODEL_ID,
978+
repo_type="dataset",
979+
missing_ok=True,
980+
)
981+
982+
781983
@contextmanager
782984
def tmp_current_directory() -> Generator[str, None, None]:
783985
with SoftTemporaryDirectory() as tmp_dir:

0 commit comments

Comments
 (0)