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.
- huggingface_hub/__init__.py +33 -45
- huggingface_hub/_commit_api.py +39 -43
- huggingface_hub/_commit_scheduler.py +11 -8
- huggingface_hub/_inference_endpoints.py +8 -8
- huggingface_hub/_jobs_api.py +20 -20
- huggingface_hub/_login.py +17 -43
- huggingface_hub/_oauth.py +8 -8
- huggingface_hub/_snapshot_download.py +135 -50
- huggingface_hub/_space_api.py +4 -4
- huggingface_hub/_tensorboard_logger.py +5 -5
- huggingface_hub/_upload_large_folder.py +18 -32
- huggingface_hub/_webhooks_payload.py +3 -3
- huggingface_hub/_webhooks_server.py +2 -2
- huggingface_hub/cli/__init__.py +0 -14
- huggingface_hub/cli/_cli_utils.py +143 -39
- huggingface_hub/cli/auth.py +105 -171
- huggingface_hub/cli/cache.py +594 -361
- huggingface_hub/cli/download.py +120 -112
- huggingface_hub/cli/hf.py +38 -41
- huggingface_hub/cli/jobs.py +689 -1017
- huggingface_hub/cli/lfs.py +120 -143
- huggingface_hub/cli/repo.py +282 -216
- huggingface_hub/cli/repo_files.py +50 -84
- huggingface_hub/cli/system.py +6 -25
- huggingface_hub/cli/upload.py +198 -220
- huggingface_hub/cli/upload_large_folder.py +91 -106
- huggingface_hub/community.py +5 -5
- huggingface_hub/constants.py +17 -52
- huggingface_hub/dataclasses.py +135 -21
- huggingface_hub/errors.py +47 -30
- huggingface_hub/fastai_utils.py +8 -9
- huggingface_hub/file_download.py +351 -303
- huggingface_hub/hf_api.py +398 -570
- huggingface_hub/hf_file_system.py +101 -66
- huggingface_hub/hub_mixin.py +32 -54
- huggingface_hub/inference/_client.py +177 -162
- huggingface_hub/inference/_common.py +38 -54
- huggingface_hub/inference/_generated/_async_client.py +218 -258
- huggingface_hub/inference/_generated/types/automatic_speech_recognition.py +3 -3
- huggingface_hub/inference/_generated/types/base.py +10 -7
- huggingface_hub/inference/_generated/types/chat_completion.py +16 -16
- huggingface_hub/inference/_generated/types/depth_estimation.py +2 -2
- huggingface_hub/inference/_generated/types/document_question_answering.py +2 -2
- huggingface_hub/inference/_generated/types/feature_extraction.py +2 -2
- huggingface_hub/inference/_generated/types/fill_mask.py +2 -2
- huggingface_hub/inference/_generated/types/sentence_similarity.py +3 -3
- huggingface_hub/inference/_generated/types/summarization.py +2 -2
- huggingface_hub/inference/_generated/types/table_question_answering.py +4 -4
- huggingface_hub/inference/_generated/types/text2text_generation.py +2 -2
- huggingface_hub/inference/_generated/types/text_generation.py +10 -10
- huggingface_hub/inference/_generated/types/text_to_video.py +2 -2
- huggingface_hub/inference/_generated/types/token_classification.py +2 -2
- huggingface_hub/inference/_generated/types/translation.py +2 -2
- huggingface_hub/inference/_generated/types/zero_shot_classification.py +2 -2
- huggingface_hub/inference/_generated/types/zero_shot_image_classification.py +2 -2
- huggingface_hub/inference/_generated/types/zero_shot_object_detection.py +1 -3
- huggingface_hub/inference/_mcp/agent.py +3 -3
- huggingface_hub/inference/_mcp/constants.py +1 -2
- huggingface_hub/inference/_mcp/mcp_client.py +33 -22
- huggingface_hub/inference/_mcp/types.py +10 -10
- huggingface_hub/inference/_mcp/utils.py +4 -4
- huggingface_hub/inference/_providers/__init__.py +12 -4
- huggingface_hub/inference/_providers/_common.py +62 -24
- huggingface_hub/inference/_providers/black_forest_labs.py +6 -6
- huggingface_hub/inference/_providers/cohere.py +3 -3
- huggingface_hub/inference/_providers/fal_ai.py +25 -25
- huggingface_hub/inference/_providers/featherless_ai.py +4 -4
- huggingface_hub/inference/_providers/fireworks_ai.py +3 -3
- huggingface_hub/inference/_providers/hf_inference.py +13 -13
- huggingface_hub/inference/_providers/hyperbolic.py +4 -4
- huggingface_hub/inference/_providers/nebius.py +10 -10
- huggingface_hub/inference/_providers/novita.py +5 -5
- huggingface_hub/inference/_providers/nscale.py +4 -4
- huggingface_hub/inference/_providers/replicate.py +15 -15
- huggingface_hub/inference/_providers/sambanova.py +6 -6
- huggingface_hub/inference/_providers/together.py +7 -7
- huggingface_hub/lfs.py +21 -94
- huggingface_hub/repocard.py +15 -16
- huggingface_hub/repocard_data.py +57 -57
- huggingface_hub/serialization/__init__.py +0 -1
- huggingface_hub/serialization/_base.py +9 -9
- huggingface_hub/serialization/_dduf.py +7 -7
- huggingface_hub/serialization/_torch.py +28 -28
- huggingface_hub/utils/__init__.py +11 -6
- huggingface_hub/utils/_auth.py +5 -5
- huggingface_hub/utils/_cache_manager.py +49 -74
- huggingface_hub/utils/_deprecation.py +1 -1
- huggingface_hub/utils/_dotenv.py +3 -3
- huggingface_hub/utils/_fixes.py +0 -10
- huggingface_hub/utils/_git_credential.py +3 -3
- huggingface_hub/utils/_headers.py +7 -29
- huggingface_hub/utils/_http.py +371 -208
- huggingface_hub/utils/_pagination.py +4 -4
- huggingface_hub/utils/_parsing.py +98 -0
- huggingface_hub/utils/_paths.py +5 -5
- huggingface_hub/utils/_runtime.py +59 -23
- huggingface_hub/utils/_safetensors.py +21 -21
- huggingface_hub/utils/_subprocess.py +9 -9
- huggingface_hub/utils/_telemetry.py +3 -3
- huggingface_hub/{commands/_cli_utils.py → utils/_terminal.py} +4 -9
- huggingface_hub/utils/_typing.py +3 -3
- huggingface_hub/utils/_validators.py +53 -72
- huggingface_hub/utils/_xet.py +16 -16
- huggingface_hub/utils/_xet_progress_reporting.py +1 -1
- huggingface_hub/utils/insecure_hashlib.py +3 -9
- huggingface_hub/utils/tqdm.py +3 -3
- {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/METADATA +16 -35
- huggingface_hub-1.0.0.dist-info/RECORD +152 -0
- {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/entry_points.txt +0 -1
- huggingface_hub/commands/__init__.py +0 -27
- huggingface_hub/commands/delete_cache.py +0 -476
- huggingface_hub/commands/download.py +0 -204
- huggingface_hub/commands/env.py +0 -39
- huggingface_hub/commands/huggingface_cli.py +0 -65
- huggingface_hub/commands/lfs.py +0 -200
- huggingface_hub/commands/repo.py +0 -151
- huggingface_hub/commands/repo_files.py +0 -132
- huggingface_hub/commands/scan_cache.py +0 -183
- huggingface_hub/commands/tag.py +0 -161
- huggingface_hub/commands/upload.py +0 -318
- huggingface_hub/commands/upload_large_folder.py +0 -131
- huggingface_hub/commands/user.py +0 -208
- huggingface_hub/commands/version.py +0 -40
- huggingface_hub/inference_api.py +0 -217
- huggingface_hub/keras_mixin.py +0 -497
- huggingface_hub/repository.py +0 -1471
- huggingface_hub/serialization/_tensorflow.py +0 -92
- huggingface_hub/utils/_hf_folder.py +0 -68
- huggingface_hub-0.36.0rc0.dist-info/RECORD +0 -170
- {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/LICENSE +0 -0
- {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/WHEEL +0 -0
- {huggingface_hub-0.36.0rc0.dist-info → huggingface_hub-1.0.0.dist-info}/top_level.txt +0 -0
huggingface_hub/utils/_http.py
CHANGED
|
@@ -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
|
|
15
|
+
"""Contains utilities to handle HTTP requests in huggingface_hub."""
|
|
16
16
|
|
|
17
|
+
import atexit
|
|
17
18
|
import io
|
|
18
|
-
import
|
|
19
|
+
import json
|
|
19
20
|
import re
|
|
20
21
|
import threading
|
|
21
22
|
import time
|
|
22
23
|
import uuid
|
|
23
|
-
from
|
|
24
|
+
from contextlib import contextmanager
|
|
25
|
+
from http import HTTPStatus
|
|
24
26
|
from shlex import quote
|
|
25
|
-
from typing import Any, Callable,
|
|
27
|
+
from typing import Any, Callable, Generator, Optional, Union
|
|
26
28
|
|
|
27
|
-
import
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
137
|
+
def default_async_client_factory() -> httpx.AsyncClient:
|
|
138
|
+
"""
|
|
139
|
+
Factory function to create a `httpx.AsyncClient` with the default transport.
|
|
127
140
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
148
|
+
CLIENT_FACTORY_T = Callable[[], httpx.Client]
|
|
149
|
+
ASYNC_CLIENT_FACTORY_T = Callable[[], httpx.AsyncClient]
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
global _GLOBAL_CLIENT_FACTORY
|
|
169
|
+
with _CLIENT_LOCK:
|
|
170
|
+
close_session()
|
|
171
|
+
_GLOBAL_CLIENT_FACTORY = client_factory
|
|
160
172
|
|
|
161
173
|
|
|
162
|
-
def
|
|
174
|
+
def set_async_client_factory(async_client_factory: ASYNC_CLIENT_FACTORY_T) -> None:
|
|
163
175
|
"""
|
|
164
|
-
|
|
176
|
+
Set the HTTP async client factory to be used by `huggingface_hub`.
|
|
165
177
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
182
|
+
<Tip warning={true}>
|
|
172
183
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
185
|
-
|
|
193
|
+
def get_session() -> httpx.Client:
|
|
194
|
+
"""
|
|
195
|
+
Get a `httpx.Client` object, using the transport factory from the user.
|
|
186
196
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
See [`configure_http_backend`] for more details.
|
|
219
|
+
</Tip>
|
|
199
220
|
"""
|
|
200
|
-
|
|
221
|
+
return _GLOBAL_ASYNC_CLIENT_FACTORY()
|
|
201
222
|
|
|
202
223
|
|
|
203
|
-
|
|
204
|
-
def _get_session_from_cache(process_id: int, thread_id: int) -> requests.Session:
|
|
224
|
+
def close_session() -> None:
|
|
205
225
|
"""
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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[
|
|
220
|
-
|
|
221
|
-
|
|
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,
|
|
345
|
+
retry_on_status_codes: Union[int, tuple[int, ...]] = HTTPStatus.SERVICE_UNAVAILABLE,
|
|
225
346
|
**kwargs,
|
|
226
|
-
) -> Response:
|
|
227
|
-
"""Wrapper around
|
|
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 (`
|
|
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 `
|
|
253
|
-
retry_on_status_codes (`int` or `
|
|
254
|
-
Define on which status codes the request must be retried. By default,
|
|
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 `
|
|
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 "
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
325
|
-
raise err
|
|
478
|
+
<Tip warning={true}>
|
|
326
479
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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:
|
|
553
|
-
"""Convert a `
|
|
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:
|
|
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", "{
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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]
|
|
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
|
|