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