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.
- huggingface_hub/__init__.py +28 -45
- huggingface_hub/_commit_api.py +28 -28
- 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 +13 -39
- huggingface_hub/_oauth.py +8 -8
- huggingface_hub/_snapshot_download.py +14 -28
- huggingface_hub/_space_api.py +4 -4
- huggingface_hub/_tensorboard_logger.py +5 -5
- huggingface_hub/_upload_large_folder.py +15 -15
- 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 +80 -3
- huggingface_hub/cli/auth.py +104 -150
- huggingface_hub/cli/cache.py +102 -126
- huggingface_hub/cli/download.py +93 -110
- huggingface_hub/cli/hf.py +37 -41
- huggingface_hub/cli/jobs.py +689 -1017
- huggingface_hub/cli/lfs.py +120 -143
- huggingface_hub/cli/repo.py +158 -216
- huggingface_hub/cli/repo_files.py +50 -84
- huggingface_hub/cli/system.py +6 -25
- huggingface_hub/cli/upload.py +198 -212
- huggingface_hub/cli/upload_large_folder.py +90 -105
- huggingface_hub/commands/_cli_utils.py +2 -2
- huggingface_hub/commands/delete_cache.py +11 -11
- huggingface_hub/commands/download.py +4 -13
- huggingface_hub/commands/lfs.py +4 -4
- huggingface_hub/commands/repo_files.py +2 -2
- huggingface_hub/commands/tag.py +1 -3
- huggingface_hub/commands/upload.py +4 -4
- huggingface_hub/commands/upload_large_folder.py +3 -3
- huggingface_hub/commands/user.py +4 -5
- huggingface_hub/community.py +5 -5
- huggingface_hub/constants.py +3 -41
- huggingface_hub/dataclasses.py +16 -22
- huggingface_hub/errors.py +43 -30
- huggingface_hub/fastai_utils.py +8 -9
- huggingface_hub/file_download.py +154 -253
- huggingface_hub/hf_api.py +329 -558
- huggingface_hub/hf_file_system.py +104 -62
- huggingface_hub/hub_mixin.py +32 -54
- huggingface_hub/inference/_client.py +178 -163
- huggingface_hub/inference/_common.py +38 -54
- huggingface_hub/inference/_generated/_async_client.py +219 -259
- 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 +2 -13
- huggingface_hub/inference/_providers/_common.py +24 -25
- 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 +24 -33
- huggingface_hub/repocard.py +16 -17
- huggingface_hub/repocard_data.py +56 -56
- 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 +10 -4
- huggingface_hub/utils/_auth.py +5 -5
- huggingface_hub/utils/_cache_manager.py +31 -31
- 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 +369 -209
- huggingface_hub/utils/_pagination.py +4 -4
- huggingface_hub/utils/_paths.py +5 -5
- huggingface_hub/utils/_runtime.py +15 -13
- huggingface_hub/utils/_safetensors.py +21 -21
- huggingface_hub/utils/_subprocess.py +9 -9
- huggingface_hub/utils/_telemetry.py +3 -3
- 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.35.1.dist-info → huggingface_hub-1.0.0rc1.dist-info}/METADATA +17 -26
- huggingface_hub-1.0.0rc1.dist-info/RECORD +161 -0
- huggingface_hub/inference/_providers/publicai.py +0 -6
- huggingface_hub/inference/_providers/scaleway.py +0 -28
- huggingface_hub/inference_api.py +0 -217
- huggingface_hub/keras_mixin.py +0 -500
- huggingface_hub/repository.py +0 -1477
- huggingface_hub/serialization/_tensorflow.py +0 -95
- huggingface_hub/utils/_hf_folder.py +0 -68
- huggingface_hub-0.35.1.dist-info/RECORD +0 -168
- {huggingface_hub-0.35.1.dist-info → huggingface_hub-1.0.0rc1.dist-info}/LICENSE +0 -0
- {huggingface_hub-0.35.1.dist-info → huggingface_hub-1.0.0rc1.dist-info}/WHEEL +0 -0
- {huggingface_hub-0.35.1.dist-info → huggingface_hub-1.0.0rc1.dist-info}/entry_points.txt +0 -0
- {huggingface_hub-0.35.1.dist-info → huggingface_hub-1.0.0rc1.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,273 @@ REPO_API_REGEX = re.compile(
|
|
|
71
69
|
)
|
|
72
70
|
|
|
73
71
|
|
|
74
|
-
class
|
|
75
|
-
|
|
72
|
+
class HfHubTransport(httpx.HTTPTransport):
|
|
73
|
+
"""
|
|
74
|
+
Transport 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
|
+
"""
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
if constants.
|
|
93
|
-
|
|
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().
|
|
96
|
-
except
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
144
|
+
def default_async_client_factory() -> httpx.AsyncClient:
|
|
127
145
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
155
|
+
CLIENT_FACTORY_T = Callable[[], httpx.Client]
|
|
156
|
+
ASYNC_CLIENT_FACTORY_T = Callable[[], httpx.AsyncClient]
|
|
138
157
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
global _GLOBAL_CLIENT_FACTORY
|
|
176
|
+
with _CLIENT_LOCK:
|
|
177
|
+
close_session()
|
|
178
|
+
_GLOBAL_CLIENT_FACTORY = client_factory
|
|
160
179
|
|
|
161
180
|
|
|
162
|
-
def
|
|
181
|
+
def set_async_client_factory(async_client_factory: ASYNC_CLIENT_FACTORY_T) -> None:
|
|
163
182
|
"""
|
|
164
|
-
|
|
183
|
+
Set the HTTP async client factory to be used by `huggingface_hub`.
|
|
165
184
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
189
|
+
<Tip warning={true}>
|
|
172
190
|
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
195
|
-
"""
|
|
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
|
-
|
|
198
|
-
See [`configure_http_backend`] for more details.
|
|
226
|
+
</Tip>
|
|
199
227
|
"""
|
|
200
|
-
|
|
228
|
+
return _GLOBAL_ASYNC_CLIENT_FACTORY()
|
|
201
229
|
|
|
202
230
|
|
|
203
|
-
|
|
204
|
-
def _get_session_from_cache(process_id: int, thread_id: int) -> requests.Session:
|
|
231
|
+
def close_session() -> None:
|
|
205
232
|
"""
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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[
|
|
220
|
-
|
|
221
|
-
|
|
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,
|
|
352
|
+
retry_on_status_codes: Union[int, tuple[int, ...]] = HTTPStatus.SERVICE_UNAVAILABLE,
|
|
224
353
|
**kwargs,
|
|
225
|
-
) -> Response:
|
|
226
|
-
"""Wrapper around
|
|
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 (`
|
|
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 `
|
|
252
|
-
retry_on_status_codes (`int` or `
|
|
253
|
-
Define on which status codes the request must be retried. By default,
|
|
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 `
|
|
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 "
|
|
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 `
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
raise err
|
|
488
|
+
<Tip warning={true}>
|
|
328
489
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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:
|
|
558
|
-
"""Convert a `
|
|
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:
|
|
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", "{
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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]
|
|
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)]
|