repos.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. # Copyright 2025 The HuggingFace Team. All rights reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Contains commands to interact with repositories on the Hugging Face Hub.
  15. Usage:
  16. # create a new dataset repo on the Hub
  17. hf repos create my-cool-dataset --repo-type=dataset
  18. # create a private model repo on the Hub
  19. hf repos create my-cool-model --private
  20. # delete files from a repo on the Hub
  21. hf repos delete-files my-model file.txt
  22. """
  23. import enum
  24. from typing import Annotated
  25. import typer
  26. from huggingface_hub import SpaceHardware, SpaceStorage
  27. from huggingface_hub.errors import CLIError, HfHubHTTPError, RepositoryNotFoundError, RevisionNotFoundError
  28. from ._cli_utils import (
  29. EnvFileOpt,
  30. EnvOpt,
  31. FormatWithAutoOpt,
  32. PrivateOpt,
  33. RepoIdArg,
  34. RepoType,
  35. RepoTypeOpt,
  36. RevisionOpt,
  37. SecretsFileOpt,
  38. SecretsOpt,
  39. TokenOpt,
  40. VolumesOpt,
  41. env_map_to_key_value_list,
  42. get_hf_api,
  43. parse_env_map,
  44. parse_volumes,
  45. typer_factory,
  46. )
  47. from ._output import OutputFormatWithAuto, out
  48. repos_cli = typer_factory(help="Manage repos on the Hub.")
  49. @repos_cli.callback(invoke_without_command=True)
  50. def _repos_callback(ctx: typer.Context) -> None:
  51. if ctx.info_name == "repo":
  52. out.warning("`hf repo` is deprecated in favor of `hf repos`.")
  53. tag_cli = typer_factory(help="Manage tags for a repo on the Hub.")
  54. branch_cli = typer_factory(help="Manage branches for a repo on the Hub.")
  55. repos_cli.add_typer(tag_cli, name="tag")
  56. repos_cli.add_typer(branch_cli, name="branch")
  57. class GatedChoices(str, enum.Enum):
  58. auto = "auto"
  59. manual = "manual"
  60. false = "false"
  61. PublicOpt = Annotated[
  62. bool | None,
  63. typer.Option(
  64. "--public",
  65. help="Whether to make the repo public. Ignored if the repo already exists.",
  66. ),
  67. ]
  68. ProtectedOpt = Annotated[
  69. bool | None,
  70. typer.Option(
  71. "--protected",
  72. help="Whether to make the Space protected (Spaces only). Ignored if the repo already exists.",
  73. ),
  74. ]
  75. SpaceHardwareOpt = Annotated[
  76. SpaceHardware | None,
  77. typer.Option(
  78. "--flavor",
  79. help="Space hardware flavor (e.g. 'cpu-basic', 't4-medium', 'l4x4'). Only for Spaces.",
  80. ),
  81. ]
  82. SpaceStorageOpt = Annotated[
  83. SpaceStorage | None,
  84. typer.Option(
  85. "--storage",
  86. help="(Deprecated, use volumes instead) Space persistent storage tier ('small', 'medium', or 'large'). Only for Spaces.",
  87. ),
  88. ]
  89. SpaceSleepTimeOpt = Annotated[
  90. int | None,
  91. typer.Option(
  92. "--sleep-time",
  93. help="Seconds of inactivity before the Space is put to sleep. Use -1 to disable. Only for Spaces.",
  94. ),
  95. ]
  96. @repos_cli.command(
  97. "create",
  98. examples=[
  99. "hf repos create my-model",
  100. "hf repos create my-dataset --repo-type dataset --private",
  101. "hf repos create my-space --type space --space-sdk gradio --flavor t4-medium --secrets HF_TOKEN -e THEME=dark --protected",
  102. "hf repos create my-space --type space --space-sdk gradio -v hf://gpt2:/models -v hf://buckets/org/b:/data",
  103. ],
  104. )
  105. def repo_create(
  106. repo_id: RepoIdArg,
  107. repo_type: RepoTypeOpt = RepoType.model,
  108. space_sdk: Annotated[
  109. str | None,
  110. typer.Option(
  111. help="Hugging Face Spaces SDK type. Required when --type is set to 'space'.",
  112. ),
  113. ] = None,
  114. private: PrivateOpt = None,
  115. public: PublicOpt = None,
  116. protected: ProtectedOpt = None,
  117. token: TokenOpt = None,
  118. exist_ok: Annotated[
  119. bool,
  120. typer.Option(
  121. help="Do not raise an error if repo already exists.",
  122. ),
  123. ] = False,
  124. resource_group_id: Annotated[
  125. str | None,
  126. typer.Option(
  127. help="Resource group in which to create the repo. Resource groups is only available for Enterprise Hub organizations.",
  128. ),
  129. ] = None,
  130. hardware: SpaceHardwareOpt = None,
  131. storage: SpaceStorageOpt = None,
  132. sleep_time: SpaceSleepTimeOpt = None,
  133. secrets: SecretsOpt = None,
  134. secrets_file: SecretsFileOpt = None,
  135. env: EnvOpt = None,
  136. env_file: EnvFileOpt = None,
  137. volume: VolumesOpt = None,
  138. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  139. ) -> None:
  140. """Create a new repo on the Hub."""
  141. api = get_hf_api(token=token)
  142. repo_url = api.create_repo(
  143. repo_id=repo_id,
  144. repo_type=repo_type.value,
  145. visibility="private" if private else "public" if public else "protected" if protected else None, # type: ignore [arg-type]
  146. token=token,
  147. exist_ok=exist_ok,
  148. resource_group_id=resource_group_id,
  149. space_sdk=space_sdk,
  150. space_hardware=hardware,
  151. space_storage=storage,
  152. space_sleep_time=sleep_time,
  153. space_secrets=env_map_to_key_value_list(parse_env_map(secrets, secrets_file)),
  154. space_variables=env_map_to_key_value_list(parse_env_map(env, env_file)),
  155. space_volumes=parse_volumes(volume),
  156. )
  157. out.result("Repo created", repo_id=repo_url.repo_id, url=str(repo_url))
  158. @repos_cli.command(
  159. "duplicate",
  160. examples=[
  161. "hf repos duplicate openai/gdpval --type dataset",
  162. "hf repos duplicate multimodalart/dreambooth-training my-dreambooth --type space --flavor l4x4 --secrets HF_TOKEN --private",
  163. "hf repos duplicate org/my-space my-space --type space -v hf://gpt2:/models -v hf://buckets/org/b:/data",
  164. ],
  165. )
  166. def repo_duplicate(
  167. from_id: RepoIdArg,
  168. to_id: Annotated[
  169. str | None,
  170. typer.Argument(
  171. help="Destination repo ID (e.g. `myorg/my-copy`). Defaults to your namespace with the same repo name.",
  172. ),
  173. ] = None,
  174. repo_type: RepoTypeOpt = RepoType.model,
  175. private: PrivateOpt = None,
  176. public: PublicOpt = None,
  177. protected: ProtectedOpt = None,
  178. token: TokenOpt = None,
  179. exist_ok: Annotated[
  180. bool,
  181. typer.Option(
  182. help="Do not raise an error if repo already exists.",
  183. ),
  184. ] = False,
  185. hardware: SpaceHardwareOpt = None,
  186. storage: SpaceStorageOpt = None,
  187. sleep_time: SpaceSleepTimeOpt = None,
  188. secrets: SecretsOpt = None,
  189. secrets_file: SecretsFileOpt = None,
  190. env: EnvOpt = None,
  191. env_file: EnvFileOpt = None,
  192. volume: VolumesOpt = None,
  193. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  194. ) -> None:
  195. """Duplicate a repo on the Hub (model, dataset, or Space)."""
  196. api = get_hf_api(token=token)
  197. repo_url = api.duplicate_repo(
  198. from_id=from_id,
  199. to_id=to_id,
  200. repo_type=repo_type.value,
  201. visibility="private" if private else "public" if public else "protected" if protected else None, # type: ignore [arg-type]
  202. token=token,
  203. exist_ok=exist_ok,
  204. space_hardware=hardware,
  205. space_storage=storage,
  206. space_sleep_time=sleep_time,
  207. space_secrets=env_map_to_key_value_list(parse_env_map(secrets, secrets_file)),
  208. space_variables=env_map_to_key_value_list(parse_env_map(env, env_file)),
  209. space_volumes=parse_volumes(volume),
  210. )
  211. out.result("Repo duplicated", from_id=from_id, to_id=repo_url.repo_id, url=str(repo_url))
  212. @repos_cli.command("delete", examples=["hf repos delete my-model"])
  213. def repo_delete(
  214. repo_id: RepoIdArg,
  215. repo_type: RepoTypeOpt = RepoType.model,
  216. token: TokenOpt = None,
  217. missing_ok: Annotated[
  218. bool,
  219. typer.Option(
  220. help="If set to True, do not raise an error if repo does not exist.",
  221. ),
  222. ] = False,
  223. yes: Annotated[
  224. bool,
  225. typer.Option(
  226. "-y",
  227. "--yes",
  228. help="Answer Yes to prompt automatically.",
  229. ),
  230. ] = False,
  231. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  232. ) -> None:
  233. """Delete a repo from the Hub. This is an irreversible operation."""
  234. out.confirm(f"You are about to permanently delete {repo_type.value} '{repo_id}'. Proceed?", yes=yes)
  235. api = get_hf_api(token=token)
  236. api.delete_repo(
  237. repo_id=repo_id,
  238. repo_type=repo_type.value,
  239. missing_ok=missing_ok,
  240. )
  241. out.result("Repo deleted", repo_id=repo_id)
  242. @repos_cli.command("move", examples=["hf repos move old-namespace/my-model new-namespace/my-model"])
  243. def repo_move(
  244. from_id: RepoIdArg,
  245. to_id: RepoIdArg,
  246. token: TokenOpt = None,
  247. repo_type: RepoTypeOpt = RepoType.model,
  248. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  249. ) -> None:
  250. """Move a repository from a namespace to another namespace."""
  251. api = get_hf_api(token=token)
  252. api.move_repo(
  253. from_id=from_id,
  254. to_id=to_id,
  255. repo_type=repo_type.value,
  256. )
  257. out.result("Repo moved", from_id=from_id, to_id=to_id)
  258. @repos_cli.command(
  259. "settings",
  260. examples=[
  261. "hf repos settings my-model --private",
  262. "hf repos settings my-model --gated auto",
  263. "hf repos settings my-space --repo-type space --protected",
  264. ],
  265. )
  266. def repo_settings(
  267. repo_id: RepoIdArg,
  268. gated: Annotated[
  269. GatedChoices | None,
  270. typer.Option(
  271. help="The gated status for the repository.",
  272. ),
  273. ] = None,
  274. private: PrivateOpt = None,
  275. public: PublicOpt = None,
  276. protected: ProtectedOpt = None,
  277. token: TokenOpt = None,
  278. repo_type: RepoTypeOpt = RepoType.model,
  279. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  280. ) -> None:
  281. """Update the settings of a repository."""
  282. api = get_hf_api(token=token)
  283. api.update_repo_settings(
  284. repo_id=repo_id,
  285. gated=(None if gated is None else False if gated is GatedChoices.false else gated.value),
  286. visibility="private" if private else "public" if public else "protected" if protected else None, # type: ignore [arg-type]
  287. repo_type=repo_type.value,
  288. )
  289. out.result("Repo settings updated", repo_id=repo_id)
  290. @repos_cli.command(
  291. "delete-files",
  292. examples=[
  293. "hf repos delete-files my-model file.txt",
  294. 'hf repos delete-files my-model "*.json"',
  295. "hf repos delete-files my-model folder/",
  296. ],
  297. )
  298. def repo_delete_files(
  299. repo_id: RepoIdArg,
  300. patterns: Annotated[
  301. list[str],
  302. typer.Argument(
  303. help="Glob patterns to match files to delete. Based on fnmatch, '*' matches files recursively.",
  304. ),
  305. ],
  306. repo_type: RepoTypeOpt = RepoType.model,
  307. revision: RevisionOpt = None,
  308. commit_message: Annotated[
  309. str | None,
  310. typer.Option(
  311. help="The summary / title / first line of the generated commit.",
  312. ),
  313. ] = None,
  314. commit_description: Annotated[
  315. str | None,
  316. typer.Option(
  317. help="The description of the generated commit.",
  318. ),
  319. ] = None,
  320. create_pr: Annotated[
  321. bool,
  322. typer.Option(
  323. help="Whether to create a new Pull Request for these changes.",
  324. ),
  325. ] = False,
  326. token: TokenOpt = None,
  327. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  328. ) -> None:
  329. """Delete files from a repo on the Hub."""
  330. api = get_hf_api(token=token)
  331. url = api.delete_files(
  332. delete_patterns=patterns,
  333. repo_id=repo_id,
  334. repo_type=repo_type.value,
  335. revision=revision,
  336. commit_message=commit_message,
  337. commit_description=commit_description,
  338. create_pr=create_pr,
  339. )
  340. out.result("Files deleted", repo_id=repo_id, commit_url=url)
  341. @branch_cli.command(
  342. "create",
  343. examples=[
  344. "hf repos branch create my-model dev",
  345. "hf repos branch create my-model dev --revision abc123",
  346. ],
  347. )
  348. def branch_create(
  349. repo_id: RepoIdArg,
  350. branch: Annotated[
  351. str,
  352. typer.Argument(
  353. help="The name of the branch to create.",
  354. ),
  355. ],
  356. revision: RevisionOpt = None,
  357. token: TokenOpt = None,
  358. repo_type: RepoTypeOpt = RepoType.model,
  359. exist_ok: Annotated[
  360. bool,
  361. typer.Option(
  362. help="If set to True, do not raise an error if branch already exists.",
  363. ),
  364. ] = False,
  365. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  366. ) -> None:
  367. """Create a new branch for a repo on the Hub."""
  368. api = get_hf_api(token=token)
  369. api.create_branch(
  370. repo_id=repo_id,
  371. branch=branch,
  372. revision=revision,
  373. repo_type=repo_type.value,
  374. exist_ok=exist_ok,
  375. )
  376. out.result("Branch created", branch=branch, repo_type=repo_type.value, repo_id=repo_id)
  377. @branch_cli.command("delete", examples=["hf repos branch delete my-model dev"])
  378. def branch_delete(
  379. repo_id: RepoIdArg,
  380. branch: Annotated[
  381. str,
  382. typer.Argument(
  383. help="The name of the branch to delete.",
  384. ),
  385. ],
  386. token: TokenOpt = None,
  387. repo_type: RepoTypeOpt = RepoType.model,
  388. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  389. ) -> None:
  390. """Delete a branch from a repo on the Hub."""
  391. api = get_hf_api(token=token)
  392. api.delete_branch(
  393. repo_id=repo_id,
  394. branch=branch,
  395. repo_type=repo_type.value,
  396. )
  397. out.result("Branch deleted", branch=branch, repo_type=repo_type.value, repo_id=repo_id)
  398. @tag_cli.command(
  399. "create",
  400. examples=[
  401. "hf repos tag create my-model v1.0",
  402. 'hf repos tag create my-model v1.0 -m "First release"',
  403. ],
  404. )
  405. def tag_create(
  406. repo_id: RepoIdArg,
  407. tag: Annotated[
  408. str,
  409. typer.Argument(
  410. help="The name of the tag to create.",
  411. ),
  412. ],
  413. message: Annotated[
  414. str | None,
  415. typer.Option(
  416. "-m",
  417. "--message",
  418. help="The description of the tag to create.",
  419. ),
  420. ] = None,
  421. revision: RevisionOpt = None,
  422. token: TokenOpt = None,
  423. repo_type: RepoTypeOpt = RepoType.model,
  424. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  425. ) -> None:
  426. """Create a tag for a repo."""
  427. repo_type_str = repo_type.value
  428. api = get_hf_api(token=token)
  429. try:
  430. api.create_tag(repo_id=repo_id, tag=tag, tag_message=message, revision=revision, repo_type=repo_type_str)
  431. except RepositoryNotFoundError as e:
  432. raise CLIError(f"{repo_type_str.capitalize()} '{repo_id}' not found.") from e
  433. except RevisionNotFoundError as e:
  434. raise CLIError(f"Revision '{revision}' not found.") from e
  435. except HfHubHTTPError as e:
  436. if e.response.status_code == 409:
  437. raise CLIError(f"Tag '{tag}' already exists on '{repo_id}'.") from e
  438. raise
  439. out.result("Tag created", tag=tag, repo_type=repo_type_str, repo_id=repo_id)
  440. @tag_cli.command("list | ls", examples=["hf repos tag list my-model"])
  441. def tag_list(
  442. repo_id: RepoIdArg,
  443. token: TokenOpt = None,
  444. repo_type: RepoTypeOpt = RepoType.model,
  445. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  446. ) -> None:
  447. """List tags for a repo."""
  448. repo_type_str = repo_type.value
  449. api = get_hf_api(token=token)
  450. try:
  451. refs = api.list_repo_refs(repo_id=repo_id, repo_type=repo_type_str)
  452. except RepositoryNotFoundError as e:
  453. raise CLIError(f"{repo_type_str.capitalize()} '{repo_id}' not found.") from e
  454. items = [{"name": t.name, "target_commit": t.target_commit, "ref": t.ref} for t in refs.tags]
  455. out.table(items)
  456. @tag_cli.command("delete", examples=["hf repos tag delete my-model v1.0"])
  457. def tag_delete(
  458. repo_id: RepoIdArg,
  459. tag: Annotated[
  460. str,
  461. typer.Argument(
  462. help="The name of the tag to delete.",
  463. ),
  464. ],
  465. yes: Annotated[
  466. bool,
  467. typer.Option(
  468. "-y",
  469. "--yes",
  470. help="Answer Yes to prompt automatically",
  471. ),
  472. ] = False,
  473. token: TokenOpt = None,
  474. repo_type: RepoTypeOpt = RepoType.model,
  475. format: FormatWithAutoOpt = OutputFormatWithAuto.auto,
  476. ) -> None:
  477. """Delete a tag for a repo."""
  478. repo_type_str = repo_type.value
  479. out.text(f"You are about to delete tag {tag} on {repo_type_str} {repo_id}")
  480. out.confirm("Proceed?", yes=yes)
  481. api = get_hf_api(token=token)
  482. try:
  483. api.delete_tag(repo_id=repo_id, tag=tag, repo_type=repo_type_str)
  484. except RepositoryNotFoundError as e:
  485. raise CLIError(f"{repo_type_str.capitalize()} '{repo_id}' not found.") from e
  486. except RevisionNotFoundError as e:
  487. raise CLIError(f"Tag '{tag}' not found on '{repo_id}'.") from e
  488. out.result("Tag deleted", tag=tag, repo_type=repo_type_str, repo_id=repo_id)