huggingface-hub 0.13.3__py3-none-any.whl → 0.14.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of huggingface-hub might be problematic. Click here for more details.

Files changed (40) hide show
  1. huggingface_hub/__init__.py +59 -5
  2. huggingface_hub/_commit_api.py +26 -71
  3. huggingface_hub/_login.py +17 -16
  4. huggingface_hub/_multi_commits.py +305 -0
  5. huggingface_hub/_snapshot_download.py +4 -0
  6. huggingface_hub/_space_api.py +6 -0
  7. huggingface_hub/_webhooks_payload.py +124 -0
  8. huggingface_hub/_webhooks_server.py +362 -0
  9. huggingface_hub/commands/lfs.py +3 -5
  10. huggingface_hub/commands/user.py +0 -3
  11. huggingface_hub/community.py +21 -0
  12. huggingface_hub/constants.py +3 -0
  13. huggingface_hub/file_download.py +54 -13
  14. huggingface_hub/hf_api.py +666 -139
  15. huggingface_hub/hf_file_system.py +441 -0
  16. huggingface_hub/hub_mixin.py +1 -1
  17. huggingface_hub/inference_api.py +2 -4
  18. huggingface_hub/keras_mixin.py +1 -1
  19. huggingface_hub/lfs.py +196 -176
  20. huggingface_hub/repocard.py +2 -2
  21. huggingface_hub/repository.py +1 -1
  22. huggingface_hub/templates/modelcard_template.md +1 -1
  23. huggingface_hub/utils/__init__.py +8 -11
  24. huggingface_hub/utils/_errors.py +4 -4
  25. huggingface_hub/utils/_experimental.py +65 -0
  26. huggingface_hub/utils/_git_credential.py +1 -80
  27. huggingface_hub/utils/_http.py +85 -2
  28. huggingface_hub/utils/_pagination.py +4 -3
  29. huggingface_hub/utils/_paths.py +2 -0
  30. huggingface_hub/utils/_runtime.py +12 -0
  31. huggingface_hub/utils/_subprocess.py +22 -0
  32. huggingface_hub/utils/_telemetry.py +2 -4
  33. huggingface_hub/utils/tqdm.py +23 -18
  34. {huggingface_hub-0.13.3.dist-info → huggingface_hub-0.14.0.dev0.dist-info}/METADATA +5 -1
  35. huggingface_hub-0.14.0.dev0.dist-info/RECORD +61 -0
  36. {huggingface_hub-0.13.3.dist-info → huggingface_hub-0.14.0.dev0.dist-info}/entry_points.txt +3 -0
  37. huggingface_hub-0.13.3.dist-info/RECORD +0 -56
  38. {huggingface_hub-0.13.3.dist-info → huggingface_hub-0.14.0.dev0.dist-info}/LICENSE +0 -0
  39. {huggingface_hub-0.13.3.dist-info → huggingface_hub-0.14.0.dev0.dist-info}/WHEEL +0 -0
  40. {huggingface_hub-0.13.3.dist-info → huggingface_hub-0.14.0.dev0.dist-info}/top_level.txt +0 -0
huggingface_hub/hf_api.py CHANGED
@@ -27,7 +27,13 @@ from urllib.parse import quote
27
27
  import requests
28
28
  from requests.exceptions import HTTPError
29
29
 
30
- from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
30
+ from huggingface_hub.utils import (
31
+ IGNORE_GIT_FOLDER_PATTERNS,
32
+ EntryNotFoundError,
33
+ RepositoryNotFoundError,
34
+ experimental,
35
+ get_session,
36
+ )
31
37
 
