huggingface-hub 0.36.0rc0__py3-none-any.whl → 1.0.0__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 (132) hide show
  1. huggingface_hub/__init__.py +33 -45
  2. huggingface_hub/_commit_api.py +39 -43
  3. huggingface_hub/_commit_scheduler.py +11 -8
  4. huggingface_hub/_inference_endpoints.py +8 -8
  5. huggingface_hub/_jobs_api.py +20 -20
  6. huggingface_hub/_login.py +17 -43
  7. huggingface_hub/_oauth.py +8 -8
  8. huggingface_hub/_snapshot_download.py +135 -50
  9. huggingface_hub/_space_api.py +4 -4
  10. huggingface_hub/_tensorboard_logger.py +5 -5
  11. huggingface_hub/_upload_large_folder.py +18 -32
  12. huggingface_hub/_webhooks_payload.py +3 -3
  13. huggingface_hub/_webhooks_server.py +2 -2
  14. huggingface_hub/cli/__init__.py +0 -14
  15. huggingface_hub/cli/_cli_utils.py +143 -39
  16. huggingface_hub/cli/auth.py +105 -171
  17. huggingface_hub/cli/cache.py +594 -361
  18. huggingface_hub/cli/download.py +120 -112
  19. huggingface_hub/cli/hf.py +38 -41
  20. huggingface_hub/cli/jobs.py +689 -1017
  21. huggingface_hub/cli/lfs.py +120 -143
  22. huggingface_hub/cli/repo.py +282 -216
  23. huggingface_hub/cli/repo_files.py +50 -84
  24. huggingface_hub/cli/system.py +6 -25
  25. huggingface_hub/cli/upload.py +198 -220
  26. huggingface_hub/cli/upload_large_folder.py +91 -106
  27. huggingface_hub/community.py +5 -5
  28. huggingface_hub/constants.py +17 -52
  29. huggingface_hub/dataclasses.py +135 -21
  30. huggingface_hub/errors.py +47 -30
  31. huggingface_hub/fastai_utils.py +8 -9
  32. huggingface_hub/file_download.py +351 -303
  33. huggingface_hub/hf_api.py +398 -570
  34. huggingface_hub/hf_file_system.py +101 -66
  35. huggingface_hub/hub_mixin.py +32 -54
  36. huggingface_hub/inference/_client.py +177 -162
  37. huggingface_hub/inference/_common.py +38 -54
  38. huggingface_hub/inference/_generated/_async_client.py +218 -258
  39. huggingface_hub/inference/_generated/types/automatic_speech_recognition.py +3 -3
  40. huggingface_hub/inference/_generated/types/base.py +10 -7
  41. huggingface_hub/inference/_generated/types/chat_completion.py +16 -16
  42. huggingface_hub/inference/_generated/types/depth_estimation.py +2 -2
  43. huggingface_hub/inference/_generated/types/document_question_answering.py +2 -2
  44. huggingface_hub/inference/_generated/types/feature_extraction.py +2 -2
  45. huggingface_hub/inference/_generated/types/fill_mask.py +2 -2
  46. huggingface_hub/inference/_generated/types/sentence_similarity.py +3 -3
  47. huggingface_hub/inference/_generated/types/summarization.py +2 -2
  48. huggingface_hub/inference/_generated/types/table_question_answering.py +4 -4
  49. huggingface_hub/inference/_generated/types/text2text_generation.py +2 -2
  50. huggingface_hub/inference/_generated/types/text_generation.py +10 -10
  51. huggingface_hub/inference/_generated/types/text_to_video.py +2 -2
  52. huggingface_hub/inference/_generated/types/token_classification.py +2 -2
  53. huggingface_hub/inference/_generated/types/translation.py +2 -2
  54. huggingface_hub/inference/_generated/types/zero_shot_classification.py +2 -2
  55. huggingface_hub/inference/_generated/types/zero_shot_image_classification.py +2 -2
  56. huggingface_hub/inference/_generated/types/zero_shot_object_detection.py +1 -3
  57. huggingface_hub/inference/_mcp/agent.py +3 -3
  58. huggingface_hub/inference/_mcp/constants.py +1 -2
  59. huggingface_hub/inference/_mcp/mcp_client.py +33 -22
  60. huggingface_hub/inference/_mcp/types.py +10 -10
  61. huggingface_hub/inference/_mcp/utils.py +4 -4
  62. huggingface_hub/inference/_providers/__init__.py +12 -4
  63. huggingface_hub/inference/_providers/_common.py +62 -24
  64. huggingface_hub/inference/_providers/black_forest_labs.py +6 -6
  65. huggingface_hub/inference/_providers/cohere.py +3 -3
  66. huggingface_hub/inference/_providers/fal_ai.py +25 -25
  67. huggingface_hub/inference/_providers/featherless_ai.py +4 -4
  68. huggingface_hub/inference/_providers/fireworks_ai.py +3 -3
  69. huggingface_hub/inference/_providers/hf_inference.py +13 -13
  70. huggingface_hub/inference/_providers/hyperbolic.py +4 -4
  71. huggingface_hub/inference/_providers/nebius.py +10 -10
  72. huggingface_hub/inference/_providers/novita.py +5 -5
  73. huggingface_hub/inference/_providers/nscale.py +4 -4
  74. huggingface_hub/inference/_providers/replicate.py +15 -15
  75. huggingface_hub/inference/_providers/sambanova.py +6 -6
  76. huggingface_hub/inference/_providers/together.py +7 -7
  77. huggingface_hub/lfs.py +21 -94
  78. huggingface_hub/repocard.py +15 -16
  79. huggingface_hub/repocard_data.py +57 -57
  80. huggingface_hub/serialization/__init__.py +0 -1
  81. huggingface_hub/serialization/_base.py +9 -9
  82. huggingface_hub/serialization/_dduf.py +7 -7
  83. huggingface_hub/serialization/_torch.py +28 -28
  84. huggingface_hub/utils/__init__.py +11 -6
  85. huggingface_hub/utils/_auth.py +5 -5
  86. huggingface_hub/utils/_cache_manager.py +49 -74
  87. huggingface_hub/utils/_deprecation.py +1 -1
  88. huggingface_hub/utils/_dotenv.py +3 -3
  89. huggingface_hub/utils/_fixes.py +0 -10
  90. huggingface_hub/utils/_git_credential.py +3 -3
  91. huggingface_hub/utils/_headers.py +7 -29
  92. huggingface_hub/utils/_http.py +371 -208
  93. huggingface_hub/utils/_pagination.py +4 -4
  94. huggingface_hub/utils/_parsing.py +98 -0
  95. huggingface_hub/utils/_paths.py +5 -5
  96. huggingface_hub/utils/_runtime.py +59 -23
  97. huggingface_hub/utils/_safetensors.py +21 -21
  98. huggingface_hub/utils/_subprocess.py +9 -9
  99. huggingface_hub/utils/_telemetry.py +3 -3
  100. huggingface_hub/{commands/_cli_utils.py → utils/_terminal.py} +4 -9
  101. huggingface_hub/utils/_typing.py +3 -3
  102. huggingface_hub/utils/_validators.py +53 -72
  103. huggingface_hub/utils/_xet.py +16 -16
  104. huggingface_hub/utils/_xet_progress_reporting.py +1 -1
  105. huggingface_hub/utils/insecure_hashlib.py +3 -9
  106. huggingface_hub/utils/tqdm.py +3 -3
  107. {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/METADATA +16 -35
  108. huggingface_hub-1.0.0.dist-info/RECORD +152 -0
  109. {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/entry_points.txt +0 -1
  110. huggingface_hub/commands/__init__.py +0 -27
  111. huggingface_hub/commands/delete_cache.py +0 -476
  112. huggingface_hub/commands/download.py +0 -204
  113. huggingface_hub/commands/env.py +0 -39
  114. huggingface_hub/commands/huggingface_cli.py +0 -65
  115. huggingface_hub/commands/lfs.py +0 -200
  116. huggingface_hub/commands/repo.py +0 -151
  117. huggingface_hub/commands/repo_files.py +0 -132
  118. huggingface_hub/commands/scan_cache.py +0 -183
  119. huggingface_hub/commands/tag.py +0 -161
  120. huggingface_hub/commands/upload.py +0 -318
  121. huggingface_hub/commands/upload_large_folder.py +0 -131
  122. huggingface_hub/commands/user.py +0 -208
  123. huggingface_hub/commands/version.py +0 -40
  124. huggingface_hub/inference_api.py +0 -217
  125. huggingface_hub/keras_mixin.py +0 -497
  126. huggingface_hub/repository.py +0 -1471
  127. huggingface_hub/serialization/_tensorflow.py +0 -92
  128. huggingface_hub/utils/_hf_folder.py +0 -68
  129. huggingface_hub-0.36.0rc0.dist-info/RECORD +0 -170
  130. {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/LICENSE +0 -0
  131. {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/WHEEL +0 -0
  132. {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/top_level.txt +0 -0
@@ -12,22 +12,21 @@
12
12
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
- """Contains utilities to handle HTTP requests in Huggingface Hub."""
15
+ """Contains utilities to handle HTTP requests in huggingface_hub."""
16
16
 
17
+ import atexit
17
18
  import io
18
- import os
19
+ import json
19
20
  import re
20
21
  import threading
21
22
  import time
22
23
  import uuid
23
- from functools import lru_cache
24
+ from contextlib import contextmanager
25
+ from http import HTTPStatus
24
26
  from shlex import quote
25
- from typing import Any, Callable, List, Optional, Tuple, Type, Union
27
+ from typing import Any, Callable, Generator, Optional, Union
26
28
 
27
- import requests
28
- from requests import HTTPError, Response
29
- from requests.adapters import HTTPAdapter
30
- from requests.models import PreparedRequest
29
+ import httpx
31
30
 
32
31
  from huggingface_hub.errors import OfflineModeIsEnabled
33
32
 
@@ -35,14 +34,13 @@ from .. import constants
35
34
  from ..errors import (
36
35
  BadRequestError,
37
36
  DisabledRepoError,
38
- EntryNotFoundError,
39
37
  GatedRepoError,
40
38
  HfHubHTTPError,
39
+ RemoteEntryNotFoundError,
41
40
  RepositoryNotFoundError,
42
41
  RevisionNotFoundError,
43
42
  )
44
43
  from . import logging
45
- from ._fixes import JSONDecodeError
46
44
  from ._lfs import SliceFileObj
47
45
  from ._typing import HTTP_METHOD_T
48
46
 
@@ -71,142 +69,266 @@ REPO_API_REGEX = re.compile(
71
69
  )
72
70
 
73
71
 
74
- class UniqueRequestIdAdapter(HTTPAdapter):
75
- X_AMZN_TRACE_ID = "X-Amzn-Trace-Id"
72
+ def hf_request_event_hook(request: httpx.Request) -> None:
73
+ """
74
+ Event hook that will be used to make HTTP requests to the Hugging Face Hub.
76
75
 
77
- def add_headers(self, request, **kwargs):
78
- super().add_headers(request, **kwargs)
76
+ What it does:
77
+ - Block requests if offline mode is enabled
78
+ - Add a request ID to the request headers
79
+ - Log the request if debug mode is enabled
80
+ """
81
+ if constants.HF_HUB_OFFLINE:
82
+ raise OfflineModeIsEnabled(
83
+ f"Cannot reach {request.url}: offline mode is enabled. To disable it, please unset the `HF_HUB_OFFLINE` environment variable."
84
+ )
79
85
 
80
- # Add random request ID => easier for server-side debug
81
- if X_AMZN_TRACE_ID not in request.headers:
82
- request.headers[X_AMZN_TRACE_ID] = request.headers.get(X_REQUEST_ID) or str(uuid.uuid4())
86
+ # Add random request ID => easier for server-side debugging
87
+ if X_AMZN_TRACE_ID not in request.headers:
88
+ request.headers[X_AMZN_TRACE_ID] = request.headers.get(X_REQUEST_ID) or str(uuid.uuid4())
89
+ request_id = request.headers.get(X_AMZN_TRACE_ID)
83
90
 
84
- # Add debug log
85
- has_token = len(str(request.headers.get("authorization", ""))) > 0
86
- logger.debug(
87
- f"Request {request.headers[X_AMZN_TRACE_ID]}: {request.method} {request.url} (authenticated: {has_token})"
88
- )
91
+ # Debug log
92
+ logger.debug(
93
+ "Request %s: %s %s (authenticated: %s)",
94
+ request_id,
95
+ request.method,
96
+ request.url,
97
+ request.headers.get("authorization") is not None,
98
+ )
99
+ if constants.HF_DEBUG:
100
+ logger.debug("Send: %s", _curlify(request))
89
101
 
90
- def send(self, request: PreparedRequest, *args, **kwargs) -> Response:
91
- """Catch any RequestException to append request id to the error message for debugging."""
92
- if constants.HF_DEBUG:
93
- logger.debug(f"Send: {_curlify(request)}")
94
- try:
95
- return super().send(request, *args, **kwargs)
96
- except requests.RequestException as e:
97
- request_id = request.headers.get(X_AMZN_TRACE_ID)
98
- if request_id is not None:
99
- # Taken from https://stackoverflow.com/a/58270258
100
- e.args = (*e.args, f"(Request ID: {request_id})")
101
- raise
102
+ return request_id
102
103
 
103
104
 
104
- class OfflineAdapter(HTTPAdapter):
105
- def send(self, request: PreparedRequest, *args, **kwargs) -> Response:
106
- raise OfflineModeIsEnabled(
107
- f"Cannot reach {request.url}: offline mode is enabled. To disable it, please unset the `HF_HUB_OFFLINE` environment variable."
108
- )
105
+ async def async_hf_request_event_hook(request: httpx.Request) -> None:
106
+ """
107
+ Async version of `hf_request_event_hook`.
108
+ """
109
+ return hf_request_event_hook(request)
109
110
 
110
111
 
111
- def _default_backend_factory() -> requests.Session:
112
- session = requests.Session()
113
- if constants.HF_HUB_OFFLINE:
114
- session.mount("http://", OfflineAdapter())
115
- session.mount("https://", OfflineAdapter())
116
- else:
117
- session.mount("http://", UniqueRequestIdAdapter())
118
- session.mount("https://", UniqueRequestIdAdapter())
119
- return session
112
+ async def async_hf_response_event_hook(response: httpx.Response) -> None:
113
+ if response.status_code >= 400:
114
+ # If response will raise, read content from stream to have it available when raising the exception
115
+ # If content-length is not set or is too large, skip reading the content to avoid OOM
116
+ if "Content-length" in response.headers:
117
+ try:
118
+ length = int(response.headers["Content-length"])
119
+ except ValueError:
120
+ return
121
+
122
+ if length < 1_000_000:
123
+ await response.aread()
120
124
 
121
125
 
122
- BACKEND_FACTORY_T = Callable[[], requests.Session]
123
- _GLOBAL_BACKEND_FACTORY: BACKEND_FACTORY_T = _default_backend_factory
126
+ def default_client_factory() -> httpx.Client:
127
+ """
128
+ Factory function to create a `httpx.Client` with the default transport.
129
+ """
130
+ return httpx.Client(
131
+ event_hooks={"request": [hf_request_event_hook]},
132
+ follow_redirects=True,
133
+ timeout=httpx.Timeout(constants.DEFAULT_REQUEST_TIMEOUT, write=60.0),
134
+ )
124
135
 
125
136
 
126
- def configure_http_backend(backend_factory: BACKEND_FACTORY_T = _default_backend_factory) -> None:
137
+ def default_async_client_factory() -> httpx.AsyncClient:
138
+ """
139
+ Factory function to create a `httpx.AsyncClient` with the default transport.
127
140
  """
128
- Configure the HTTP backend by providing a `backend_factory`. Any HTTP calls made by `huggingface_hub` will use a
129
- Session object instantiated by this factory. This can be useful if you are running your scripts in a specific
130
- environment requiring custom configuration (e.g. custom proxy or certifications).
141
+ return httpx.AsyncClient(
142
+ event_hooks={"request": [async_hf_request_event_hook], "response": [async_hf_response_event_hook]},
143
+ follow_redirects=True,
144
+ timeout=httpx.Timeout(constants.DEFAULT_REQUEST_TIMEOUT, write=60.0),
145
+ )
131
146
 
132
- Use [`get_session`] to get a configured Session. Since `requests.Session` is not guaranteed to be thread-safe,
133
- `huggingface_hub` creates 1 Session instance per thread. They are all instantiated using the same `backend_factory`
134
- set in [`configure_http_backend`]. A LRU cache is used to cache the created sessions (and connections) between
135
- calls. Max size is 128 to avoid memory leaks if thousands of threads are spawned.
136
147
 
137
- See [this issue](https://github.com/psf/requests/issues/2766) to know more about thread-safety in `requests`.
148
+ CLIENT_FACTORY_T = Callable[[], httpx.Client]
149
+ ASYNC_CLIENT_FACTORY_T = Callable[[], httpx.AsyncClient]
138
150
 
139
- Example:
140
- ```py
141
- import requests
142
- from huggingface_hub import configure_http_backend, get_session
151
+ _CLIENT_LOCK = threading.Lock()
152
+ _GLOBAL_CLIENT_FACTORY: CLIENT_FACTORY_T = default_client_factory
153
+ _GLOBAL_ASYNC_CLIENT_FACTORY: ASYNC_CLIENT_FACTORY_T = default_async_client_factory
154
+ _GLOBAL_CLIENT: Optional[httpx.Client] = None
143
155
 
144
- # Create a factory function that returns a Session with configured proxies
145
- def backend_factory() -> requests.Session:
146
- session = requests.Session()
147
- session.proxies = {"http": "http://10.10.1.10:3128", "https": "https://10.10.1.11:1080"}
148
- return session
149
156
 
150
- # Set it as the default session factory
151
- configure_http_backend(backend_factory=backend_factory)
157
+ def set_client_factory(client_factory: CLIENT_FACTORY_T) -> None:
158
+ """
159
+ Set the HTTP client factory to be used by `huggingface_hub`.
152
160
 
153
- # In practice, this is mostly done internally in `huggingface_hub`
154
- session = get_session()
155
- ```
161
+ The client factory is a method that returns a `httpx.Client` object. On the first call to [`get_client`] the client factory
162
+ will be used to create a new `httpx.Client` object that will be shared between all calls made by `huggingface_hub`.
163
+
164
+ This can be useful if you are running your scripts in a specific environment requiring custom configuration (e.g. custom proxy or certifications).
165
+
166
+ Use [`get_client`] to get a correctly configured `httpx.Client`.
156
167
  """
157
- global _GLOBAL_BACKEND_FACTORY
158
- _GLOBAL_BACKEND_FACTORY = backend_factory
159
- reset_sessions()
168
+ global _GLOBAL_CLIENT_FACTORY
169
+ with _CLIENT_LOCK:
170
+ close_session()
171
+ _GLOBAL_CLIENT_FACTORY = client_factory
160
172
 
161
173
 
162
- def get_session() -> requests.Session:
174
+ def set_async_client_factory(async_client_factory: ASYNC_CLIENT_FACTORY_T) -> None:
163
175
  """
164
- Get a `requests.Session` object, using the session factory from the user.
176
+ Set the HTTP async client factory to be used by `huggingface_hub`.
165
177
 
166
- Use [`get_session`] to get a configured Session. Since `requests.Session` is not guaranteed to be thread-safe,
167
- `huggingface_hub` creates 1 Session instance per thread. They are all instantiated using the same `backend_factory`
168
- set in [`configure_http_backend`]. A LRU cache is used to cache the created sessions (and connections) between
169
- calls. Max size is 128 to avoid memory leaks if thousands of threads are spawned.
178
+ The async client factory is a method that returns a `httpx.AsyncClient` object.
179
+ This can be useful if you are running your scripts in a specific environment requiring custom configuration (e.g. custom proxy or certifications).
180
+ Use [`get_async_client`] to get a correctly configured `httpx.AsyncClient`.
170
181
 
171
- See [this issue](https://github.com/psf/requests/issues/2766) to know more about thread-safety in `requests`.
182
+ <Tip warning={true}>
172
183
 
173
- Example:
174
- ```py
175
- import requests
176
- from huggingface_hub import configure_http_backend, get_session
184
+ Contrary to the `httpx.Client` that is shared between all calls made by `huggingface_hub`, the `httpx.AsyncClient` is not shared.
185
+ It is recommended to use an async context manager to ensure the client is properly closed when the context is exited.
186
+
187
+ </Tip>
188
+ """
189
+ global _GLOBAL_ASYNC_CLIENT_FACTORY
190
+ _GLOBAL_ASYNC_CLIENT_FACTORY = async_client_factory
177
191
 
178
- # Create a factory function that returns a Session with configured proxies
179
- def backend_factory() -> requests.Session:
180
- session = requests.Session()
181
- session.proxies = {"http": "http://10.10.1.10:3128", "https": "https://10.10.1.11:1080"}
182
- return session
183
192
 
184
- # Set it as the default session factory
185
- configure_http_backend(backend_factory=backend_factory)
193
+ def get_session() -> httpx.Client:
194
+ """
195
+ Get a `httpx.Client` object, using the transport factory from the user.
186
196
 
187
- # In practice, this is mostly done internally in `huggingface_hub`
188
- session = get_session()
189
- ```
197
+ This client is shared between all calls made by `huggingface_hub`. Therefore you should not close it manually.
198
+
199
+ Use [`set_client_factory`] to customize the `httpx.Client`.
200
+ """
201
+ global _GLOBAL_CLIENT
202
+ if _GLOBAL_CLIENT is None:
203
+ with _CLIENT_LOCK:
204
+ _GLOBAL_CLIENT = _GLOBAL_CLIENT_FACTORY()
205
+ return _GLOBAL_CLIENT
206
+
207
+
208
+ def get_async_session() -> httpx.AsyncClient:
190
209
  """
191
- return _get_session_from_cache(process_id=os.getpid(), thread_id=threading.get_ident())
210
+ Return a `httpx.AsyncClient` object, using the transport factory from the user.
211
+
212
+ Use [`set_async_client_factory`] to customize the `httpx.AsyncClient`.
192
213
 
214
+ <Tip warning={true}>
193
215
 
194
- def reset_sessions() -> None:
195
- """Reset the cache of sessions.
216
+ Contrary to the `httpx.Client` that is shared between all calls made by `huggingface_hub`, the `httpx.AsyncClient` is not shared.
217
+ It is recommended to use an async context manager to ensure the client is properly closed when the context is exited.
196
218
 
197
- Mostly used internally when sessions are reconfigured or an SSLError is raised.
198
- See [`configure_http_backend`] for more details.
219
+ </Tip>
199
220
  """
200
- _get_session_from_cache.cache_clear()
221
+ return _GLOBAL_ASYNC_CLIENT_FACTORY()
201
222
 
202
223
 
203
- @lru_cache
204
- def _get_session_from_cache(process_id: int, thread_id: int) -> requests.Session:
224
+ def close_session() -> None:
205
225
  """
206
- Create a new session per thread using global factory. Using LRU cache (maxsize 128) to avoid memory leaks when
207
- using thousands of threads. Cache is cleared when `configure_http_backend` is called.
226
+ Close the global `httpx.Client` used by `huggingface_hub`.
227
+
228
+ If a Client is closed, it will be recreated on the next call to [`get_session`].
229
+
230
+ Can be useful if e.g. an SSL certificate has been updated.
208
231
  """
209
- return _GLOBAL_BACKEND_FACTORY()
232
+ global _GLOBAL_CLIENT
233
+ client = _GLOBAL_CLIENT
234
+
235
+ # First, set global client to None
236
+ _GLOBAL_CLIENT = None
237
+
238
+ # Then, close the clients
239
+ if client is not None:
240
+ try:
241
+ client.close()
242
+ except Exception as e:
243
+ logger.warning(f"Error closing client: {e}")
244
+
245
+
246
+ atexit.register(close_session)
247
+
248
+
249
+ def _http_backoff_base(
250
+ method: HTTP_METHOD_T,
251
+ url: str,
252
+ *,
253
+ max_retries: int = 5,
254
+ base_wait_time: float = 1,
255
+ max_wait_time: float = 8,
256
+ retry_on_exceptions: Union[type[Exception], tuple[type[Exception], ...]] = (
257
+ httpx.TimeoutException,
258
+ httpx.NetworkError,
259
+ ),
260
+ retry_on_status_codes: Union[int, tuple[int, ...]] = HTTPStatus.SERVICE_UNAVAILABLE,
261
+ stream: bool = False,
262
+ **kwargs,
263
+ ) -> Generator[httpx.Response, None, None]:
264
+ """Internal implementation of HTTP backoff logic shared between `http_backoff` and `http_stream_backoff`."""
265
+ if isinstance(retry_on_exceptions, type): # Tuple from single exception type
266
+ retry_on_exceptions = (retry_on_exceptions,)
267
+
268
+ if isinstance(retry_on_status_codes, int): # Tuple from single status code
269
+ retry_on_status_codes = (retry_on_status_codes,)
270
+
271
+ nb_tries = 0
272
+ sleep_time = base_wait_time
273
+
274
+ # If `data` is used and is a file object (or any IO), it will be consumed on the
275
+ # first HTTP request. We need to save the initial position so that the full content
276
+ # of the file is re-sent on http backoff. See warning tip in docstring.
277
+ io_obj_initial_pos = None
278
+ if "data" in kwargs and isinstance(kwargs["data"], (io.IOBase, SliceFileObj)):
279
+ io_obj_initial_pos = kwargs["data"].tell()
280
+
281
+ client = get_session()
282
+ while True:
283
+ nb_tries += 1
284
+ try:
285
+ # If `data` is used and is a file object (or any IO), set back cursor to
286
+ # initial position.
287
+ if io_obj_initial_pos is not None:
288
+ kwargs["data"].seek(io_obj_initial_pos)
289
+
290
+ # Perform request and handle response
291
+ def _should_retry(response: httpx.Response) -> bool:
292
+ """Handle response and return True if should retry, False if should return/yield."""
293
+ if response.status_code not in retry_on_status_codes:
294
+ return False # Success, don't retry
295
+
296
+ # Wrong status code returned (HTTP 503 for instance)
297
+ logger.warning(f"HTTP Error {response.status_code} thrown while requesting {method} {url}")
298
+ if nb_tries > max_retries:
299
+ hf_raise_for_status(response) # Will raise uncaught exception
300
+ # Return/yield response to avoid infinite loop in the corner case where the
301
+ # user ask for retry on a status code that doesn't raise_for_status.
302
+ return False # Don't retry, return/yield response
303
+
304
+ return True # Should retry
305
+
306
+ if stream:
307
+ with client.stream(method=method, url=url, **kwargs) as response:
308
+ if not _should_retry(response):
309
+ yield response
310
+ return
311
+ else:
312
+ response = client.request(method=method, url=url, **kwargs)
313
+ if not _should_retry(response):
314
+ yield response
315
+ return
316
+
317
+ except retry_on_exceptions as err:
318
+ logger.warning(f"'{err}' thrown while requesting {method} {url}")
319
+
320
+ if isinstance(err, httpx.ConnectError):
321
+ close_session() # In case of SSLError it's best to close the shared httpx.Client objects
322
+
323
+ if nb_tries > max_retries:
324
+ raise err
325
+
326
+ # Sleep for X seconds
327
+ logger.warning(f"Retrying in {sleep_time}s [Retry {nb_tries}/{max_retries}].")
328
+ time.sleep(sleep_time)
329
+
330
+ # Update sleep time for next retry
331
+ sleep_time = min(max_wait_time, sleep_time * 2) # Exponential backoff
210
332
 
211
333
 
212
334
  def http_backoff(
@@ -216,15 +338,14 @@ def http_backoff(
216
338
  max_retries: int = 5,
217
339
  base_wait_time: float = 1,
218
340
  max_wait_time: float = 8,
219
- retry_on_exceptions: Union[Type[Exception], Tuple[Type[Exception], ...]] = (
220
- requests.Timeout,
221
- requests.ConnectionError,
222
- requests.exceptions.ChunkedEncodingError,
341
+ retry_on_exceptions: Union[type[Exception], tuple[type[Exception], ...]] = (
342
+ httpx.TimeoutException,
343
+ httpx.NetworkError,
223
344
  ),
224
- retry_on_status_codes: Union[int, Tuple[int, ...]] = (500, 502, 503, 504),
345
+ retry_on_status_codes: Union[int, tuple[int, ...]] = HTTPStatus.SERVICE_UNAVAILABLE,
225
346
  **kwargs,
226
- ) -> Response:
227
- """Wrapper around requests to retry calls on an endpoint, with exponential backoff.
347
+ ) -> httpx.Response:
348
+ """Wrapper around httpx to retry calls on an endpoint, with exponential backoff.
228
349
 
229
350
  Endpoint call is retried on exceptions (ex: connection timeout, proxy error,...)
230
351
  and/or on specific status codes (ex: service unavailable). If the call failed more
@@ -247,19 +368,20 @@ def http_backoff(
247
368
  `max_wait_time`.
248
369
  max_wait_time (`float`, *optional*, defaults to `8`):
249
370
  Maximum duration (in seconds) to wait before retrying.
250
- retry_on_exceptions (`Type[Exception]` or `Tuple[Type[Exception]]`, *optional*):
371
+ retry_on_exceptions (`type[Exception]` or `tuple[type[Exception]]`, *optional*):
251
372
  Define which exceptions must be caught to retry the request. Can be a single type or a tuple of types.
252
- By default, retry on `requests.Timeout`, `requests.ConnectionError` and `requests.exceptions.ChunkedEncodingError`.
253
- retry_on_status_codes (`int` or `Tuple[int]`, *optional*, defaults to `(500, 502, 503, 504)`):
254
- Define on which status codes the request must be retried. By default, 5xx errors are retried.
373
+ By default, retry on `httpx.TimeoutException` and `httpx.NetworkError`.
374
+ retry_on_status_codes (`int` or `tuple[int]`, *optional*, defaults to `503`):
375
+ Define on which status codes the request must be retried. By default, only
376
+ HTTP 503 Service Unavailable is retried.
255
377
  **kwargs (`dict`, *optional*):
256
- kwargs to pass to `requests.request`.
378
+ kwargs to pass to `httpx.request`.
257
379
 
258
380
  Example:
259
381
  ```
260
382
  >>> from huggingface_hub.utils import http_backoff
261
383
 
262
- # Same usage as "requests.request".
384
+ # Same usage as "httpx.request".
263
385
  >>> response = http_backoff("GET", "https://www.google.com")
264
386
  >>> response.raise_for_status()
265
387
 
@@ -277,59 +399,105 @@ def http_backoff(
277
399
  > will fail. If this is a hard constraint for you, please let us know by opening an
278
400
  > issue on [Github](https://github.com/huggingface/huggingface_hub).
279
401
  """
280
- if isinstance(retry_on_exceptions, type): # Tuple from single exception type
281
- retry_on_exceptions = (retry_on_exceptions,)
402
+ return next(
403
+ _http_backoff_base(
404
+ method=method,
405
+ url=url,
406
+ max_retries=max_retries,
407
+ base_wait_time=base_wait_time,
408
+ max_wait_time=max_wait_time,
409
+ retry_on_exceptions=retry_on_exceptions,
410
+ retry_on_status_codes=retry_on_status_codes,
411
+ stream=False,
412
+ **kwargs,
413
+ )
414
+ )
282
415
 
283
- if isinstance(retry_on_status_codes, int): # Tuple from single status code
284
- retry_on_status_codes = (retry_on_status_codes,)
285
416
 
286
- nb_tries = 0
287
- sleep_time = base_wait_time
417
+ @contextmanager
418
+ def http_stream_backoff(
419
+ method: HTTP_METHOD_T,
420
+ url: str,
421
+ *,
422
+ max_retries: int = 5,
423
+ base_wait_time: float = 1,
424
+ max_wait_time: float = 8,
425
+ retry_on_exceptions: Union[type[Exception], tuple[type[Exception], ...]] = (
426
+ httpx.TimeoutException,
427
+ httpx.NetworkError,
428
+ ),
429
+ retry_on_status_codes: Union[int, tuple[int, ...]] = HTTPStatus.SERVICE_UNAVAILABLE,
430
+ **kwargs,
431
+ ) -> Generator[httpx.Response, None, None]:
432
+ """Wrapper around httpx to retry calls on an endpoint, with exponential backoff.
288
433
 
289
- # If `data` is used and is a file object (or any IO), it will be consumed on the
290
- # first HTTP request. We need to save the initial position so that the full content
291
- # of the file is re-sent on http backoff. See warning tip in docstring.
292
- io_obj_initial_pos = None
293
- if "data" in kwargs and isinstance(kwargs["data"], (io.IOBase, SliceFileObj)):
294
- io_obj_initial_pos = kwargs["data"].tell()
434
+ Endpoint call is retried on exceptions (ex: connection timeout, proxy error,...)
435
+ and/or on specific status codes (ex: service unavailable). If the call failed more
436
+ than `max_retries`, the exception is thrown or `raise_for_status` is called on the
437
+ response object.
295
438
 
296
- session = get_session()
297
- while True:
298
- nb_tries += 1
299
- try:
300
- # If `data` is used and is a file object (or any IO), set back cursor to
301
- # initial position.
302
- if io_obj_initial_pos is not None:
303
- kwargs["data"].seek(io_obj_initial_pos)
439
+ Re-implement mechanisms from the `backoff` library to avoid adding an external
440
+ dependencies to `hugging_face_hub`. See https://github.com/litl/backoff.
304
441
 
305
- # Perform request and return if status_code is not in the retry list.
306
- response = session.request(method=method, url=url, **kwargs)
307
- if response.status_code not in retry_on_status_codes:
308
- return response
442
+ Args:
443
+ method (`Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"]`):
444
+ HTTP method to perform.
445
+ url (`str`):
446
+ The URL of the resource to fetch.
447
+ max_retries (`int`, *optional*, defaults to `5`):
448
+ Maximum number of retries, defaults to 5 (no retries).
449
+ base_wait_time (`float`, *optional*, defaults to `1`):
450
+ Duration (in seconds) to wait before retrying the first time.
451
+ Wait time between retries then grows exponentially, capped by
452
+ `max_wait_time`.
453
+ max_wait_time (`float`, *optional*, defaults to `8`):
454
+ Maximum duration (in seconds) to wait before retrying.
455
+ retry_on_exceptions (`type[Exception]` or `tuple[type[Exception]]`, *optional*):
456
+ Define which exceptions must be caught to retry the request. Can be a single type or a tuple of types.
457
+ By default, retry on `httpx.Timeout` and `httpx.NetworkError`.
458
+ retry_on_status_codes (`int` or `tuple[int]`, *optional*, defaults to `503`):
459
+ Define on which status codes the request must be retried. By default, only
460
+ HTTP 503 Service Unavailable is retried.
461
+ **kwargs (`dict`, *optional*):
462
+ kwargs to pass to `httpx.request`.
309
463
 
310
- # Wrong status code returned (HTTP 503 for instance)
311
- logger.warning(f"HTTP Error {response.status_code} thrown while requesting {method} {url}")
312
- if nb_tries > max_retries:
313
- response.raise_for_status() # Will raise uncaught exception
314
- # We return response to avoid infinite loop in the corner case where the
315
- # user ask for retry on a status code that doesn't raise_for_status.
316
- return response
464
+ Example:
465
+ ```
466
+ >>> from huggingface_hub.utils import http_stream_backoff
317
467
 
318
- except retry_on_exceptions as err:
319
- logger.warning(f"'{err}' thrown while requesting {method} {url}")
468
+ # Same usage as "httpx.stream".
469
+ >>> with http_stream_backoff("GET", "https://www.google.com") as response:
470
+ ... for chunk in response.iter_bytes():
471
+ ... print(chunk)
320
472
 
321
- if isinstance(err, requests.ConnectionError):
322
- reset_sessions() # In case of SSLError it's best to reset the shared requests.Session objects
473
+ # If you expect a Gateway Timeout from time to time
474
+ >>> with http_stream_backoff("PUT", upload_url, data=data, retry_on_status_codes=504) as response:
475
+ ... response.raise_for_status()
476
+ ```
323
477
 
324
- if nb_tries > max_retries:
325
- raise err
478
+ <Tip warning={true}>
326
479
 
327
- # Sleep for X seconds
328
- logger.warning(f"Retrying in {sleep_time}s [Retry {nb_tries}/{max_retries}].")
329
- time.sleep(sleep_time)
480
+ When using `httpx` it is possible to stream data by passing an iterator to the
481
+ `data` argument. On http backoff this is a problem as the iterator is not reset
482
+ after a failed call. This issue is mitigated for file objects or any IO streams
483
+ by saving the initial position of the cursor (with `data.tell()`) and resetting the
484
+ cursor between each call (with `data.seek()`). For arbitrary iterators, http backoff
485
+ will fail. If this is a hard constraint for you, please let us know by opening an
486
+ issue on [Github](https://github.com/huggingface/huggingface_hub).
330
487
 
331
- # Update sleep time for next retry
332
- sleep_time = min(max_wait_time, sleep_time * 2) # Exponential backoff
488
+ </Tip>
489
+ """
490
+ yield from _http_backoff_base(
491
+ method=method,
492
+ url=url,
493
+ max_retries=max_retries,
494
+ base_wait_time=base_wait_time,
495
+ max_wait_time=max_wait_time,
496
+ retry_on_exceptions=retry_on_exceptions,
497
+ retry_on_status_codes=retry_on_status_codes,
498
+ stream=True,
499
+ **kwargs,
500
+ )
333
501
 
334
502
 
335
503
  def fix_hf_endpoint_in_url(url: str, endpoint: Optional[str]) -> str:
@@ -345,38 +513,18 @@ def fix_hf_endpoint_in_url(url: str, endpoint: Optional[str]) -> str:
345
513
  return url
346
514
 
347
515
 
348
- def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None) -> None:
516
+ def hf_raise_for_status(response: httpx.Response, endpoint_name: Optional[str] = None) -> None:
349
517
  """
350
- Internal version of `response.raise_for_status()` that will refine a
351
- potential HTTPError. Raised exception will be an instance of `HfHubHTTPError`.
352
-
353
- This helper is meant to be the unique method to raise_for_status when making a call
354
- to the Hugging Face Hub.
355
-
356
-
357
- Example:
358
- ```py
359
- import requests
360
- from huggingface_hub.utils import get_session, hf_raise_for_status, HfHubHTTPError
518
+ Internal version of `response.raise_for_status()` that will refine a potential HTTPError.
519
+ Raised exception will be an instance of [`~errors.HfHubHTTPError`].
361
520
 
362
- response = get_session().post(...)
363
- try:
364
- hf_raise_for_status(response)
365
- except HfHubHTTPError as e:
366
- print(str(e)) # formatted message
367
- e.request_id, e.server_message # details returned by server
368
-
369
- # Complete the error message with additional information once it's raised
370
- e.append_to_message("\n`create_commit` expects the repository to exist.")
371
- raise
372
- ```
521
+ This helper is meant to be the unique method to raise_for_status when making a call to the Hugging Face Hub.
373
522
 
374
523
  Args:
375
524
  response (`Response`):
376
525
  Response from the server.
377
526
  endpoint_name (`str`, *optional*):
378
- Name of the endpoint that has been called. If provided, the error message
379
- will be more complete.
527
+ Name of the endpoint that has been called. If provided, the error message will be more complete.
380
528
 
381
529
  > [!WARNING]
382
530
  > Raises when the request has failed:
@@ -400,7 +548,10 @@ def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None)
400
548
  """
401
549
  try:
402
550
  response.raise_for_status()
403
- except HTTPError as e:
551
+ except httpx.HTTPStatusError as e:
552
+ if response.status_code // 100 == 3:
553
+ return # Do not raise on redirects to stay consistent with `requests`
554
+
404
555
  error_code = response.headers.get("X-Error-Code")
405
556
  error_message = response.headers.get("X-Error-Message")
406
557
 
@@ -410,7 +561,7 @@ def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None)
410
561
 
411
562
  elif error_code == "EntryNotFound":
412
563
  message = f"{response.status_code} Client Error." + "\n\n" + f"Entry Not Found for url: {response.url}."
413
- raise _format(EntryNotFoundError, message, response) from e
564
+ raise _format(RemoteEntryNotFoundError, message, response) from e
414
565
 
415
566
  elif error_code == "GatedRepo":
416
567
  message = (
@@ -433,7 +584,7 @@ def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None)
433
584
  and error_message != "Invalid credentials in Authorization header"
434
585
  and response.request is not None
435
586
  and response.request.url is not None
436
- and REPO_API_REGEX.search(response.request.url) is not None
587
+ and REPO_API_REGEX.search(str(response.request.url)) is not None
437
588
  ):
438
589
  # 401 is misleading as it is returned for:
439
590
  # - private and gated repos if user is not authenticated
@@ -475,7 +626,7 @@ def hf_raise_for_status(response: Response, endpoint_name: Optional[str] = None)
475
626
  raise _format(HfHubHTTPError, str(e), response) from e
476
627
 
477
628
 
478
- def _format(error_type: Type[HfHubHTTPError], custom_message: str, response: Response) -> HfHubHTTPError:
629
+ def _format(error_type: type[HfHubHTTPError], custom_message: str, response: httpx.Response) -> HfHubHTTPError:
479
630
  server_errors = []
480
631
 
481
632
  # Retrieve server error from header
@@ -486,7 +637,19 @@ def _format(error_type: Type[HfHubHTTPError], custom_message: str, response: Res
486
637
  # Retrieve server error from body
487
638
  try:
488
639
  # Case errors are returned in a JSON format
489
- data = response.json()
640
+ try:
641
+ data = response.json()
642
+ except httpx.ResponseNotRead:
643
+ try:
644
+ response.read() # In case of streaming response, we need to read the response first
645
+ data = response.json()
646
+ except RuntimeError:
647
+ # In case of async streaming response, we can't read the stream here.
648
+ # In practice if user is using the default async client from `get_async_client`, the stream will have
649
+ # already been read in the async event hook `async_hf_response_event_hook`.
650
+ #
651
+ # Here, we are skipping reading the response to avoid RuntimeError but it happens only if async + stream + used httpx.AsyncClient directly.
652
+ data = {}
490
653
 
491
654
  error = data.get("error")
492
655
  if error is not None:
@@ -504,7 +667,7 @@ def _format(error_type: Type[HfHubHTTPError], custom_message: str, response: Res
504
667
  if "message" in error:
505
668
  server_errors.append(error["message"])
506
669
 
507
- except JSONDecodeError:
670
+ except json.JSONDecodeError:
508
671
  # If content is not JSON and not HTML, append the text
509
672
  content_type = response.headers.get("Content-Type", "")
510
673
  if response.text and "html" not in content_type.lower():
@@ -549,15 +712,15 @@ def _format(error_type: Type[HfHubHTTPError], custom_message: str, response: Res
549
712
  return error_type(final_error_message.strip(), response=response, server_message=server_message or None)
550
713
 
551
714
 
552
- def _curlify(request: requests.PreparedRequest) -> str:
553
- """Convert a `requests.PreparedRequest` into a curl command (str).
715
+ def _curlify(request: httpx.Request) -> str:
716
+ """Convert a `httpx.Request` into a curl command (str).
554
717
 
555
718
  Used for debug purposes only.
556
719
 
557
720
  Implementation vendored from https://github.com/ofw/curlify/blob/master/curlify.py.
558
721
  MIT License Copyright (c) 2016 Egor.
559
722
  """
560
- parts: List[Tuple[Any, Any]] = [
723
+ parts: list[tuple[Any, Any]] = [
561
724
  ("curl", None),
562
725
  ("-X", request.method),
563
726
  ]
@@ -565,16 +728,16 @@ def _curlify(request: requests.PreparedRequest) -> str:
565
728
  for k, v in sorted(request.headers.items()):
566
729
  if k.lower() == "authorization":
567
730
  v = "<TOKEN>" # Hide authorization header, no matter its value (can be Bearer, Key, etc.)
568
- parts += [("-H", "{0}: {1}".format(k, v))]
569
-
570
- if request.body:
571
- body = request.body
572
- if isinstance(body, bytes):
573
- body = body.decode("utf-8", errors="ignore")
574
- elif hasattr(body, "read"):
575
- body = "<file-like object>" # Don't try to read it to avoid consuming the stream
731
+ parts += [("-H", f"{k}: {v}")]
732
+
733
+ body: Optional[str] = None
734
+ if request.content is not None:
735
+ body = request.content.decode("utf-8", errors="ignore")
576
736
  if len(body) > 1000:
577
- body = body[:1000] + " ... [truncated]"
737
+ body = f"{body[:1000]} ... [truncated]"
738
+ elif request.stream is not None:
739
+ body = "<streaming body>"
740
+ if body is not None:
578
741
  parts += [("-d", body.replace("\n", ""))]
579
742
 
580
743
  parts += [(None, request.url)]
@@ -582,9 +745,9 @@ def _curlify(request: requests.PreparedRequest) -> str:
582
745
  flat_parts = []
583
746
  for k, v in parts:
584
747
  if k:
585
- flat_parts.append(quote(k))
748
+ flat_parts.append(quote(str(k)))
586
749
  if v:
587
- flat_parts.append(quote(v))
750
+ flat_parts.append(quote(str(v)))
588
751
 
589
752
  return " ".join(flat_parts)
590
753