typecast-python 0.3.14__tar.gz → 0.4.0__tar.gz
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.
- {typecast_python-0.3.14 → typecast_python-0.4.0}/PKG-INFO +10 -11
- {typecast_python-0.3.14 → typecast_python-0.4.0}/README.md +6 -4
- {typecast_python-0.3.14 → typecast_python-0.4.0}/pyproject.toml +5 -8
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/_user_agent.py +0 -16
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/async_client.py +6 -27
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/client.py +4 -12
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/models/tts.py +22 -2
- typecast_python-0.3.14/src/typecast/_httpx_compat.py +0 -191
- {typecast_python-0.3.14 → typecast_python-0.4.0}/.gitignore +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/LICENSE +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/__init__.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/_voice_clone.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/composer.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/conf.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/exceptions.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/models/__init__.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/models/error.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/models/subscription.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/models/voices.py +0 -0
- {typecast_python-0.3.14 → typecast_python-0.4.0}/src/typecast/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: typecast-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Official Typecast Python SDK - Convert text to lifelike speech using AI-powered voices
|
|
5
5
|
Project-URL: Homepage, https://typecast.ai
|
|
6
6
|
Project-URL: Documentation, https://typecast.ai/docs/overview
|
|
@@ -217,8 +217,6 @@ Classifier: Intended Audience :: Developers
|
|
|
217
217
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
218
218
|
Classifier: Operating System :: OS Independent
|
|
219
219
|
Classifier: Programming Language :: Python :: 3
|
|
220
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
221
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
222
220
|
Classifier: Programming Language :: Python :: 3.10
|
|
223
221
|
Classifier: Programming Language :: Python :: 3.11
|
|
224
222
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -226,11 +224,10 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
226
224
|
Classifier: Programming Language :: Python :: 3.14
|
|
227
225
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
228
226
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
229
|
-
Requires-Python: <3.15,>=3.
|
|
230
|
-
Requires-Dist: aiohttp>=3.14.1
|
|
231
|
-
Requires-Dist: httpx<1,>=0.28.1; python_version < '3.10'
|
|
227
|
+
Requires-Python: <3.15,>=3.10
|
|
228
|
+
Requires-Dist: aiohttp>=3.14.1
|
|
232
229
|
Requires-Dist: pydantic>=2.0.0
|
|
233
|
-
Requires-Dist: requests>=2.33.0
|
|
230
|
+
Requires-Dist: requests>=2.33.0
|
|
234
231
|
Requires-Dist: typing-extensions>=4.0.0
|
|
235
232
|
Provides-Extra: dev
|
|
236
233
|
Requires-Dist: aioresponses>=0.7.6; extra == 'dev'
|
|
@@ -256,7 +253,7 @@ Convert text to lifelike speech using AI-powered voices
|
|
|
256
253
|
[](https://pypi.org/project/typecast-python/)
|
|
257
254
|
[](../docs/coverage-policy.md)
|
|
258
255
|
[](LICENSE)
|
|
259
|
-
[](https://www.python.org/)
|
|
260
257
|
|
|
261
258
|
[Documentation](https://typecast.ai/docs) | [API Reference](https://typecast.ai/docs/api-reference) | [Get API Key](https://typecast.ai/developers/api/api-key)
|
|
262
259
|
|
|
@@ -284,6 +281,8 @@ Convert text to lifelike speech using AI-powered voices
|
|
|
284
281
|
|
|
285
282
|
## Installation
|
|
286
283
|
|
|
284
|
+
Python 3.10 through 3.14 is required. Python 3.8 and 3.9 are no longer supported because they are end-of-life. Version 0.3.15 is the last compatible published release. Upgrade Python and recreate your virtual environment before installing 0.4.0. A temporary `typecast-python==0.3.15` pin does not restore security support for an EOL runtime.
|
|
285
|
+
|
|
287
286
|
```bash
|
|
288
287
|
pip install typecast-python
|
|
289
288
|
```
|
|
@@ -383,11 +382,11 @@ response = client.text_to_speech(TTSRequest(
|
|
|
383
382
|
```python
|
|
384
383
|
from typecast.models import VoicesV2Filter, TTSModel, GenderEnum, AgeEnum
|
|
385
384
|
|
|
386
|
-
# Get all voices (
|
|
387
|
-
voices = client.
|
|
385
|
+
# Get all voices (V3 API)
|
|
386
|
+
voices = client.voices_v3()
|
|
388
387
|
|
|
389
388
|
# Filter by criteria
|
|
390
|
-
filtered = client.
|
|
389
|
+
filtered = client.voices_v3(VoicesV2Filter(
|
|
391
390
|
model=TTSModel.SSFM_V30,
|
|
392
391
|
gender=GenderEnum.FEMALE,
|
|
393
392
|
age=AgeEnum.YOUNG_ADULT
|
|
@@ -9,7 +9,7 @@ Convert text to lifelike speech using AI-powered voices
|
|
|
9
9
|
[](https://pypi.org/project/typecast-python/)
|
|
10
10
|
[](../docs/coverage-policy.md)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](https://www.python.org/)
|
|
13
13
|
|
|
14
14
|
[Documentation](https://typecast.ai/docs) | [API Reference](https://typecast.ai/docs/api-reference) | [Get API Key](https://typecast.ai/developers/api/api-key)
|
|
15
15
|
|
|
@@ -37,6 +37,8 @@ Convert text to lifelike speech using AI-powered voices
|
|
|
37
37
|
|
|
38
38
|
## Installation
|
|
39
39
|
|
|
40
|
+
Python 3.10 through 3.14 is required. Python 3.8 and 3.9 are no longer supported because they are end-of-life. Version 0.3.15 is the last compatible published release. Upgrade Python and recreate your virtual environment before installing 0.4.0. A temporary `typecast-python==0.3.15` pin does not restore security support for an EOL runtime.
|
|
41
|
+
|
|
40
42
|
```bash
|
|
41
43
|
pip install typecast-python
|
|
42
44
|
```
|
|
@@ -136,11 +138,11 @@ response = client.text_to_speech(TTSRequest(
|
|
|
136
138
|
```python
|
|
137
139
|
from typecast.models import VoicesV2Filter, TTSModel, GenderEnum, AgeEnum
|
|
138
140
|
|
|
139
|
-
# Get all voices (
|
|
140
|
-
voices = client.
|
|
141
|
+
# Get all voices (V3 API)
|
|
142
|
+
voices = client.voices_v3()
|
|
141
143
|
|
|
142
144
|
# Filter by criteria
|
|
143
|
-
filtered = client.
|
|
145
|
+
filtered = client.voices_v3(VoicesV2Filter(
|
|
144
146
|
model=TTSModel.SSFM_V30,
|
|
145
147
|
gender=GenderEnum.FEMALE,
|
|
146
148
|
age=AgeEnum.YOUNG_ADULT
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "typecast-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Official Typecast Python SDK - Convert text to lifelike speech using AI-powered voices"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Neosapience", email = "help@typecast.ai"}
|
|
@@ -14,15 +14,13 @@ maintainers = [
|
|
|
14
14
|
]
|
|
15
15
|
license = {file = "LICENSE"}
|
|
16
16
|
readme = "README.md"
|
|
17
|
-
requires-python = ">=3.
|
|
17
|
+
requires-python = ">=3.10,<3.15"
|
|
18
18
|
keywords = ["typecast", "tts", "text-to-speech", "ai", "voice", "audio", "speech-synthesis", "voice-generation"]
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Development Status :: 3 - Alpha",
|
|
21
21
|
"Intended Audience :: Developers",
|
|
22
22
|
"License :: OSI Approved :: Apache Software License",
|
|
23
23
|
"Programming Language :: Python :: 3",
|
|
24
|
-
"Programming Language :: Python :: 3.8",
|
|
25
|
-
"Programming Language :: Python :: 3.9",
|
|
26
24
|
"Programming Language :: Python :: 3.10",
|
|
27
25
|
"Programming Language :: Python :: 3.11",
|
|
28
26
|
"Programming Language :: Python :: 3.12",
|
|
@@ -33,9 +31,8 @@ classifiers = [
|
|
|
33
31
|
"Operating System :: OS Independent",
|
|
34
32
|
]
|
|
35
33
|
dependencies = [
|
|
36
|
-
"aiohttp>=3.14.1
|
|
37
|
-
"requests>=2.33.0
|
|
38
|
-
"httpx>=0.28.1,<1; python_version < '3.10'",
|
|
34
|
+
"aiohttp>=3.14.1",
|
|
35
|
+
"requests>=2.33.0",
|
|
39
36
|
"pydantic>=2.0.0",
|
|
40
37
|
"typing-extensions>=4.0.0",
|
|
41
38
|
]
|
|
@@ -83,7 +80,7 @@ exclude = [
|
|
|
83
80
|
|
|
84
81
|
[tool.black]
|
|
85
82
|
line-length = 88
|
|
86
|
-
target-version = ['
|
|
83
|
+
target-version = ['py310']
|
|
87
84
|
include = '\.pyi?$'
|
|
88
85
|
|
|
89
86
|
[tool.isort]
|
|
@@ -108,19 +108,3 @@ def aiohttp_user_agent(
|
|
|
108
108
|
generated_by=generated_by,
|
|
109
109
|
)
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
def httpx_user_agent(
|
|
113
|
-
host: str,
|
|
114
|
-
mode: str,
|
|
115
|
-
transport: str = "rest",
|
|
116
|
-
source: Optional[str] = None,
|
|
117
|
-
generated_by: Optional[str] = None,
|
|
118
|
-
) -> str:
|
|
119
|
-
return build_user_agent(
|
|
120
|
-
mode=mode,
|
|
121
|
-
http_library=f"httpx/{_package_version('httpx')}",
|
|
122
|
-
host=host,
|
|
123
|
-
transport=transport,
|
|
124
|
-
source=source,
|
|
125
|
-
generated_by=generated_by,
|
|
126
|
-
)
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import sys
|
|
5
4
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
5
|
+
from typing import Any, AsyncIterator, BinaryIO, Optional, Union
|
|
7
6
|
from urllib.parse import quote
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
import aiohttp
|
|
11
|
-
else: # pragma: no cover
|
|
12
|
-
aiohttp = None # type: ignore[assignment]
|
|
8
|
+
import aiohttp
|
|
13
9
|
|
|
14
10
|
from . import conf
|
|
15
|
-
from ._user_agent import aiohttp_user_agent, attribution_suffix
|
|
11
|
+
from ._user_agent import aiohttp_user_agent, attribution_suffix
|
|
16
12
|
from ._voice_clone import (
|
|
17
13
|
normalize_clone_model,
|
|
18
14
|
validate_clone_inputs,
|
|
@@ -20,8 +16,6 @@ from ._voice_clone import (
|
|
|
20
16
|
validate_voice_id,
|
|
21
17
|
)
|
|
22
18
|
|
|
23
|
-
if TYPE_CHECKING or sys.version_info < (3, 10): # pragma: no cover
|
|
24
|
-
from ._httpx_compat import AiohttpCompatSession, ClientTimeout, FormData
|
|
25
19
|
from .client import (
|
|
26
20
|
_guess_audio_mime,
|
|
27
21
|
_output_with_inferred_format,
|
|
@@ -120,21 +114,12 @@ class AsyncTypecast:
|
|
|
120
114
|
aiohttp_user_agent(
|
|
121
115
|
self.host, source=self.source, generated_by=self.generated_by
|
|
122
116
|
)
|
|
123
|
-
if aiohttp
|
|
124
|
-
else httpx_user_agent(
|
|
125
|
-
self.host,
|
|
126
|
-
"async",
|
|
127
|
-
source=self.source,
|
|
128
|
-
generated_by=self.generated_by,
|
|
129
|
-
)
|
|
130
117
|
)
|
|
131
118
|
}
|
|
132
119
|
if self.api_key:
|
|
133
120
|
headers["X-API-KEY"] = self.api_key
|
|
134
121
|
self.session = (
|
|
135
122
|
aiohttp.ClientSession(headers=headers)
|
|
136
|
-
if aiohttp
|
|
137
|
-
else AiohttpCompatSession(headers=headers)
|
|
138
123
|
)
|
|
139
124
|
return self
|
|
140
125
|
|
|
@@ -288,8 +273,6 @@ class AsyncTypecast:
|
|
|
288
273
|
endpoint = "/v1/text-to-speech/stream"
|
|
289
274
|
stream_timeout = (
|
|
290
275
|
aiohttp.ClientTimeout(sock_connect=10, sock_read=300)
|
|
291
|
-
if aiohttp
|
|
292
|
-
else ClientTimeout(sock_connect=10, sock_read=300)
|
|
293
276
|
)
|
|
294
277
|
async with self.session.post(
|
|
295
278
|
f"{self.host}{endpoint}",
|
|
@@ -377,7 +360,7 @@ class AsyncTypecast:
|
|
|
377
360
|
audio_bytes, filename = validate_clone_inputs(audio, name)
|
|
378
361
|
model_str = normalize_clone_model(model)
|
|
379
362
|
|
|
380
|
-
form: Any = aiohttp.FormData()
|
|
363
|
+
form: Any = aiohttp.FormData()
|
|
381
364
|
form.add_field("name", name)
|
|
382
365
|
form.add_field("model", model_str)
|
|
383
366
|
form.add_field(
|
|
@@ -388,8 +371,6 @@ class AsyncTypecast:
|
|
|
388
371
|
)
|
|
389
372
|
timeout = (
|
|
390
373
|
aiohttp.ClientTimeout(total=300, connect=10)
|
|
391
|
-
if aiohttp
|
|
392
|
-
else ClientTimeout(total=300, connect=10)
|
|
393
374
|
)
|
|
394
375
|
async with self.session.post(
|
|
395
376
|
f"{self.host}/v1/custom-voices/instant-clone",
|
|
@@ -418,8 +399,6 @@ class AsyncTypecast:
|
|
|
418
399
|
validate_custom_voice_id(voice_id)
|
|
419
400
|
timeout = (
|
|
420
401
|
aiohttp.ClientTimeout(total=60, connect=10)
|
|
421
|
-
if aiohttp
|
|
422
|
-
else ClientTimeout(total=60, connect=10)
|
|
423
402
|
)
|
|
424
403
|
async with self.session.delete(
|
|
425
404
|
f"{self.host}/v1/custom-voices/{quote(voice_id, safe='')}",
|
|
@@ -441,12 +420,12 @@ class AsyncTypecast:
|
|
|
441
420
|
if self.session is None:
|
|
442
421
|
raise TypecastError("Client session not initialized; use 'async with'.")
|
|
443
422
|
audio_bytes, filename = validate_clone_inputs(audio, name)
|
|
444
|
-
form: Any = aiohttp.FormData()
|
|
423
|
+
form: Any = aiohttp.FormData()
|
|
445
424
|
form.add_field("name", name)
|
|
446
425
|
form.add_field("language", str(language.value if hasattr(language, "value") else language))
|
|
447
426
|
form.add_field("model", normalize_clone_model(model))
|
|
448
427
|
form.add_field("files", audio_bytes, filename=filename, content_type=_guess_audio_mime(filename))
|
|
449
|
-
timeout = aiohttp.ClientTimeout(total=300, connect=10)
|
|
428
|
+
timeout = aiohttp.ClientTimeout(total=300, connect=10)
|
|
450
429
|
async with self.session.post(
|
|
451
430
|
f"{self.host}/v1/custom-voices/professional-clone",
|
|
452
431
|
data=form, timeout=timeout, headers=self._request_headers(),
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import sys
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
4
|
+
from typing import Any, BinaryIO, Iterator, Optional, Union
|
|
6
5
|
from urllib.parse import quote
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
import requests
|
|
10
|
-
else: # pragma: no cover
|
|
11
|
-
requests = None # type: ignore[assignment]
|
|
7
|
+
import requests
|
|
12
8
|
|
|
13
9
|
from . import conf
|
|
14
|
-
from ._user_agent import attribution_suffix,
|
|
10
|
+
from ._user_agent import attribution_suffix, requests_user_agent
|
|
15
11
|
from ._voice_clone import (
|
|
16
12
|
normalize_clone_model,
|
|
17
13
|
validate_clone_inputs,
|
|
@@ -19,8 +15,6 @@ from ._voice_clone import (
|
|
|
19
15
|
validate_voice_id,
|
|
20
16
|
)
|
|
21
17
|
|
|
22
|
-
if TYPE_CHECKING or sys.version_info < (3, 10): # pragma: no cover
|
|
23
|
-
from ._httpx_compat import RequestsCompatSession
|
|
24
18
|
from .composer import SpeechComposer
|
|
25
19
|
from .exceptions import (
|
|
26
20
|
BadRequestError,
|
|
@@ -144,13 +138,11 @@ class Typecast:
|
|
|
144
138
|
if session is not None:
|
|
145
139
|
self.session = session
|
|
146
140
|
else:
|
|
147
|
-
self.session = requests.Session()
|
|
141
|
+
self.session = requests.Session()
|
|
148
142
|
headers = {
|
|
149
143
|
"Content-Type": "application/json",
|
|
150
144
|
"User-Agent": (
|
|
151
145
|
requests_user_agent(self.host, source=source, generated_by=generated_by)
|
|
152
|
-
if requests
|
|
153
|
-
else httpx_user_agent(self.host, "sync", source=source, generated_by=generated_by)
|
|
154
146
|
),
|
|
155
147
|
}
|
|
156
148
|
if self.api_key:
|
|
@@ -124,6 +124,13 @@ TTSPrompt = Union[Prompt, PresetPrompt, SmartPrompt]
|
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
class Output(BaseModel):
|
|
127
|
+
remove_silence_ms: Optional[int] = Field(
|
|
128
|
+
default=None,
|
|
129
|
+
strict=True,
|
|
130
|
+
ge=0,
|
|
131
|
+
le=1000,
|
|
132
|
+
description="Remaining detected silence in milliseconds. 0 removes silence; None disables this processing.",
|
|
133
|
+
)
|
|
127
134
|
volume: Optional[int] = Field(
|
|
128
135
|
default=100,
|
|
129
136
|
ge=0,
|
|
@@ -191,6 +198,14 @@ class OutputStream(BaseModel):
|
|
|
191
198
|
|
|
192
199
|
model_config = ConfigDict(extra="forbid")
|
|
193
200
|
|
|
201
|
+
remove_silence_ms: Optional[int] = Field(
|
|
202
|
+
default=None,
|
|
203
|
+
strict=True,
|
|
204
|
+
ge=0,
|
|
205
|
+
le=1000,
|
|
206
|
+
description="Remaining detected silence in milliseconds. 0 removes silence; None disables this processing.",
|
|
207
|
+
)
|
|
208
|
+
|
|
194
209
|
audio_pitch: Optional[int] = Field(default=0, ge=-12, le=12)
|
|
195
210
|
audio_tempo: Optional[float] = Field(default=1.0, ge=0.5, le=2.0)
|
|
196
211
|
audio_format: Optional[str] = Field(
|
|
@@ -419,6 +434,7 @@ class TTSWithTimestampsResponse(BaseModel):
|
|
|
419
434
|
def audio_bytes(self) -> bytes:
|
|
420
435
|
"""Return decoded audio bytes from the base64 `audio` field."""
|
|
421
436
|
import base64
|
|
437
|
+
|
|
422
438
|
return base64.b64decode(self.audio, validate=True)
|
|
423
439
|
|
|
424
440
|
def save_audio(self, path: str) -> None:
|
|
@@ -436,7 +452,9 @@ class TTSWithTimestampsResponse(BaseModel):
|
|
|
436
452
|
subtitle guidelines (7.0s / 42 chars).
|
|
437
453
|
"""
|
|
438
454
|
segments, word_mode = _segments_for_captioning(self.words, self.characters)
|
|
439
|
-
cues = _group_into_cues(
|
|
455
|
+
cues = _group_into_cues(
|
|
456
|
+
segments, word_mode=word_mode, max_seconds=max_seconds, max_chars=max_chars
|
|
457
|
+
)
|
|
440
458
|
if not cues:
|
|
441
459
|
raise ValueError("no alignment segments to caption from")
|
|
442
460
|
lines = []
|
|
@@ -457,7 +475,9 @@ class TTSWithTimestampsResponse(BaseModel):
|
|
|
457
475
|
subtitle guidelines (7.0s / 42 chars).
|
|
458
476
|
"""
|
|
459
477
|
segments, word_mode = _segments_for_captioning(self.words, self.characters)
|
|
460
|
-
cues = _group_into_cues(
|
|
478
|
+
cues = _group_into_cues(
|
|
479
|
+
segments, word_mode=word_mode, max_seconds=max_seconds, max_chars=max_chars
|
|
480
|
+
)
|
|
461
481
|
if not cues:
|
|
462
482
|
raise ValueError("no alignment segments to caption from")
|
|
463
483
|
lines = ["WEBVTT", ""]
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Tuple
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _timeout(value: Any) -> Any:
|
|
9
|
-
if isinstance(value, ClientTimeout):
|
|
10
|
-
total = value.total or value.sock_read or 5.0
|
|
11
|
-
return httpx.Timeout(
|
|
12
|
-
total, connect=value.connect or value.sock_connect or total
|
|
13
|
-
)
|
|
14
|
-
if isinstance(value, tuple):
|
|
15
|
-
return httpx.Timeout(value[1], connect=value[0])
|
|
16
|
-
return value
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _headers(defaults: Dict[str, str], supplied: Optional[dict]) -> Dict[str, str]:
|
|
20
|
-
headers = dict(defaults)
|
|
21
|
-
for key, value in (supplied or {}).items():
|
|
22
|
-
if value is None:
|
|
23
|
-
headers.pop(key, None)
|
|
24
|
-
else:
|
|
25
|
-
headers[key] = value
|
|
26
|
-
return headers
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class RequestsCompatResponse:
|
|
30
|
-
def __init__(self, response: httpx.Response):
|
|
31
|
-
self._response = response
|
|
32
|
-
|
|
33
|
-
def __getattr__(self, name: str) -> Any:
|
|
34
|
-
return getattr(self._response, name)
|
|
35
|
-
|
|
36
|
-
def iter_content(self, chunk_size: int) -> Iterator[bytes]:
|
|
37
|
-
return self._response.iter_bytes(chunk_size)
|
|
38
|
-
|
|
39
|
-
def close(self) -> None:
|
|
40
|
-
self._response.close()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class RequestsCompatSession:
|
|
44
|
-
def __init__(self, client: Optional[httpx.Client] = None):
|
|
45
|
-
self._client = client or httpx.Client(timeout=None)
|
|
46
|
-
self.headers: Dict[str, str] = {}
|
|
47
|
-
|
|
48
|
-
def _request(self, method: str, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
49
|
-
stream = kwargs.pop("stream", False)
|
|
50
|
-
kwargs["headers"] = _headers(self.headers, kwargs.get("headers"))
|
|
51
|
-
if "timeout" in kwargs:
|
|
52
|
-
kwargs["timeout"] = _timeout(kwargs["timeout"])
|
|
53
|
-
if stream:
|
|
54
|
-
request = self._client.build_request(method, url, **kwargs)
|
|
55
|
-
response = self._client.send(request, stream=True)
|
|
56
|
-
else:
|
|
57
|
-
response = self._client.request(method, url, **kwargs)
|
|
58
|
-
return RequestsCompatResponse(response)
|
|
59
|
-
|
|
60
|
-
def get(self, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
61
|
-
return self._request("GET", url, **kwargs)
|
|
62
|
-
|
|
63
|
-
def post(self, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
64
|
-
return self._request("POST", url, **kwargs)
|
|
65
|
-
|
|
66
|
-
def delete(self, url: str, **kwargs: Any) -> RequestsCompatResponse:
|
|
67
|
-
return self._request("DELETE", url, **kwargs)
|
|
68
|
-
|
|
69
|
-
def close(self) -> None:
|
|
70
|
-
self._client.close()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class ClientTimeout:
|
|
74
|
-
def __init__(
|
|
75
|
-
self,
|
|
76
|
-
*,
|
|
77
|
-
total: Optional[float] = None,
|
|
78
|
-
connect: Optional[float] = None,
|
|
79
|
-
sock_connect: Optional[float] = None,
|
|
80
|
-
sock_read: Optional[float] = None,
|
|
81
|
-
):
|
|
82
|
-
self.total = total
|
|
83
|
-
self.connect = connect
|
|
84
|
-
self.sock_connect = sock_connect
|
|
85
|
-
self.sock_read = sock_read
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class FormData:
|
|
89
|
-
def __init__(self):
|
|
90
|
-
self.fields: List[Tuple[str, Any, Optional[str], Optional[str]]] = []
|
|
91
|
-
|
|
92
|
-
def add_field(
|
|
93
|
-
self,
|
|
94
|
-
name: str,
|
|
95
|
-
value: Any,
|
|
96
|
-
*,
|
|
97
|
-
filename: Optional[str] = None,
|
|
98
|
-
content_type: Optional[str] = None,
|
|
99
|
-
) -> None:
|
|
100
|
-
self.fields.append((name, value, filename, content_type))
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class _AsyncContent:
|
|
104
|
-
def __init__(self, response: httpx.Response):
|
|
105
|
-
self._response = response
|
|
106
|
-
|
|
107
|
-
async def iter_chunked(self, chunk_size: int) -> AsyncIterator[bytes]:
|
|
108
|
-
async for chunk in self._response.aiter_bytes(chunk_size):
|
|
109
|
-
yield chunk
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class AiohttpCompatResponse:
|
|
113
|
-
def __init__(self, response: httpx.Response):
|
|
114
|
-
self._response = response
|
|
115
|
-
self.status = response.status_code
|
|
116
|
-
self.headers = response.headers
|
|
117
|
-
self.content = _AsyncContent(response)
|
|
118
|
-
|
|
119
|
-
async def read(self) -> bytes:
|
|
120
|
-
return await self._response.aread()
|
|
121
|
-
|
|
122
|
-
async def text(self) -> str:
|
|
123
|
-
await self._response.aread()
|
|
124
|
-
return self._response.text
|
|
125
|
-
|
|
126
|
-
async def json(self) -> Any:
|
|
127
|
-
await self._response.aread()
|
|
128
|
-
return self._response.json()
|
|
129
|
-
|
|
130
|
-
async def close(self) -> None:
|
|
131
|
-
await self._response.aclose()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class _AsyncRequestContext:
|
|
135
|
-
def __init__(self, request: Any):
|
|
136
|
-
self._request = request
|
|
137
|
-
self._response: Optional[AiohttpCompatResponse] = None
|
|
138
|
-
|
|
139
|
-
async def __aenter__(self) -> AiohttpCompatResponse:
|
|
140
|
-
response = await self._request
|
|
141
|
-
self._response = response
|
|
142
|
-
return response
|
|
143
|
-
|
|
144
|
-
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
145
|
-
if self._response:
|
|
146
|
-
await self._response.close()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class AiohttpCompatSession:
|
|
150
|
-
def __init__(
|
|
151
|
-
self,
|
|
152
|
-
headers: Optional[dict] = None,
|
|
153
|
-
client: Optional[httpx.AsyncClient] = None,
|
|
154
|
-
):
|
|
155
|
-
self._client = client or httpx.AsyncClient(timeout=300)
|
|
156
|
-
self.headers = dict(headers or {})
|
|
157
|
-
|
|
158
|
-
async def _request(
|
|
159
|
-
self, method: str, url: str, **kwargs: Any
|
|
160
|
-
) -> AiohttpCompatResponse:
|
|
161
|
-
kwargs["headers"] = _headers(self.headers, kwargs.get("headers"))
|
|
162
|
-
if "timeout" in kwargs:
|
|
163
|
-
kwargs["timeout"] = _timeout(kwargs["timeout"])
|
|
164
|
-
form = kwargs.get("data")
|
|
165
|
-
if isinstance(form, FormData):
|
|
166
|
-
kwargs.pop("data")
|
|
167
|
-
kwargs["data"] = {
|
|
168
|
-
name: value
|
|
169
|
-
for name, value, filename, _ in form.fields
|
|
170
|
-
if filename is None
|
|
171
|
-
}
|
|
172
|
-
kwargs["files"] = [
|
|
173
|
-
(name, (filename, value, content_type))
|
|
174
|
-
for name, value, filename, content_type in form.fields
|
|
175
|
-
if filename is not None
|
|
176
|
-
]
|
|
177
|
-
request = self._client.build_request(method, url, **kwargs)
|
|
178
|
-
response = await self._client.send(request, stream=True)
|
|
179
|
-
return AiohttpCompatResponse(response)
|
|
180
|
-
|
|
181
|
-
def get(self, url: str, **kwargs: Any) -> _AsyncRequestContext:
|
|
182
|
-
return _AsyncRequestContext(self._request("GET", url, **kwargs))
|
|
183
|
-
|
|
184
|
-
def post(self, url: str, **kwargs: Any) -> _AsyncRequestContext:
|
|
185
|
-
return _AsyncRequestContext(self._request("POST", url, **kwargs))
|
|
186
|
-
|
|
187
|
-
def delete(self, url: str, **kwargs: Any) -> _AsyncRequestContext:
|
|
188
|
-
return _AsyncRequestContext(self._request("DELETE", url, **kwargs))
|
|
189
|
-
|
|
190
|
-
async def close(self) -> None:
|
|
191
|
-
await self._client.aclose()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|