32
38
  from ._commit_api import (
33
39
  CommitOperation,
@@ -38,6 +44,19 @@ from ._commit_api import (
38
44
  upload_lfs_files,
39
45
  warn_on_overwriting_operations,
40
46
  )
47
+ from ._multi_commits import (
48
+ MULTI_COMMIT_PR_CLOSE_COMMENT_FAILURE_BAD_REQUEST_TEMPLATE,
49
+ MULTI_COMMIT_PR_CLOSE_COMMENT_FAILURE_NO_CHANGES_TEMPLATE,
50
+ MULTI_COMMIT_PR_CLOSING_COMMENT_TEMPLATE,
51
+ MULTI_COMMIT_PR_COMPLETION_COMMENT_TEMPLATE,
52
+ MultiCommitException,
53
+ MultiCommitStep,
54
+ MultiCommitStrategy,
55
+ multi_commit_create_pull_request,
56
+ multi_commit_generate_comment,
57
+ multi_commit_parse_pr_description,
58
+ plan_multi_commits,
59
+ )
41
60
  from ._space_api import SpaceHardware, SpaceRuntime
42
61
  from .community import (
43
62
  Discussion,
@@ -58,24 +77,21 @@ from .constants import (
58
77
  SPACES_SDK_TYPES,
59
78
  )
60
79
  from .utils import ( # noqa: F401 # imported for backward compatibility
80
+ BadRequestError,
61
81
  HfFolder,
62
82
  HfHubHTTPError,
63
83
  build_hf_headers,
64
- erase_from_credential_store,
65
84
  filter_repo_objects,
66
85
  hf_raise_for_status,
67
86
  logging,
87
+ paginate,
68
88
  parse_datetime,
69
- read_from_credential_store,
70
89
  validate_hf_hub_args,
71
- write_to_credential_store,
72
90
  )
73
91
  from .utils._deprecation import (
74
92
  _deprecate_arguments,
75
93
  _deprecate_list_output,
76
- _deprecate_method,
77
94
  )
78
- from .utils._pagination import paginate
79
95
  from .utils._typing import Literal, TypedDict
80
96
  from .utils.endpoint_helpers import (
81
97
  AttributeDictionary,
@@ -90,6 +106,7 @@ from .utils.endpoint_helpers import (
90
106
  USERNAME_PLACEHOLDER = "hf_user"
91
107
  _REGEX_DISCUSSION_URL = re.compile(r".*/discussions/(\d+)$")
92
108
 
109
+
93
110
  logger = logging.get_logger(__name__)
94
111
 
95
112
 
@@ -192,6 +209,7 @@ def repo_type_and_id_from_hf_id(hf_id: str, hub_url: Optional[str] = None) -> Tu
192
209
  class BlobLfsInfo(TypedDict, total=False):
193
210
  size: int
194
211
  sha256: str
212
+ pointer_size: int
195
213
 
196
214
 
197
215
  @dataclass
@@ -312,23 +330,21 @@ class RepoUrl(str):
312
330
 
313
331
  class RepoFile(ReprMixin):
314
332
  """
315
- Data structure that represents a public file inside a repo, accessible from
316
- huggingface.co
333
+ Data structure that represents a public file inside a repo, accessible from huggingface.co
317
334
 
318
335
  Args:
319
336
  rfilename (str):
320
- file name, relative to the repo root. This is the only attribute
321
- that's guaranteed to be here, but under certain conditions there can
322
- certain other stuff.
337
+ file name, relative to the repo root. This is the only attribute that's guaranteed to be here, but under
338
+ certain conditions there can certain other stuff.
323
339
  size (`int`, *optional*):
324
- The file's size, in bytes. This attribute is present when `files_metadata` argument
325
- of [`repo_info`] is set to `True`. It's `None` otherwise.
340
+ The file's size, in bytes. This attribute is present when `files_metadata` argument of [`repo_info`] is set
341
+ to `True`. It's `None` otherwise.
326
342
  blob_id (`str`, *optional*):
327
- The file's git OID. This attribute is present when `files_metadata` argument
328
- of [`repo_info`] is set to `True`. It's `None` otherwise.
343
+ The file's git OID. This attribute is present when `files_metadata` argument of [`repo_info`] is set to
344
+ `True`. It's `None` otherwise.
329
345
  lfs (`BlobLfsInfo`, *optional*):
330
- The file's LFS metadata. This attribute is present when`files_metadata` argument
331
- of [`repo_info`] is set to `True` and the file is stored with Git LFS. It's `None` otherwise.
346
+ The file's LFS metadata. This attribute is present when`files_metadata` argument of [`repo_info`] is set to
347
+ `True` and the file is stored with Git LFS. It's `None` otherwise.
332
348
  """
333
349
 
334
350
  def __init__(
@@ -822,7 +838,7 @@ class HfApi:
822
838
  Hugging Face token. Will default to the locally saved token if
823
839
  not provided.
824
840
  """
825
- r = requests.get(
841
+ r = get_session().get(
826
842
  f"{self.endpoint}/api/whoami-v2",
827
843
  headers=self._build_hf_headers(
828
844
  # If `token` is provided and not `None`, it will be used by default.
@@ -857,43 +873,10 @@ class HfApi:
857
873
  except HTTPError:
858
874
  return False
859
875
 
860
- @staticmethod
861
- @_deprecate_method(
862
- version="0.14",
863
- message=(
864
- "`HfApi.set_access_token` is deprecated as it is very ambiguous. Use"
865
- " `login` or `set_git_credential` instead."
866
- ),
867
- )
868
- def set_access_token(access_token: str):
869
- """
870
- Saves the passed access token so git can correctly authenticate the
871
- user.
872
-
873
- Args:
874
- access_token (`str`):
875
- The access token to save.
876
- """
877
- write_to_credential_store(USERNAME_PLACEHOLDER, access_token)
878
-
879
- @staticmethod
880
- @_deprecate_method(
881
- version="0.14",
882
- message=(
883
- "`HfApi.unset_access_token` is deprecated as it is very ambiguous. Use"
884
- " `login` or `unset_git_credential` instead."
885
- ),
886
- )
887
- def unset_access_token():
888
- """
889
- Resets the user's access token.
890
- """
891
- erase_from_credential_store(USERNAME_PLACEHOLDER)
892
-
893
876
  def get_model_tags(self) -> ModelTags:
894
877
  "Gets all valid model tags as a nested namespace object"
895
878
  path = f"{self.endpoint}/api/models-tags-by-type"
896
- r = requests.get(path)
879
+ r = get_session().get(path)
897
880
  hf_raise_for_status(r)
898
881
  d = r.json()
899
882
  return ModelTags(d)
@@ -903,7 +886,7 @@ class HfApi:
903
886
  Gets all valid dataset tags as a nested namespace object.
904
887
  """
905
888
  path = f"{self.endpoint}/api/datasets-tags-by-type"
906
- r = requests.get(path)
889
+ r = get_session().get(path)
907
890
  hf_raise_for_status(r)
908
891
  d = r.json()
909
892
  return DatasetTags(d)
@@ -936,8 +919,7 @@ class HfApi:
936
919
  A string which identify the author (user or organization) of the
937
920
  returned models
938
921
  search (`str`, *optional*):
939
- A string that will be contained in the returned models Example
940
- usage:
922
+ A string that will be contained in the returned model ids.
941
923
  emissions_thresholds (`Tuple`, *optional*):
942
924
  A tuple of two ints or floats representing a minimum and maximum
943
925
  carbon footprint to filter the resulting models with in grams.
@@ -1293,7 +1275,7 @@ class HfApi:
1293
1275
  `List[MetricInfo]`: a list of [`MetricInfo`] objects which.
1294
1276
  """
1295
1277
  path = f"{self.endpoint}/api/metrics"
1296
- r = requests.get(path)
1278
+ r = get_session().get(path)
1297
1279
  hf_raise_for_status(r)
1298
1280
  d = r.json()
1299
1281
  return [MetricInfo(**x) for x in d]
@@ -1427,7 +1409,7 @@ class HfApi:
1427
1409
  """
1428
1410
  if repo_type is None:
1429
1411
  repo_type = REPO_TYPE_MODEL
1430
- response = requests.post(
1412
+ response = get_session().post(
1431
1413
  url=f"{self.endpoint}/api/{repo_type}s/{repo_id}/like",
1432
1414
  headers=self._build_hf_headers(token=token),
1433
1415
  )
@@ -1475,10 +1457,8 @@ class HfApi:
1475
1457
  """
1476
1458
  if repo_type is None:
1477
1459
  repo_type = REPO_TYPE_MODEL
1478
- # TODO: use requests.delete(".../like") instead when https://github.com/huggingface/moon-landing/pull/4813 is merged
1479
- response = requests.delete(
1480
- url=f"{self.endpoint}/api/{repo_type}s/{repo_id}/like",
1481
- headers=self._build_hf_headers(token=token),
1460
+ response = get_session().delete(
1461
+ url=f"{self.endpoint}/api/{repo_type}s/{repo_id}/like", headers=self._build_hf_headers(token=token)
1482
1462
  )
1483
1463
  hf_raise_for_status(response)
1484
1464
 
@@ -1620,7 +1600,7 @@ class HfApi:
1620
1600
  params["securityStatus"] = True
1621
1601
  if files_metadata:
1622
1602
  params["blobs"] = True
1623
- r = requests.get(path, headers=headers, timeout=timeout, params=params)
1603
+ r = get_session().get(path, headers=headers, timeout=timeout, params=params)
1624
1604
  hf_raise_for_status(r)
1625
1605
  d = r.json()
1626
1606
  return ModelInfo(**d)
@@ -1683,7 +1663,7 @@ class HfApi:
1683
1663
  if files_metadata:
1684
1664
  params["blobs"] = True
1685
1665
 
1686
- r = requests.get(path, headers=headers, timeout=timeout, params=params)
1666
+ r = get_session().get(path, headers=headers, timeout=timeout, params=params)
1687
1667
  hf_raise_for_status(r)
1688
1668
  d = r.json()
1689
1669
  return DatasetInfo(**d)
@@ -1746,7 +1726,7 @@ class HfApi:
1746
1726
  if files_metadata:
1747
1727
  params["blobs"] = True
1748
1728
 
1749
- r = requests.get(path, headers=headers, timeout=timeout, params=params)
1729
+ r = get_session().get(path, headers=headers, timeout=timeout, params=params)
1750
1730
  hf_raise_for_status(r)
1751
1731
  d = r.json()
1752
1732
  return SpaceInfo(**d)
@@ -1819,6 +1799,141 @@ class HfApi:
1819
1799
  files_metadata=files_metadata,
1820
1800
  )
1821
1801
 
1802
+ @validate_hf_hub_args
1803
+ def list_files_info(
1804
+ self,
1805
+ repo_id: str,
1806
+ paths: Union[List[str], str, None] = None,
1807
+ *,
1808
+ revision: Optional[str] = None,
1809
+ repo_type: Optional[str] = None,
1810
+ token: Optional[Union[bool, str]] = None,
1811
+ ) -> Iterable[RepoFile]:
1812
+ """
1813
+ List files on a repo and get information about them.
1814
+
1815
+ Takes as input a list of paths. Those paths can be either files or folders. Two server endpoints are called:
1816
+ 1. POST "/paths-info" to get information about the provided paths. Called once.
1817
+ 2. GET "/tree?recursive=True" to paginate over the input folders. Called only if a folder path is provided as
1818
+ input. Will be called multiple times to follow pagination.
1819
+ If no path is provided as input, step 1. is ignored and all files from the repo are listed.
1820
+
1821
+ Args:
1822
+ repo_id (`str`):
1823
+ A namespace (user or an organization) and a repo name separated by a `/`.
1824
+ paths (`Union[List[str], str, None]`, *optional*):
1825
+ The paths to get information about. Paths to files are directly resolved. Paths to folders are resolved
1826
+ recursively which means that information is returned about all files in the folder and its subfolders.
1827
+ If `None`, all files are returned (the default). If a path do not exist, it is ignored without raising
1828
+ an exception.
1829
+ revision (`str`, *optional*):
1830
+ The revision of the repository from which to get the information. Defaults to `"main"` branch.
1831
+ repo_type (`str`, *optional*):
1832
+ The type of the repository from which to get the information (`"model"`, `"dataset"` or `"space"`.
1833
+ Defaults to `"model"`.
1834
+ token (`bool` or `str`, *optional*):
1835
+ A valid authentication token (see https://huggingface.co/settings/token). If `None` or `True` and
1836
+ machine is logged in (through `huggingface-cli login` or [`~huggingface_hub.login`]), token will be
1837
+ retrieved from the cache. If `False`, token is not sent in the request header.
1838
+
1839
+ Returns:
1840
+ `Iterable[RepoFile]`:
1841
+ The information about the files, as an iterable of [`RepoFile`] objects. The order of the files is
1842
+ not guaranteed.
1843
+
1844
+ Raises:
1845
+ [`~utils.RepositoryNotFoundError`]:
1846
+ If repository is not found (error 404): wrong repo_id/repo_type, private but not authenticated or repo
1847
+ does not exist.
1848
+ [`~utils.RevisionNotFoundError`]:
1849
+ If revision is not found (error 404) on the repo.
1850
+
1851
+ Examples:
1852
+
1853
+ Get information about files on a repo.
1854
+ ```py
1855
+ >>> from huggingface_hub import list_files_info
1856
+ >>> files_info = list_files_info("lysandre/arxiv-nlp", ["README.md", "config.json"])
1857
+ >>> files_info
1858
+ <generator object HfApi.list_files_info at 0x7f93b848e730>
1859
+ >>> list(files_info)
1860
+ [
1861
+ RepoFile: {"blob_id": "43bd404b159de6fba7c2f4d3264347668d43af25", "lfs": None, "rfilename": "README.md", "size": 391},
1862
+ RepoFile: {"blob_id": "2f9618c3a19b9a61add74f70bfb121335aeef666", "lfs": None, "rfilename": "config.json", "size": 554},
1863
+ ]
1864
+ ```
1865
+
1866
+ List LFS files from the "vae/" folder in "stabilityai/stable-diffusion-2" repository.
1867
+
1868
+ ```py
1869
+ >>> from huggingface_hub import list_files_info
1870
+ >>> [info.rfilename for info in list_files_info("stabilityai/stable-diffusion-2", "vae") if info.lfs is not None]
1871
+ ['vae/diffusion_pytorch_model.bin', 'vae/diffusion_pytorch_model.safetensors']
1872
+ ```
1873
+
1874
+ List all files on a repo.
1875
+ ```py
1876
+ >>> from huggingface_hub import list_files_info
1877
+ >>> [info.rfilename for info in list_files_info("glue", repo_type="dataset")]
1878
+ ['.gitattributes', 'README.md', 'dataset_infos.json', 'glue.py']
1879
+ ```
1880
+ """
1881
+ repo_type = repo_type or REPO_TYPE_MODEL
1882
+ revision = quote(revision, safe="") if revision is not None else DEFAULT_REVISION
1883
+ headers = self._build_hf_headers(token=token)
1884
+
1885
+ def _format_as_repo_file(info: Dict) -> RepoFile:
1886
+ # Quick alias very specific to the server return type of /paths-info and /tree endpoints. Let's keep this
1887
+ # logic here.
1888
+ rfilename = info.pop("path")
1889
+ size = info.pop("size")
1890
+ blobId = info.pop("oid")
1891
+ lfs = info.pop("lfs", None)
1892
+ info.pop("type", None) # "file" or "folder" -> not needed in practice since we know it's a file
1893
+ # "lastCommit": behavior might change server-side in the near future (it might become optional)
1894
+ # In the meantime, let's remove it so that users don't expect it
1895
+ # TODO: set it back when https://github.com/huggingface/moon-landing/issues/5993 is settled
1896
+ info.pop("lastCommit", None)
1897
+ if lfs is not None:
1898
+ lfs = BlobLfsInfo(size=lfs["size"], sha256=lfs["oid"], pointer_size=lfs["pointerSize"])
1899
+ return RepoFile(rfilename=rfilename, size=size, blobId=blobId, lfs=lfs, **info)
1900
+
1901
+ folder_paths = []
1902
+ if paths is None:
1903
+ # `paths` is not provided => list all files from the repo
1904
+ folder_paths.append("")
1905
+ elif paths == []:
1906
+ # corner case: server would return a 400 error if `paths` is an empty list. Let's return early.
1907
+ return
1908
+ else:
1909
+ # `paths` is provided => get info about those
1910
+ response = get_session().post(
1911
+ f"{self.endpoint}/api/{repo_type}s/{repo_id}/paths-info/{revision}",
1912
+ data={
1913
+ "paths": paths if isinstance(paths, list) else [paths],
1914
+ # "expand": True, # TODO: related to "lastCommit" (see above). Do not return it for now.
1915
+ },
1916
+ headers=headers,
1917
+ )
1918
+ hf_raise_for_status(response)
1919
+ paths_info = response.json()
1920
+
1921
+ # List top-level files first
1922
+ for path_info in paths_info:
1923
+ if path_info["type"] == "file":
1924
+ yield _format_as_repo_file(path_info)
1925
+ else:
1926
+ folder_paths.append(path_info["path"])
1927
+
1928
+ # List files in subdirectories
1929
+ for path in folder_paths:
1930
+ encoded_path = "/" + quote(path, safe="") if path else ""
1931
+ tree_url = f"{self.endpoint}/api/{repo_type}s/{repo_id}/tree/{revision}{encoded_path}"
1932
+ for subpath_info in paginate(path=tree_url, headers=headers, params={"recursive": True}):
1933
+ if subpath_info["type"] == "file":
1934
+ yield _format_as_repo_file(subpath_info)
1935
+
1936
+ @_deprecate_arguments(version="0.17", deprecated_args=["timeout"], custom_message="timeout is not used anymore.")
1822
1937
  @validate_hf_hub_args
1823
1938
  def list_repo_files(
1824
1939
  self,
@@ -1834,35 +1949,26 @@ class HfApi:
1834
1949
 
1835
1950
  Args:
1836
1951
  repo_id (`str`):
1837
- A namespace (user or an organization) and a repo name separated
1838
- by a `/`.
1952
+ A namespace (user or an organization) and a repo name separated by a `/`.
1839
1953
  revision (`str`, *optional*):
1840
- The revision of the model repository from which to get the
1841
- information.
1954
+ The revision of the model repository from which to get the information.
1842
1955
  repo_type (`str`, *optional*):
1843
- Set to `"dataset"` or `"space"` if uploading to a dataset or
1844
- space, `None` or `"model"` if uploading to a model. Default is
1845
- `None`.
1846
- timeout (`float`, *optional*):
1847
- Whether to set a timeout for the request to the Hub.
1956
+ Set to `"dataset"` or `"space"` if uploading to a dataset or space, `None` or `"model"` if uploading to
1957
+ a model. Default is `None`.
1848
1958
  token (`bool` or `str`, *optional*):
1849
- A valid authentication token (see https://huggingface.co/settings/token).
1850
- If `None` or `True` and machine is logged in (through `huggingface-cli login`
1851
- or [`~huggingface_hub.login`]), token will be retrieved from the cache.
1852
- If `False`, token is not sent in the request header.
1959
+ A valid authentication token (see https://huggingface.co/settings/token). If `None` or `True` and
1960
+ machine is logged in (through `huggingface-cli login` or [`~huggingface_hub.login`]), token will be
1961
+ retrieved from the cache. If `False`, token is not sent in the request header.
1853
1962
 
1854
1963
  Returns:
1855
1964
  `List[str]`: the list of files in a given repository.
1856
1965
  """
1857
- # TODO: use https://huggingface.co/api/{repo_type}/{repo_id}/tree/{revision}/{subfolder}
1858
- repo_info = self.repo_info(
1859
- repo_id,
1860
- revision=revision,
1861
- repo_type=repo_type,
1862
- token=token,
1863
- timeout=timeout,
1864
- )
1865
- return [f.rfilename for f in repo_info.siblings]
1966
+ return [
1967
+ f.rfilename
1968
+ for f in self.list_files_info(
1969
+ repo_id=repo_id, paths=None, revision=revision, repo_type=repo_type, token=token
1970
+ )
1971
+ ]
1866
1972
 
1867
1973
  @validate_hf_hub_args
1868
1974
  def list_repo_refs(
@@ -1913,9 +2019,8 @@ class HfApi:
1913
2019
  repo on the Hub.
1914
2020
  """
1915
2021
  repo_type = repo_type or REPO_TYPE_MODEL
1916
- response = requests.get(
1917
- f"{self.endpoint}/api/{repo_type}s/{repo_id}/refs",
1918
- headers=self._build_hf_headers(token=token),
2022
+ response = get_session().get(
2023
+ f"{self.endpoint}/api/{repo_type}s/{repo_id}/refs", headers=self._build_hf_headers(token=token)
1919
2024
  )
1920
2025
  hf_raise_for_status(response)
1921
2026
  data = response.json()
@@ -2074,7 +2179,7 @@ class HfApi:
2074
2179
  # See https://github.com/huggingface/huggingface_hub/pull/733/files#r820604472
2075
2180
  json["lfsmultipartthresh"] = self._lfsmultipartthresh # type: ignore
2076
2181
  headers = self._build_hf_headers(token=token, is_write_action=True)
2077
- r = requests.post(path, headers=headers, json=json)
2182
+ r = get_session().post(path, headers=headers, json=json)
2078
2183
 
2079
2184
  try:
2080
2185
  hf_raise_for_status(r)
@@ -2140,7 +2245,7 @@ class HfApi:
2140
2245
  json["type"] = repo_type
2141
2246
 
2142
2247
  headers = self._build_hf_headers(token=token, is_write_action=True)
2143
- r = requests.delete(path, headers=headers, json=json)
2248
+ r = get_session().delete(path, headers=headers, json=json)
2144
2249
  hf_raise_for_status(r)
2145
2250
 
2146
2251
  @validate_hf_hub_args
@@ -2195,7 +2300,7 @@ class HfApi:
2195
2300
  if repo_type is None:
2196
2301
  repo_type = REPO_TYPE_MODEL # default repo type
2197
2302
 
2198
- r = requests.put(
2303
+ r = get_session().put(
2199
2304
  url=f"{self.endpoint}/api/{repo_type}s/{namespace}/{name}/settings",
2200
2305
  headers=self._build_hf_headers(token=token, is_write_action=True),
2201
2306
  json={"private": private},
@@ -2255,7 +2360,7 @@ class HfApi:
2255
2360
 
2256
2361
  path = f"{self.endpoint}/api/repos/move"
2257
2362
  headers = self._build_hf_headers(token=token, is_write_action=True)
2258
- r = requests.post(path, headers=headers, json=json)
2363
+ r = get_session().post(path, headers=headers, json=json)
2259
2364
  try:
2260
2365
  hf_raise_for_status(r)
2261
2366
  except HfHubHTTPError as e:
@@ -2439,7 +2544,7 @@ class HfApi:
2439
2544
  params = {"create_pr": "1"} if create_pr else None
2440
2545
 
2441
2546
  try:
2442
- commit_resp = requests.post(url=commit_url, headers=headers, data=data, params=params)
2547
+ commit_resp = get_session().post(url=commit_url, headers=headers, data=data, params=params)
2443
2548
  hf_raise_for_status(commit_resp, endpoint_name="commit")
2444
2549
  except RepositoryNotFoundError as e:
2445
2550
  e.append_to_message(_CREATE_COMMIT_NO_REPO_ERROR_MESSAGE)
@@ -2461,6 +2566,306 @@ class HfApi:
2461
2566
  pr_url=commit_data["pullRequestUrl"] if create_pr else None,
2462
2567
  )
2463
2568
 
2569
+ @experimental
2570
+ @validate_hf_hub_args
2571
+ def create_commits_on_pr(
2572
+ self,
2573
+ *,
2574
+ repo_id: str,
2575
+ addition_commits: List[List[CommitOperationAdd]],
2576
+ deletion_commits: List[List[CommitOperationDelete]],
2577
+ commit_message: str,
2578
+ commit_description: Optional[str] = None,
2579
+ token: Optional[str] = None,
2580
+ repo_type: Optional[str] = None,
2581
+ merge_pr: bool = True,
2582
+ num_threads: int = 5, # TODO: use to multithread uploads
2583
+ verbose: bool = False,
2584
+ ) -> str:
2585
+ """Push changes to the Hub in multiple commits.
2586
+
2587
+ Commits are pushed to a draft PR branch. If the upload fails or gets interrupted, it can be resumed. Progress
2588
+ is tracked in the PR description. At the end of the process, the PR is set as open and the title is updated to
2589
+ match the initial commit message. If `merge_pr=True` is passed, the PR is merged automatically.
2590
+
2591
+ All deletion commits are pushed first, followed by the addition commits. The order of the commits is not
2592
+ guaranteed as we might implement parallel commits in the future. Be sure that your are not updating several
2593
+ times the same file.
2594
+
2595
+ <Tip warning={true}>
2596
+
2597
+ `create_commits_on_pr` is experimental. Its API and behavior is subject to change in the future without prior notice.
2598
+
2599
+ </Tip>
2600
+
2601
+ Args:
2602
+ repo_id (`str`):
2603
+ The repository in which the commits will be pushed. Example: `"username/my-cool-model"`.
2604
+
2605
+ addition_commits (`List` of `List` of [`~hf_api.CommitOperationAdd`]):
2606
+ A list containing lists of [`~hf_api.CommitOperationAdd`]. Each sublist will result in a commit on the
2607
+ PR.
2608
+
2609
+ deletion_commits
2610
+ A list containing lists of [`~hf_api.CommitOperationDelete`]. Each sublist will result in a commit on
2611
+ the PR. Deletion commits are pushed before addition commits.
2612
+
2613
+ commit_message (`str`):
2614
+ The summary (first line) of the commit that will be created. Will also be the title of the PR.
2615
+
2616
+ commit_description (`str`, *optional*):
2617
+ The description of the commit that will be created. The description will be added to the PR.
2618
+
2619
+ token (`str`, *optional*):
2620
+ Authentication token, obtained with `HfApi.login` method. Will default to the stored token.
2621
+
2622
+ repo_type (`str`, *optional*):
2623
+ Set to `"dataset"` or `"space"` if uploading to a dataset or space, `None` or `"model"` if uploading to
2624
+ a model. Default is `None`.
2625
+
2626
+ merge_pr (`bool`):
2627
+ If set to `True`, the Pull Request is merged at the end of the process. Defaults to `True`.
2628
+
2629
+ num_threads (`int`, *optional*):
2630
+ Number of concurrent threads for uploading files. Defaults to 5.
2631
+
2632
+ verbose (`bool`):
2633
+ If set to `True`, process will run on verbose mode i.e. print information about the ongoing tasks.
2634
+ Defaults to `False`.
2635
+
2636
+ Returns:
2637
+ `str`: URL to the created PR.
2638
+
2639
+ Example:
2640
+ ```python
2641
+ >>> from huggingface_hub import HfApi, plan_multi_commits
2642
+ >>> addition_commits, deletion_commits = plan_multi_commits(
2643
+ ... operations=[
2644
+ ... CommitOperationAdd(...),
2645
+ ... CommitOperationAdd(...),
2646
+ ... CommitOperationDelete(...),
2647
+ ... CommitOperationDelete(...),
2648
+ ... CommitOperationAdd(...),
2649
+ ... ],
2650
+ ... )
2651
+ >>> HfApi().create_commits_on_pr(
2652
+ ... repo_id="my-cool-model",
2653
+ ... addition_commits=addition_commits,
2654
+ ... deletion_commits=deletion_commits,
2655
+ ... (...)
2656
+ ... verbose=True,
2657
+ ... )
2658
+ ```
2659
+
2660
+ Raises:
2661
+ [`MultiCommitException`]:
2662
+ If an unexpected issue occur in the process: empty commits, unexpected commits in a PR, unexpected PR
2663
+ description, etc.
2664
+
2665
+ <Tip warning={true}>
2666
+
2667
+ `create_commits_on_pr` assumes that the repo already exists on the Hub. If you get a Client error 404, please
2668
+ make sure you are authenticated and that `repo_id` and `repo_type` are set correctly. If repo does not exist,
2669
+ create it first using [`~hf_api.create_repo`].
2670
+
2671
+ </Tip>
2672
+ """
2673
+ logger = logging.get_logger(__name__ + ".create_commits_on_pr")
2674
+ if verbose:
2675
+ logger.setLevel("INFO")
2676
+
2677
+ # 1. Get strategy ID
2678
+ logger.info(
2679
+ f"Will create {len(deletion_commits)} deletion commit(s) and {len(addition_commits)} addition commit(s),"
2680
+ f" totalling {sum(len(ops) for ops in addition_commits+deletion_commits)} atomic operations."
2681
+ )
2682
+ strategy = MultiCommitStrategy(
2683
+ addition_commits=[MultiCommitStep(operations=operations) for operations in addition_commits], # type: ignore
2684
+ deletion_commits=[MultiCommitStep(operations=operations) for operations in deletion_commits], # type: ignore
2685
+ )
2686
+ logger.info(f"Multi-commits strategy with ID {strategy.id}.")
2687
+
2688
+ # 2. Get or create a PR with this strategy ID
2689
+ for discussion in self.get_repo_discussions(repo_id=repo_id, repo_type=repo_type, token=token):
2690
+ # search for a draft PR with strategy ID
2691
+ if discussion.is_pull_request and discussion.status == "draft" and strategy.id in discussion.title:
2692
+ pr = self.get_discussion_details(
2693
+ repo_id=repo_id, discussion_num=discussion.num, repo_type=repo_type, token=token
2694
+ )
2695
+ logger.info(f"PR already exists: {pr.url}. Will resume process where it stopped.")
2696
+ break
2697
+ else:
2698
+ # did not find a PR matching the strategy ID
2699
+ pr = multi_commit_create_pull_request(
2700
+ self,
2701
+ repo_id=repo_id,
2702
+ commit_message=commit_message,
2703
+ commit_description=commit_description,
2704
+ strategy=strategy,
2705
+ token=token,
2706
+ repo_type=repo_type,
2707
+ )
2708
+ logger.info(f"New PR created: {pr.url}")
2709
+
2710
+ # 3. Parse PR description to check consistency with strategy (e.g. same commits are scheduled)
2711
+ for event in pr.events:
2712
+ if isinstance(event, DiscussionComment):
2713
+ pr_comment = event
2714
+ break
2715
+ else:
2716
+ raise MultiCommitException(f"PR #{pr.num} must have at least 1 comment")
2717
+
2718
+ description_commits = multi_commit_parse_pr_description(pr_comment.content)
2719
+ if len(description_commits) != len(strategy.all_steps):
2720
+ raise MultiCommitException(
2721
+ f"Corrupted multi-commit PR #{pr.num}: got {len(description_commits)} steps in"
2722
+ f" description but {len(strategy.all_steps)} in strategy."
2723
+ )
2724
+ for step_id in strategy.all_steps:
2725
+ if step_id not in description_commits:
2726
+ raise MultiCommitException(
2727
+ f"Corrupted multi-commit PR #{pr.num}: expected step {step_id} but didn't find"
2728
+ f" it (have {', '.join(description_commits)})."
2729
+ )
2730
+
2731
+ # 4. Retrieve commit history (and check consistency)
2732
+ commits_on_main_branch = {
2733
+ commit.commit_id
2734
+ for commit in self.list_repo_commits(
2735
+ repo_id=repo_id, repo_type=repo_type, token=token, revision=DEFAULT_REVISION
2736
+ )
2737
+ }
2738
+ pr_commits = [
2739
+ commit
2740
+ for commit in self.list_repo_commits(
2741
+ repo_id=repo_id, repo_type=repo_type, token=token, revision=pr.git_reference
2742
+ )
2743
+ if commit.commit_id not in commits_on_main_branch
2744
+ ]
2745
+ if len(pr_commits) > 0:
2746
+ logger.info(f"Found {len(pr_commits)} existing commits on the PR.")
2747
+
2748
+ # At this point `pr_commits` is a list of commits pushed to the PR. We expect all of these commits (if any) to have
2749
+ # a step_id as title. We raise exception if an unexpected commit has been pushed.
2750
+ if len(pr_commits) > len(strategy.all_steps):
2751
+ raise MultiCommitException(
2752
+ f"Corrupted multi-commit PR #{pr.num}: scheduled {len(strategy.all_steps)} steps but"
2753
+ f" {len(pr_commits)} commits have already been pushed to the PR."
2754
+ )
2755
+
2756
+ # Check which steps are already completed
2757
+ remaining_additions = {step.id: step for step in strategy.addition_commits}
2758
+ remaining_deletions = {step.id: step for step in strategy.deletion_commits}
2759
+ for commit in pr_commits:
2760
+ if commit.title in remaining_additions:
2761
+ step = remaining_additions.pop(commit.title)
2762
+ step.completed = True
2763
+ elif commit.title in remaining_deletions:
2764
+ step = remaining_deletions.pop(commit.title)
2765
+ step.completed = True
2766
+
2767
+ if len(remaining_deletions) > 0 and len(remaining_additions) < len(strategy.addition_commits):
2768
+ raise MultiCommitException(
2769
+ f"Corrupted multi-commit PR #{pr.num}: some addition commits have already been pushed to the PR but"
2770
+ " deletion commits are not all completed yet."
2771
+ )
2772
+ nb_remaining = len(remaining_deletions) + len(remaining_additions)
2773
+ if len(pr_commits) > 0:
2774
+ logger.info(
2775
+ f"{nb_remaining} commits remaining ({len(remaining_deletions)} deletion commits and"
2776
+ f" {len(remaining_additions)} addition commits)"
2777
+ )
2778
+
2779
+ # 5. Push remaining commits to the PR + update description
2780
+ # TODO: multi-thread this
2781
+ for step in list(remaining_deletions.values()) + list(remaining_additions.values()):
2782
+ # Push new commit
2783
+ self.create_commit(
2784
+ repo_id=repo_id,
2785
+ repo_type=repo_type,
2786
+ token=token,
2787
+ commit_message=step.id,
2788
+ revision=pr.git_reference,
2789
+ num_threads=num_threads,
2790
+ operations=step.operations,
2791
+ create_pr=False,
2792
+ )
2793
+ step.completed = True
2794
+ nb_remaining -= 1
2795
+ logger.info(f" step {step.id} completed (still {nb_remaining} to go).")
2796
+
2797
+ # Update PR description
2798
+ self.edit_discussion_comment(
2799
+ repo_id=repo_id,
2800
+ repo_type=repo_type,
2801
+ token=token,
2802
+ discussion_num=pr.num,
2803
+ comment_id=pr_comment.id,
2804
+ new_content=multi_commit_generate_comment(
2805
+ commit_message=commit_message, commit_description=commit_description, strategy=strategy
2806
+ ),
2807
+ )
2808
+ logger.info("All commits have been pushed.")
2809
+
2810
+ # 6. Update PR (and merge)
2811
+ self.rename_discussion(
2812
+ repo_id=repo_id,
2813
+ repo_type=repo_type,
2814
+ token=token,
2815
+ discussion_num=pr.num,
2816
+ new_title=commit_message,
2817
+ )
2818
+ self.change_discussion_status(
2819
+ repo_id=repo_id,
2820
+ repo_type=repo_type,
2821
+ token=token,
2822
+ discussion_num=pr.num,
2823
+ new_status="open",
2824
+ comment=MULTI_COMMIT_PR_COMPLETION_COMMENT_TEMPLATE,
2825
+ )
2826
+ logger.info("PR is now open for reviews.")
2827
+
2828
+ if merge_pr: # User don't want a PR => merge it
2829
+ try:
2830
+ self.merge_pull_request(
2831
+ repo_id=repo_id,
2832
+ repo_type=repo_type,
2833
+ token=token,
2834
+ discussion_num=pr.num,
2835
+ comment=MULTI_COMMIT_PR_CLOSING_COMMENT_TEMPLATE,
2836
+ )
2837
+ logger.info("PR has been automatically merged (`merge_pr=True` was passed).")
2838
+ except BadRequestError as error:
2839
+ if error.server_message is not None and "no associated changes" in error.server_message:
2840
+ # PR cannot be merged as no changes are associated. We close the PR without merging with a comment to
2841
+ # explain.
2842
+ self.change_discussion_status(
2843
+ repo_id=repo_id,
2844
+ repo_type=repo_type,
2845
+ token=token,
2846
+ discussion_num=pr.num,
2847
+ comment=MULTI_COMMIT_PR_CLOSE_COMMENT_FAILURE_NO_CHANGES_TEMPLATE,
2848
+ new_status="closed",
2849
+ )
2850
+ logger.warning("Couldn't merge the PR: no associated changes.")
2851
+ else:
2852
+ # PR cannot be merged for another reason (conflicting files for example). We comment the PR to explain
2853
+ # and re-raise the exception.
2854
+ self.comment_discussion(
2855
+ repo_id=repo_id,
2856
+ repo_type=repo_type,
2857
+ token=token,
2858
+ discussion_num=pr.num,
2859
+ comment=MULTI_COMMIT_PR_CLOSE_COMMENT_FAILURE_BAD_REQUEST_TEMPLATE.format(
2860
+ error_message=error.server_message
2861
+ ),
2862
+ )
2863
+ raise MultiCommitException(
2864
+ f"Couldn't merge Pull Request in multi-commit: {error.server_message}"
2865
+ ) from error
2866
+
2867
+ return pr.url
2868
+
2464
2869
  @validate_hf_hub_args
2465
2870
  def upload_file(
2466
2871
  self,
@@ -2627,9 +3032,11 @@ class HfApi:
2627
3032
  allow_patterns: Optional[Union[List[str], str]] = None,
2628
3033
  ignore_patterns: Optional[Union[List[str], str]] = None,
2629
3034
  delete_patterns: Optional[Union[List[str], str]] = None,
3035
+ multi_commits: bool = False,
3036
+ multi_commits_verbose: bool = False,
2630
3037
  ):
2631
3038
  """
2632
- Upload a local folder to the given repo. The upload is done through a HTTP request and doesn't require git or
3039
+ Upload a local folder to the given repo. The upload is done through a HTTP requests, and doesn't require git or
2633
3040
  git-lfs to be installed.
2634
3041
 
2635
3042
  The structure of the folder will be preserved. Files with the same name already present in the repository will
@@ -2643,9 +3050,12 @@ class HfApi:
2643
3050
  Use the `delete_patterns` argument to specify remote files you want to delete. Input type is the same as for
2644
3051
  `allow_patterns` (see above). If `path_in_repo` is also provided, the patterns are matched against paths
2645
3052
  relative to this folder. For example, `upload_folder(..., path_in_repo="experiment", delete_patterns="logs/*")`
2646
- will delete any remote file under `experiment/logs/`. Note that the `.gitattributes` file will not be deleted
3053
+ will delete any remote file under `./experiment/logs/`. Note that the `.gitattributes` file will not be deleted
2647
3054
  even if it matches the patterns.
2648
3055
 
3056
+ Any `.git/` folder present in any subdirectory will be ignored. However, please be aware that the `.gitignore`
3057
+ file is not taken into account.
3058
+
2649
3059
  Uses `HfApi.create_commit` under the hood.
2650
3060
 
2651
3061
  Args:
@@ -2672,11 +3082,11 @@ class HfApi:
2672
3082
  commit_description (`str` *optional*):
2673
3083
  The description of the generated commit
2674
3084
  create_pr (`boolean`, *optional*):
2675
- Whether or not to create a Pull Request with that commit. Defaults to `False`.
2676
- If `revision` is not set, PR is opened against the `"main"` branch. If
2677
- `revision` is set and is a branch, PR is opened against this branch. If
2678
- `revision` is set and is not a branch name (example: a commit oid), an
2679
- `RevisionNotFoundError` is returned by the server.
3085
+ Whether or not to create a Pull Request with that commit. Defaults to `False`. If `revision` is not
3086
+ set, PR is opened against the `"main"` branch. If `revision` is set and is a branch, PR is opened
3087
+ against this branch. If `revision` is set and is not a branch name (example: a commit oid), an
3088
+ `RevisionNotFoundError` is returned by the server. If both `multi_commits` and `create_pr` are True,
3089
+ the PR created in the multi-commit process is kept opened.
2680
3090
  parent_commit (`str`, *optional*):
2681
3091
  The OID / SHA of the parent commit, as a hexadecimal string. Shorthands (7 first characters) are also supported.
2682
3092
  If specified and `create_pr` is `False`, the commit will fail if `revision` does not point to `parent_commit`.
@@ -2691,6 +3101,10 @@ class HfApi:
2691
3101
  If provided, remote files matching any of the patterns will be deleted from the repo while committing
2692
3102
  new files. This is useful if you don't know which files have already been uploaded.
2693
3103
  Note: to avoid discrepancies the `.gitattributes` file is not deleted even if it matches the pattern.
3104
+ multi_commits (`bool`):
3105
+ If True, changes are pushed to a PR using a multi-commit process. Defaults to `False`.
3106
+ multi_commits_verbose (`bool`):
3107
+ If True and `multi_commits` is used, more information will be displayed to the user.
2694
3108
 
2695
3109
  Returns:
2696
3110
  `str`: A URL to visualize the uploaded folder on the hub
@@ -2714,6 +3128,12 @@ class HfApi:
2714
3128
 
2715
3129
  </Tip>
2716
3130
 
3131
+ <Tip warning={true}>
3132
+
3133
+ `multi_commits` is experimental. Its API and behavior is subject to change in the future without prior notice.
3134
+
3135
+ </Tip>
3136
+
2717
3137
  Example:
2718
3138
 
2719
3139
  ```python
@@ -2749,20 +3169,27 @@ class HfApi:
2749
3169
  ... token="my_token",
2750
3170
  ... create_pr=True,
2751
3171
  ... )
2752
- # "https://huggingface.co/datasets/username/my-dataset/tree/refs%2Fpr%2F1/remote/experiment/checkpoints"
3172
+ "https://huggingface.co/datasets/username/my-dataset/tree/refs%2Fpr%2F1/remote/experiment/checkpoints"
2753
3173
 
2754
3174
  ```
2755
3175
  """
2756
3176
  if repo_type not in REPO_TYPES:
2757
3177
  raise ValueError(f"Invalid repo type, must be one of {REPO_TYPES}")
2758
3178
 
3179
+ if multi_commits:
3180
+ if revision is not None and revision != DEFAULT_REVISION:
3181
+ raise ValueError("Cannot use `multi_commit` to commit changes other than the main branch.")
3182
+
2759
3183
  # By default, upload folder to the root directory in repo.
2760
3184
  if path_in_repo is None:
2761
3185
  path_in_repo = ""
2762
3186
 
2763
- commit_message = (
2764
- commit_message if commit_message is not None else f"Upload {path_in_repo} with huggingface_hub"
2765
- )
3187
+ # Do not upload .git folder
3188
+ if ignore_patterns is None:
3189
+ ignore_patterns = []
3190
+ elif isinstance(ignore_patterns, str):
3191
+ ignore_patterns = [ignore_patterns]
3192
+ ignore_patterns += IGNORE_GIT_FOLDER_PATTERNS
2766
3193
 
2767
3194
  delete_operations = self._prepare_upload_folder_deletions(
2768
3195
  repo_id=repo_id,
@@ -2787,20 +3214,37 @@ class HfApi:
2787
3214
  ]
2788
3215
  commit_operations = delete_operations + add_operations
2789
3216
 
2790
- commit_info = self.create_commit(
2791
- repo_type=repo_type,
2792
- repo_id=repo_id,
2793
- operations=commit_operations,
2794
- commit_message=commit_message,
2795
- commit_description=commit_description,
2796
- token=token,
2797
- revision=revision,
2798
- create_pr=create_pr,
2799
- parent_commit=parent_commit,
2800
- )
3217
+ pr_url: Optional[str]
3218
+ commit_message = commit_message or "Upload folder using huggingface_hub"
3219
+ if multi_commits:
3220
+ addition_commits, deletion_commits = plan_multi_commits(operations=commit_operations)
3221
+ pr_url = self.create_commits_on_pr(
3222
+ repo_id=repo_id,
3223
+ repo_type=repo_type,
3224
+ addition_commits=addition_commits,
3225
+ deletion_commits=deletion_commits,
3226
+ commit_message=commit_message,
3227
+ commit_description=commit_description,
3228
+ token=token,
3229
+ merge_pr=not create_pr,
3230
+ verbose=multi_commits_verbose,
3231
+ )
3232
+ else:
3233
+ commit_info = self.create_commit(
3234
+ repo_type=repo_type,
3235
+ repo_id=repo_id,
3236
+ operations=commit_operations,
3237
+ commit_message=commit_message,
3238
+ commit_description=commit_description,
3239
+ token=token,
3240
+ revision=revision,
3241
+ create_pr=create_pr,
3242
+ parent_commit=parent_commit,
3243
+ )
3244
+ pr_url = commit_info.pr_url
2801
3245
 
2802
- if commit_info.pr_url is not None:
2803
- revision = quote(_parse_revision_from_pr_url(commit_info.pr_url), safe="")
3246
+ if create_pr and pr_url is not None:
3247
+ revision = quote(_parse_revision_from_pr_url(pr_url), safe="")
2804
3248
  if repo_type in REPO_TYPES_URL_PREFIXES:
2805
3249
  repo_id = REPO_TYPES_URL_PREFIXES[repo_type] + repo_id
2806
3250
  revision = revision if revision is not None else DEFAULT_REVISION
@@ -3020,7 +3464,7 @@ class HfApi:
3020
3464
  payload["startingPoint"] = revision
3021
3465
 
3022
3466
  # Create branch
3023
- response = requests.post(url=branch_url, headers=headers, json=payload)
3467
+ response = get_session().post(url=branch_url, headers=headers, json=payload)
3024
3468
  try:
3025
3469
  hf_raise_for_status(response)
3026
3470
  except HfHubHTTPError as e:
@@ -3073,7 +3517,7 @@ class HfApi:
3073
3517
  headers = self._build_hf_headers(token=token, is_write_action=True)
3074
3518
 
3075
3519
  # Delete branch
3076
- response = requests.delete(url=branch_url, headers=headers)
3520
+ response = get_session().delete(url=branch_url, headers=headers)
3077
3521
  hf_raise_for_status(response)
3078
3522
 
3079
3523
  @validate_hf_hub_args
@@ -3140,7 +3584,7 @@ class HfApi:
3140
3584
  payload["message"] = tag_message
3141
3585
 
3142
3586
  # Tag
3143
- response = requests.post(url=tag_url, headers=headers, json=payload)
3587
+ response = get_session().post(url=tag_url, headers=headers, json=payload)
3144
3588
  try:
3145
3589
  hf_raise_for_status(response)
3146
3590
  except HfHubHTTPError as e:
@@ -3190,7 +3634,7 @@ class HfApi:
3190
3634
  headers = self._build_hf_headers(token=token, is_write_action=True)
3191
3635
 
3192
3636
  # Un-tag
3193
- response = requests.delete(url=tag_url, headers=headers)
3637
+ response = get_session().delete(url=tag_url, headers=headers)
3194
3638
  hf_raise_for_status(response)
3195
3639
 
3196
3640
  @validate_hf_hub_args
@@ -3281,7 +3725,7 @@ class HfApi:
3281
3725
 
3282
3726
  def _fetch_discussion_page(page_index: int):
3283
3727
  path = f"{self.endpoint}/api/{repo_type}s/{repo_id}/discussions?p={page_index}"
3284
- resp = requests.get(path, headers=headers)
3728
+ resp = get_session().get(path, headers=headers)
3285
3729
  hf_raise_for_status(resp)
3286
3730
  paginated_discussions = resp.json()
3287
3731
  total = paginated_discussions["count"]
@@ -3304,6 +3748,7 @@ class HfApi:
3304
3748
  repo_id=discussion["repo"]["name"],
3305
3749
  repo_type=discussion["repo"]["type"],
3306
3750
  is_pull_request=discussion["isPullRequest"],
3751
+ endpoint=self.endpoint,
3307
3752
  )
3308
3753
  page_index = page_index + 1
3309
3754
 
@@ -3356,7 +3801,7 @@ class HfApi:
3356
3801
 
3357
3802
  path = f"{self.endpoint}/api/{repo_type}s/{repo_id}/discussions/{discussion_num}"
3358
3803
  headers = self._build_hf_headers(token=token)
3359
- resp = requests.get(path, params={"diff": "1"}, headers=headers)
3804
+ resp = get_session().get(path, params={"diff": "1"}, headers=headers)
3360
3805
  hf_raise_for_status(resp)
3361
3806
 
3362
3807
  discussion_details = resp.json()
@@ -3380,6 +3825,7 @@ class HfApi:
3380
3825
  target_branch=target_branch,
3381
3826
  merge_commit_oid=merge_commit_oid,
3382
3827
  diff=discussion_details.get("diff"),
3828
+ endpoint=self.endpoint,
3383
3829
  )
3384
3830
 
3385
3831
  @validate_hf_hub_args
@@ -3453,7 +3899,7 @@ class HfApi:
3453
3899
  )
3454
3900
 
3455
3901
  headers = self._build_hf_headers(token=token, is_write_action=True)
3456
- resp = requests.post(
3902
+ resp = get_session().post(
3457
3903
  f"{self.endpoint}/api/{repo_type}s/{repo_id}/discussions",
3458
3904
  json={
3459
3905
  "title": title.strip(),
@@ -3961,7 +4407,7 @@ class HfApi:
3961
4407
  token (`str`, *optional*):
3962
4408
  Hugging Face token. Will default to the locally saved token if not provided.
3963
4409
  """
3964
- r = requests.post(
4410
+ r = get_session().post(
3965
4411
  f"{self.endpoint}/api/spaces/{repo_id}/secrets",
3966
4412
  headers=self._build_hf_headers(token=token),
3967
4413
  json={"key": key, "value": value},
@@ -3983,7 +4429,7 @@ class HfApi:
3983
4429
  token (`str`, *optional*):
3984
4430
  Hugging Face token. Will default to the locally saved token if not provided.
3985
4431
  """
3986
- r = requests.delete(
4432
+ r = get_session().delete(
3987
4433
  f"{self.endpoint}/api/spaces/{repo_id}/secrets",
3988
4434
  headers=self._build_hf_headers(token=token),
3989
4435
  json={"key": key},
@@ -4003,12 +4449,21 @@ class HfApi:
4003
4449
  Returns:
4004
4450
  [`SpaceRuntime`]: Runtime information about a Space including Space stage and hardware.
4005
4451
  """
4006
- r = requests.get(f"{self.endpoint}/api/spaces/{repo_id}/runtime", headers=self._build_hf_headers(token=token))
4452
+ r = get_session().get(
4453
+ f"{self.endpoint}/api/spaces/{repo_id}/runtime", headers=self._build_hf_headers(token=token)
4454
+ )
4007
4455
  hf_raise_for_status(r)
4008
4456
  return SpaceRuntime(r.json())
4009
4457
 
4010
4458
  @validate_hf_hub_args
4011
- def request_space_hardware(self, repo_id: str, hardware: SpaceHardware, *, token: Optional[str] = None) -> None:
4459
+ def request_space_hardware(
4460
+ self,
4461
+ repo_id: str,
4462
+ hardware: SpaceHardware,
4463
+ *,
4464
+ token: Optional[str] = None,
4465
+ sleep_time: Optional[int] = None,
4466
+ ) -> SpaceRuntime:
4012
4467
  """Request new hardware for a Space.
4013
4468
 
4014
4469
  Args:
@@ -4018,6 +4473,13 @@ class HfApi:
4018
4473
  Hardware on which to run the Space. Example: `"t4-medium"`.
4019
4474
  token (`str`, *optional*):
4020
4475
  Hugging Face token. Will default to the locally saved token if not provided.
4476
+ sleep_time (`int`, *optional*):
4477
+ Number of seconds of inactivity to wait before a Space is put to sleep. Set to `-1` if you don't want
4478
+ your Space to sleep (default behavior for upgraded hardware). For free hardware, you can't configure
4479
+ the sleep time (value is fixed to 48 hours of inactivity).
4480
+ See https://huggingface.co/docs/hub/spaces-gpus#sleep-time for more details.
4481
+ Returns:
4482
+ [`SpaceRuntime`]: Runtime information about a Space including Space stage and hardware.
4021
4483
 
4022
4484
  <Tip>
4023
4485
 
@@ -4025,19 +4487,80 @@ class HfApi:
4025
4487
 
4026
4488
  </Tip>
4027
4489
  """
4028
- r = requests.post(
4490
+ if sleep_time is not None and hardware == SpaceHardware.CPU_BASIC:
4491
+ warnings.warn(
4492
+ (
4493
+ "If your Space runs on the default 'cpu-basic' hardware, it will go to sleep if inactive for more"
4494
+ " than 48 hours. This value is not configurable. If you don't want your Space to deactivate or if"
4495
+ " you want to set a custom sleep time, you need to upgrade to a paid Hardware."
4496
+ ),
4497
+ UserWarning,
4498
+ )
4499
+ payload: Dict[str, Any] = {"flavor": hardware}
4500
+ if sleep_time is not None:
4501
+ payload["sleepTimeSeconds"] = sleep_time
4502
+ r = get_session().post(
4029
4503
  f"{self.endpoint}/api/spaces/{repo_id}/hardware",
4030
4504
  headers=self._build_hf_headers(token=token),
4031
- json={"flavor": hardware},
4505
+ json=payload,
4506
+ )
4507
+ hf_raise_for_status(r)
4508
+ return SpaceRuntime(r.json())
4509
+
4510
+ @validate_hf_hub_args
4511
+ def set_space_sleep_time(self, repo_id: str, sleep_time: int, *, token: Optional[str] = None) -> SpaceRuntime:
4512
+ """Set a custom sleep time for a Space running on upgraded hardware..
4513
+
4514
+ Your Space will go to sleep after X seconds of inactivity. You are not billed when your Space is in "sleep"
4515
+ mode. If a new visitor lands on your Space, it will "wake it up". Only upgraded hardware can have a
4516
+ configurable sleep time. To know more about the sleep stage, please refer to
4517
+ https://huggingface.co/docs/hub/spaces-gpus#sleep-time.
4518
+
4519
+ Args:
4520
+ repo_id (`str`):
4521
+ ID of the repo to update. Example: `"bigcode/in-the-stack"`.
4522
+ sleep_time (`int`, *optional*):
4523
+ Number of seconds of inactivity to wait before a Space is put to sleep. Set to `-1` if you don't want
4524
+ your Space to pause (default behavior for upgraded hardware). For free hardware, you can't configure
4525
+ the sleep time (value is fixed to 48 hours of inactivity).
4526
+ See https://huggingface.co/docs/hub/spaces-gpus#sleep-time for more details.
4527
+ token (`str`, *optional*):
4528
+ Hugging Face token. Will default to the locally saved token if not provided.
4529
+ Returns:
4530
+ [`SpaceRuntime`]: Runtime information about a Space including Space stage and hardware.
4531
+
4532
+ <Tip>
4533
+
4534
+ It is also possible to set a custom sleep time when requesting hardware with [`request_space_hardware`].
4535
+
4536
+ </Tip>
4537
+ """
4538
+ r = get_session().post(
4539
+ f"{self.endpoint}/api/spaces/{repo_id}/sleeptime",
4540
+ headers=self._build_hf_headers(token=token),
4541
+ json={"seconds": sleep_time},
4032
4542
  )
4033
4543
  hf_raise_for_status(r)
4544
+ runtime = SpaceRuntime(r.json())
4545
+
4546
+ hardware = runtime.requested_hardware or runtime.hardware
4547
+ if hardware == SpaceHardware.CPU_BASIC:
4548
+ warnings.warn(
4549
+ (
4550
+ "If your Space runs on the default 'cpu-basic' hardware, it will go to sleep if inactive for more"
4551
+ " than 48 hours. This value is not configurable. If you don't want your Space to deactivate or if"
4552
+ " you want to set a custom sleep time, you need to upgrade to a paid Hardware."
4553
+ ),
4554
+ UserWarning,
4555
+ )
4556
+ return runtime
4034
4557
 
4035
4558
  @validate_hf_hub_args
4036
4559
  def pause_space(self, repo_id: str, *, token: Optional[str] = None) -> SpaceRuntime:
4037
4560
  """Pause your Space.
4038
4561
 
4039
4562
  A paused Space stops executing until manually restarted by its owner. This is different from the sleeping
4040
- state in which free Spaces go after 72h of inactivity. Paused time is not billed to your account, no matter the
4563
+ state in which free Spaces go after 48h of inactivity. Paused time is not billed to your account, no matter the
4041
4564
  hardware you've selected. To restart your Space, use [`restart_space`] and go to your Space settings page.
4042
4565
 
4043
4566
  For more details, please visit [the docs](https://huggingface.co/docs/hub/spaces-gpus#pause).
@@ -4062,7 +4585,9 @@ class HfApi:
4062
4585
  If your Space is a static Space. Static Spaces are always running and never billed. If you want to hide
4063
4586
  a static Space, you can set it to private.
4064
4587
  """
4065
- r = requests.post(f"{self.endpoint}/api/spaces/{repo_id}/pause", headers=self._build_hf_headers(token=token))
4588
+ r = get_session().post(
4589
+ f"{self.endpoint}/api/spaces/{repo_id}/pause", headers=self._build_hf_headers(token=token)
4590
+ )
4066
4591
  hf_raise_for_status(r)
4067
4592
  return SpaceRuntime(r.json())
4068
4593
 
@@ -4096,7 +4621,9 @@ class HfApi:
4096
4621
  If your Space is a static Space. Static Spaces are always running and never billed. If you want to hide
4097
4622
  a static Space, you can set it to private.
4098
4623
  """
4099
- r = requests.post(f"{self.endpoint}/api/spaces/{repo_id}/restart", headers=self._build_hf_headers(token=token))
4624
+ r = get_session().post(
4625
+ f"{self.endpoint}/api/spaces/{repo_id}/restart", headers=self._build_hf_headers(token=token)
4626
+ )
4100
4627
  hf_raise_for_status(r)
4101
4628
  return SpaceRuntime(r.json())
4102
4629
 
@@ -4109,7 +4636,7 @@ class HfApi:
4109
4636
  private: Optional[bool] = None,
4110
4637
  token: Optional[str] = None,
4111
4638
  exist_ok: bool = False,
4112
- ) -> str:
4639
+ ) -> RepoUrl:
4113
4640
  """Duplicate a Space.
4114
4641
 
4115
4642
  Programmatically duplicate a Space. The new Space will be created in your account and will be in the same state
@@ -4170,7 +4697,7 @@ class HfApi:
4170
4697
  if private is not None:
4171
4698
  payload["private"] = private
4172
4699
 
4173
- r = requests.post(
4700
+ r = get_session().post(
4174
4701
  f"{self.endpoint}/api/spaces/{from_id}/duplicate",
4175
4702
  headers=self._build_hf_headers(token=token, is_write_action=True),
4176
4703
  json=payload,
@@ -4304,9 +4831,6 @@ def _parse_revision_from_pr_url(pr_url: str) -> str:
4304
4831
 
4305
4832
  api = HfApi()
4306
4833
 
4307
- set_access_token = api.set_access_token
4308
- unset_access_token = api.unset_access_token
4309
-
4310
4834
  whoami = api.whoami
4311
4835
 
4312
4836
  list_models = api.list_models
@@ -4322,6 +4846,7 @@ repo_info = api.repo_info
4322
4846
  list_repo_files = api.list_repo_files
4323
4847
  list_repo_refs = api.list_repo_refs
4324
4848
  list_repo_commits = api.list_repo_commits
4849
+ list_files_info = api.list_files_info
4325
4850
 
4326
4851
  list_metrics = api.list_metrics
4327
4852
 
@@ -4337,6 +4862,7 @@ upload_file = api.upload_file
4337
4862
  upload_folder = api.upload_folder
4338
4863
  delete_file = api.delete_file
4339
4864
  delete_folder = api.delete_folder
4865
+ create_commits_on_pr = api.create_commits_on_pr
4340
4866
  create_branch = api.create_branch
4341
4867
  delete_branch = api.delete_branch
4342
4868
  create_tag = api.create_tag
@@ -4364,6 +4890,7 @@ add_space_secret = api.add_space_secret
4364
4890
  delete_space_secret = api.delete_space_secret
4365
4891
  get_space_runtime = api.get_space_runtime
4366
4892
  request_space_hardware = api.request_space_hardware
4893
+ set_space_sleep_time = api.set_space_sleep_time
4367
4894
  pause_space = api.pause_space
4368
4895
  restart_space = api.restart_space
4369
4896
  duplicate_space = api.duplicate_space