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