spitch 1.26.0__tar.gz → 1.27.1__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.
Potentially problematic release.
This version of spitch might be problematic. Click here for more details.
- spitch-1.27.1/.release-please-manifest.json +3 -0
- {spitch-1.26.0 → spitch-1.27.1}/CHANGELOG.md +48 -0
- {spitch-1.26.0 → spitch-1.27.1}/CONTRIBUTING.md +1 -2
- {spitch-1.26.0 → spitch-1.27.1}/PKG-INFO +3 -3
- {spitch-1.26.0 → spitch-1.27.1}/README.md +2 -2
- {spitch-1.26.0 → spitch-1.27.1}/SECURITY.md +2 -2
- {spitch-1.26.0 → spitch-1.27.1}/pyproject.toml +3 -2
- {spitch-1.26.0 → spitch-1.27.1}/requirements-dev.lock +4 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/__init__.py +5 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_base_client.py +22 -2
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_models.py +2 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_response.py +1 -1
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_types.py +2 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_proxy.py +4 -1
- spitch-1.27.1/src/spitch/_utils/_resources_proxy.py +24 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_version.py +1 -1
- {spitch-1.26.0 → spitch-1.27.1}/tests/conftest.py +2 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_client.py +118 -42
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_utils/test_proxy.py +11 -0
- spitch-1.26.0/.release-please-manifest.json +0 -3
- {spitch-1.26.0 → spitch-1.27.1}/.gitignore +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/LICENSE +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/api.md +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/bin/check-release-environment +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/bin/publish-pypi +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/examples/.keep +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/examples/example.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/mypy.ini +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/noxfile.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/release-please-config.json +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/requirements.lock +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_client.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_compat.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_constants.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_exceptions.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_files.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_qs.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_resource.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_streaming.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/__init__.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_logs.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_reflection.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_streams.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_sync.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_transform.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_typing.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/_utils/_utils.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/lib/.keep +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/py.typed +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/resources/__init__.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/resources/speech.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/resources/text.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/__init__.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/speech_generate_params.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/speech_transcribe_params.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/speech_transcribe_response.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/text_tone_mark_params.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/text_tone_mark_response.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/text_translate_params.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/src/spitch/types/text_translate_response.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/__init__.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/api_resources/__init__.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/api_resources/test_speech.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/api_resources/test_text.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/sample_file.txt +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_deepcopy.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_extract_files.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_files.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_models.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_qs.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_required_args.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_response.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_streaming.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_transform.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/test_utils/test_typing.py +0 -0
- {spitch-1.26.0 → spitch-1.27.1}/tests/utils.py +0 -0
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.27.1 (2025-06-19)
|
|
4
|
+
|
|
5
|
+
Full Changelog: [v1.27.0...v1.27.1](https://github.com/spi-tch/spitch-python/compare/v1.27.0...v1.27.1)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **tests:** fix: tests which call HTTP endpoints directly with the example parameters ([e6df2f6](https://github.com/spi-tch/spitch-python/commit/e6df2f6b390f5f3313ff1e20ee2890fd2ac53265))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Chores
|
|
13
|
+
|
|
14
|
+
* **readme:** update badges ([d272526](https://github.com/spi-tch/spitch-python/commit/d2725265a638bba5ef65bf8ab468a33af6b5b594))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* **client:** fix httpx.Timeout documentation reference ([d8af609](https://github.com/spi-tch/spitch-python/commit/d8af609deca7a01713250544babf4ced45dc2376))
|
|
20
|
+
|
|
21
|
+
## 1.27.0 (2025-06-17)
|
|
22
|
+
|
|
23
|
+
Full Changelog: [v1.26.0...v1.27.0](https://github.com/spi-tch/spitch-python/compare/v1.26.0...v1.27.0)
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* **client:** add follow_redirects request option ([23f17af](https://github.com/spi-tch/spitch-python/commit/23f17afc3292d6641890115e849134e14412ab7c))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* **client:** correctly parse binary response | stream ([43997d9](https://github.com/spi-tch/spitch-python/commit/43997d9e1f67269df4f5ed9472f1abb6dfc8afc3))
|
|
33
|
+
* **package:** support direct resource imports ([3b664db](https://github.com/spi-tch/spitch-python/commit/3b664db8de2fe51363215430c04ef0d45a96ef15))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Chores
|
|
37
|
+
|
|
38
|
+
* broadly detect json family of content-type headers ([1358fe6](https://github.com/spi-tch/spitch-python/commit/1358fe6da25145ce5d57317a835bf306e9886f5c))
|
|
39
|
+
* **ci:** enable for pull requests ([f21c633](https://github.com/spi-tch/spitch-python/commit/f21c633b3bc746fdfcf95214bda7e664212fce3f))
|
|
40
|
+
* **ci:** fix installation instructions ([c23d223](https://github.com/spi-tch/spitch-python/commit/c23d22393563b50ac77742d75cda7f7b95e8a4a0))
|
|
41
|
+
* **ci:** only use depot for staging repos ([86c82b9](https://github.com/spi-tch/spitch-python/commit/86c82b989e1599bc9d71060f2f2998eaa4b79081))
|
|
42
|
+
* **ci:** upload sdks to package manager ([a40305e](https://github.com/spi-tch/spitch-python/commit/a40305e129dd6795b0e0e1408b863758cfad62d0))
|
|
43
|
+
* **docs:** grammar improvements ([e31d3bb](https://github.com/spi-tch/spitch-python/commit/e31d3bbe8aa390cdbae514e16e31e45b3ee5a4d5))
|
|
44
|
+
* **docs:** remove reference to rye shell ([b7f1703](https://github.com/spi-tch/spitch-python/commit/b7f1703ce312c6733e2986d045568b003ac4bba1))
|
|
45
|
+
* **internal:** avoid errors for isinstance checks on proxies ([aaf12a5](https://github.com/spi-tch/spitch-python/commit/aaf12a5580a673adbf303bea88f958f384b0ebcd))
|
|
46
|
+
* **internal:** codegen related update ([fc70ee0](https://github.com/spi-tch/spitch-python/commit/fc70ee0bf2bb47f12ec36a35fad1b27d257897ce))
|
|
47
|
+
* **internal:** update conftest.py ([1b062cd](https://github.com/spi-tch/spitch-python/commit/1b062cdf9d6200399d24bceb73e31779a80be190))
|
|
48
|
+
* **tests:** add tests for httpx client instantiation & proxies ([a57f4cc](https://github.com/spi-tch/spitch-python/commit/a57f4cc2105706eb53c9483ac74e2ec4995bd757))
|
|
49
|
+
* **tests:** run tests in parallel ([da93455](https://github.com/spi-tch/spitch-python/commit/da93455365f885480cf4553ab138fc71eebd59f9))
|
|
50
|
+
|
|
3
51
|
## 1.26.0 (2025-04-23)
|
|
4
52
|
|
|
5
53
|
Full Changelog: [v1.25.0...v1.26.0](https://github.com/spi-tch/spitch-python/compare/v1.25.0...v1.26.0)
|
|
@@ -17,8 +17,7 @@ $ rye sync --all-features
|
|
|
17
17
|
You can then run scripts using `rye run python script.py` or by activating the virtual environment:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
|
|
21
|
-
# or manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work
|
|
20
|
+
# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work
|
|
22
21
|
$ source .venv/bin/activate
|
|
23
22
|
|
|
24
23
|
# now you can omit the `rye run` prefix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: spitch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.27.1
|
|
4
4
|
Summary: The official Python library for the spitch API
|
|
5
5
|
Project-URL: Homepage, https://github.com/spi-tch/spitch-python
|
|
6
6
|
Project-URL: Repository, https://github.com/spi-tch/spitch-python
|
|
@@ -32,7 +32,7 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# Spitch Python API library
|
|
34
34
|
|
|
35
|
-
[](https://pypi.org/project/spitch/)
|
|
35
|
+
[>)](https://pypi.org/project/spitch/)
|
|
36
36
|
|
|
37
37
|
The Spitch Python library provides convenient access to the Spitch REST API from any Python 3.8+
|
|
38
38
|
application. The library includes type definitions for all request params and response fields,
|
|
@@ -202,7 +202,7 @@ client.with_options(max_retries=5).speech.generate(
|
|
|
202
202
|
### Timeouts
|
|
203
203
|
|
|
204
204
|
By default requests time out after 1 minute. You can configure this with a `timeout` option,
|
|
205
|
-
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
|
|
205
|
+
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
|
|
206
206
|
|
|
207
207
|
```python
|
|
208
208
|
from spitch import Spitch
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Spitch Python API library
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/spitch/)
|
|
3
|
+
[>)](https://pypi.org/project/spitch/)
|
|
4
4
|
|
|
5
5
|
The Spitch Python library provides convenient access to the Spitch REST API from any Python 3.8+
|
|
6
6
|
application. The library includes type definitions for all request params and response fields,
|
|
@@ -170,7 +170,7 @@ client.with_options(max_retries=5).speech.generate(
|
|
|
170
170
|
### Timeouts
|
|
171
171
|
|
|
172
172
|
By default requests time out after 1 minute. You can configure this with a `timeout` option,
|
|
173
|
-
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/#fine-tuning-the-configuration) object:
|
|
173
|
+
which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object:
|
|
174
174
|
|
|
175
175
|
```python
|
|
176
176
|
from spitch import Spitch
|
|
@@ -16,11 +16,11 @@ before making any information public.
|
|
|
16
16
|
## Reporting Non-SDK Related Security Issues
|
|
17
17
|
|
|
18
18
|
If you encounter security issues that are not directly related to SDKs but pertain to the services
|
|
19
|
-
or products provided by Spitch please follow the respective company's security reporting guidelines.
|
|
19
|
+
or products provided by Spitch, please follow the respective company's security reporting guidelines.
|
|
20
20
|
|
|
21
21
|
### Spitch Terms and Policies
|
|
22
22
|
|
|
23
|
-
Please contact dev@spitch.app for any questions or concerns regarding security of our services.
|
|
23
|
+
Please contact dev@spitch.app for any questions or concerns regarding the security of our services.
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "spitch"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.27.1"
|
|
4
4
|
description = "The official Python library for the spitch API"
|
|
5
5
|
dynamic = ["readme"]
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -55,6 +55,7 @@ dev-dependencies = [
|
|
|
55
55
|
"importlib-metadata>=6.7.0",
|
|
56
56
|
"rich>=13.7.1",
|
|
57
57
|
"nest_asyncio==1.6.0",
|
|
58
|
+
"pytest-xdist>=3.6.1",
|
|
58
59
|
]
|
|
59
60
|
|
|
60
61
|
[tool.rye.scripts]
|
|
@@ -126,7 +127,7 @@ replacement = '[\1](https://github.com/spi-tch/spitch-python/tree/main/\g<2>)'
|
|
|
126
127
|
|
|
127
128
|
[tool.pytest.ini_options]
|
|
128
129
|
testpaths = ["tests"]
|
|
129
|
-
addopts = "--tb=short"
|
|
130
|
+
addopts = "--tb=short -n auto"
|
|
130
131
|
xfail_strict = true
|
|
131
132
|
asyncio_mode = "auto"
|
|
132
133
|
asyncio_default_fixture_loop_scope = "session"
|
|
@@ -30,6 +30,8 @@ distro==1.8.0
|
|
|
30
30
|
exceptiongroup==1.2.2
|
|
31
31
|
# via anyio
|
|
32
32
|
# via pytest
|
|
33
|
+
execnet==2.1.1
|
|
34
|
+
# via pytest-xdist
|
|
33
35
|
filelock==3.12.4
|
|
34
36
|
# via virtualenv
|
|
35
37
|
h11==0.14.0
|
|
@@ -72,7 +74,9 @@ pygments==2.18.0
|
|
|
72
74
|
pyright==1.1.399
|
|
73
75
|
pytest==8.3.3
|
|
74
76
|
# via pytest-asyncio
|
|
77
|
+
# via pytest-xdist
|
|
75
78
|
pytest-asyncio==0.24.0
|
|
79
|
+
pytest-xdist==3.7.0
|
|
76
80
|
python-dateutil==2.8.2
|
|
77
81
|
# via time-machine
|
|
78
82
|
pytz==2023.3.post1
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
|
|
2
2
|
|
|
3
|
+
import typing as _t
|
|
4
|
+
|
|
3
5
|
from . import types
|
|
4
6
|
from ._types import NOT_GIVEN, NoneType, NotGiven, Transport, ProxiesTypes
|
|
5
7
|
from ._utils import file_from_path
|
|
@@ -67,6 +69,9 @@ __all__ = [
|
|
|
67
69
|
"DefaultAsyncHttpxClient",
|
|
68
70
|
]
|
|
69
71
|
|
|
72
|
+
if not _t.TYPE_CHECKING:
|
|
73
|
+
from ._utils._resources_proxy import resources as resources
|
|
74
|
+
|
|
70
75
|
_setup_logging()
|
|
71
76
|
|
|
72
77
|
# Update the __module__ attribute for exported symbols so that
|
|
@@ -960,6 +960,9 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
|
|
|
960
960
|
if self.custom_auth is not None:
|
|
961
961
|
kwargs["auth"] = self.custom_auth
|
|
962
962
|
|
|
963
|
+
if options.follow_redirects is not None:
|
|
964
|
+
kwargs["follow_redirects"] = options.follow_redirects
|
|
965
|
+
|
|
963
966
|
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
|
|
964
967
|
|
|
965
968
|
response = None
|
|
@@ -1068,7 +1071,14 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
|
|
|
1068
1071
|
) -> ResponseT:
|
|
1069
1072
|
origin = get_origin(cast_to) or cast_to
|
|
1070
1073
|
|
|
1071
|
-
if
|
|
1074
|
+
if (
|
|
1075
|
+
inspect.isclass(origin)
|
|
1076
|
+
and issubclass(origin, BaseAPIResponse)
|
|
1077
|
+
# we only want to actually return the custom BaseAPIResponse class if we're
|
|
1078
|
+
# returning the raw response, or if we're not streaming SSE, as if we're streaming
|
|
1079
|
+
# SSE then `cast_to` doesn't actively reflect the type we need to parse into
|
|
1080
|
+
and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
|
|
1081
|
+
):
|
|
1072
1082
|
if not issubclass(origin, APIResponse):
|
|
1073
1083
|
raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}")
|
|
1074
1084
|
|
|
@@ -1464,6 +1474,9 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
|
|
|
1464
1474
|
if self.custom_auth is not None:
|
|
1465
1475
|
kwargs["auth"] = self.custom_auth
|
|
1466
1476
|
|
|
1477
|
+
if options.follow_redirects is not None:
|
|
1478
|
+
kwargs["follow_redirects"] = options.follow_redirects
|
|
1479
|
+
|
|
1467
1480
|
log.debug("Sending HTTP Request: %s %s", request.method, request.url)
|
|
1468
1481
|
|
|
1469
1482
|
response = None
|
|
@@ -1572,7 +1585,14 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
|
|
|
1572
1585
|
) -> ResponseT:
|
|
1573
1586
|
origin = get_origin(cast_to) or cast_to
|
|
1574
1587
|
|
|
1575
|
-
if
|
|
1588
|
+
if (
|
|
1589
|
+
inspect.isclass(origin)
|
|
1590
|
+
and issubclass(origin, BaseAPIResponse)
|
|
1591
|
+
# we only want to actually return the custom BaseAPIResponse class if we're
|
|
1592
|
+
# returning the raw response, or if we're not streaming SSE, as if we're streaming
|
|
1593
|
+
# SSE then `cast_to` doesn't actively reflect the type we need to parse into
|
|
1594
|
+
and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER)))
|
|
1595
|
+
):
|
|
1576
1596
|
if not issubclass(origin, AsyncAPIResponse):
|
|
1577
1597
|
raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}")
|
|
1578
1598
|
|
|
@@ -733,6 +733,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
|
|
|
733
733
|
idempotency_key: str
|
|
734
734
|
json_data: Body
|
|
735
735
|
extra_json: AnyMapping
|
|
736
|
+
follow_redirects: bool
|
|
736
737
|
|
|
737
738
|
|
|
738
739
|
@final
|
|
@@ -746,6 +747,7 @@ class FinalRequestOptions(pydantic.BaseModel):
|
|
|
746
747
|
files: Union[HttpxRequestFiles, None] = None
|
|
747
748
|
idempotency_key: Union[str, None] = None
|
|
748
749
|
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
|
|
750
|
+
follow_redirects: Union[bool, None] = None
|
|
749
751
|
|
|
750
752
|
# It should be noted that we cannot use `json` here as that would override
|
|
751
753
|
# a BaseModel method in an incompatible fashion.
|
|
@@ -227,7 +227,7 @@ class BaseAPIResponse(Generic[R]):
|
|
|
227
227
|
# split is required to handle cases where additional information is included
|
|
228
228
|
# in the response, e.g. application/json; charset=utf-8
|
|
229
229
|
content_type, *_ = response.headers.get("content-type", "*").split(";")
|
|
230
|
-
if content_type
|
|
230
|
+
if not content_type.endswith("json"):
|
|
231
231
|
if is_basemodel(cast_to):
|
|
232
232
|
try:
|
|
233
233
|
data = response.json()
|
|
@@ -100,6 +100,7 @@ class RequestOptions(TypedDict, total=False):
|
|
|
100
100
|
params: Query
|
|
101
101
|
extra_json: AnyMapping
|
|
102
102
|
idempotency_key: str
|
|
103
|
+
follow_redirects: bool
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
# Sentinel class used until PEP 0661 is accepted
|
|
@@ -217,3 +218,4 @@ class _GenericAlias(Protocol):
|
|
|
217
218
|
|
|
218
219
|
class HttpxSendArgs(TypedDict, total=False):
|
|
219
220
|
auth: httpx.Auth
|
|
221
|
+
follow_redirects: bool
|
|
@@ -46,7 +46,10 @@ class LazyProxy(Generic[T], ABC):
|
|
|
46
46
|
@property # type: ignore
|
|
47
47
|
@override
|
|
48
48
|
def __class__(self) -> type: # pyright: ignore
|
|
49
|
-
|
|
49
|
+
try:
|
|
50
|
+
proxied = self.__get_proxied__()
|
|
51
|
+
except Exception:
|
|
52
|
+
return type(self)
|
|
50
53
|
if issubclass(type(proxied), LazyProxy):
|
|
51
54
|
return type(proxied)
|
|
52
55
|
return proxied.__class__
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing_extensions import override
|
|
5
|
+
|
|
6
|
+
from ._proxy import LazyProxy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ResourcesProxy(LazyProxy[Any]):
|
|
10
|
+
"""A proxy for the `spitch.resources` module.
|
|
11
|
+
|
|
12
|
+
This is used so that we can lazily import `spitch.resources` only when
|
|
13
|
+
needed *and* so that users can just import `spitch` and reference `spitch.resources`
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@override
|
|
17
|
+
def __load__(self) -> Any:
|
|
18
|
+
import importlib
|
|
19
|
+
|
|
20
|
+
mod = importlib.import_module("spitch.resources")
|
|
21
|
+
return mod
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
resources = ResourcesProxy().__as_proxied__()
|
|
@@ -23,12 +23,16 @@ from pydantic import ValidationError
|
|
|
23
23
|
|
|
24
24
|
from spitch import Spitch, AsyncSpitch, APIResponseValidationError
|
|
25
25
|
from spitch._types import Omit
|
|
26
|
-
from spitch._utils import maybe_transform
|
|
27
26
|
from spitch._models import BaseModel, FinalRequestOptions
|
|
28
|
-
from spitch._constants import RAW_RESPONSE_HEADER
|
|
29
27
|
from spitch._exceptions import SpitchError, APIStatusError, APITimeoutError, APIResponseValidationError
|
|
30
|
-
from spitch._base_client import
|
|
31
|
-
|
|
28
|
+
from spitch._base_client import (
|
|
29
|
+
DEFAULT_TIMEOUT,
|
|
30
|
+
HTTPX_DEFAULT_TIMEOUT,
|
|
31
|
+
BaseClient,
|
|
32
|
+
DefaultHttpxClient,
|
|
33
|
+
DefaultAsyncHttpxClient,
|
|
34
|
+
make_request_options,
|
|
35
|
+
)
|
|
32
36
|
|
|
33
37
|
from .utils import update_env
|
|
34
38
|
|
|
@@ -706,36 +710,21 @@ class TestSpitch:
|
|
|
706
710
|
|
|
707
711
|
@mock.patch("spitch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
|
708
712
|
@pytest.mark.respx(base_url=base_url)
|
|
709
|
-
def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
|
|
713
|
+
def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Spitch) -> None:
|
|
710
714
|
respx_mock.post("/v1/speech").mock(side_effect=httpx.TimeoutException("Test timeout error"))
|
|
711
715
|
|
|
712
716
|
with pytest.raises(APITimeoutError):
|
|
713
|
-
|
|
714
|
-
"/v1/speech",
|
|
715
|
-
body=cast(
|
|
716
|
-
object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
|
|
717
|
-
),
|
|
718
|
-
cast_to=httpx.Response,
|
|
719
|
-
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
|
|
720
|
-
)
|
|
717
|
+
client.speech.with_streaming_response.generate(language="yo", text="text", voice="sade").__enter__()
|
|
721
718
|
|
|
722
719
|
assert _get_open_connections(self.client) == 0
|
|
723
720
|
|
|
724
721
|
@mock.patch("spitch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
|
725
722
|
@pytest.mark.respx(base_url=base_url)
|
|
726
|
-
def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
|
|
723
|
+
def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Spitch) -> None:
|
|
727
724
|
respx_mock.post("/v1/speech").mock(return_value=httpx.Response(500))
|
|
728
725
|
|
|
729
726
|
with pytest.raises(APIStatusError):
|
|
730
|
-
|
|
731
|
-
"/v1/speech",
|
|
732
|
-
body=cast(
|
|
733
|
-
object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
|
|
734
|
-
),
|
|
735
|
-
cast_to=httpx.Response,
|
|
736
|
-
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
|
|
737
|
-
)
|
|
738
|
-
|
|
727
|
+
client.speech.with_streaming_response.generate(language="yo", text="text", voice="sade").__enter__()
|
|
739
728
|
assert _get_open_connections(self.client) == 0
|
|
740
729
|
|
|
741
730
|
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
|
|
@@ -819,6 +808,55 @@ class TestSpitch:
|
|
|
819
808
|
|
|
820
809
|
assert response.http_request.headers.get("x-stainless-retry-count") == "42"
|
|
821
810
|
|
|
811
|
+
def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
812
|
+
# Test that the proxy environment variables are set correctly
|
|
813
|
+
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
|
|
814
|
+
|
|
815
|
+
client = DefaultHttpxClient()
|
|
816
|
+
|
|
817
|
+
mounts = tuple(client._mounts.items())
|
|
818
|
+
assert len(mounts) == 1
|
|
819
|
+
assert mounts[0][0].pattern == "https://"
|
|
820
|
+
|
|
821
|
+
@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
|
|
822
|
+
def test_default_client_creation(self) -> None:
|
|
823
|
+
# Ensure that the client can be initialized without any exceptions
|
|
824
|
+
DefaultHttpxClient(
|
|
825
|
+
verify=True,
|
|
826
|
+
cert=None,
|
|
827
|
+
trust_env=True,
|
|
828
|
+
http1=True,
|
|
829
|
+
http2=False,
|
|
830
|
+
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
@pytest.mark.respx(base_url=base_url)
|
|
834
|
+
def test_follow_redirects(self, respx_mock: MockRouter) -> None:
|
|
835
|
+
# Test that the default follow_redirects=True allows following redirects
|
|
836
|
+
respx_mock.post("/redirect").mock(
|
|
837
|
+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
|
838
|
+
)
|
|
839
|
+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
|
|
840
|
+
|
|
841
|
+
response = self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
|
|
842
|
+
assert response.status_code == 200
|
|
843
|
+
assert response.json() == {"status": "ok"}
|
|
844
|
+
|
|
845
|
+
@pytest.mark.respx(base_url=base_url)
|
|
846
|
+
def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
|
|
847
|
+
# Test that follow_redirects=False prevents following redirects
|
|
848
|
+
respx_mock.post("/redirect").mock(
|
|
849
|
+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
with pytest.raises(APIStatusError) as exc_info:
|
|
853
|
+
self.client.post(
|
|
854
|
+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
assert exc_info.value.response.status_code == 302
|
|
858
|
+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
|
|
859
|
+
|
|
822
860
|
|
|
823
861
|
class TestAsyncSpitch:
|
|
824
862
|
client = AsyncSpitch(base_url=base_url, api_key=api_key, _strict_response_validation=True)
|
|
@@ -1490,36 +1528,25 @@ class TestAsyncSpitch:
|
|
|
1490
1528
|
|
|
1491
1529
|
@mock.patch("spitch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
|
1492
1530
|
@pytest.mark.respx(base_url=base_url)
|
|
1493
|
-
async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
|
|
1531
|
+
async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncSpitch) -> None:
|
|
1494
1532
|
respx_mock.post("/v1/speech").mock(side_effect=httpx.TimeoutException("Test timeout error"))
|
|
1495
1533
|
|
|
1496
1534
|
with pytest.raises(APITimeoutError):
|
|
1497
|
-
await
|
|
1498
|
-
"
|
|
1499
|
-
|
|
1500
|
-
object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
|
|
1501
|
-
),
|
|
1502
|
-
cast_to=httpx.Response,
|
|
1503
|
-
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
|
|
1504
|
-
)
|
|
1535
|
+
await async_client.speech.with_streaming_response.generate(
|
|
1536
|
+
language="yo", text="text", voice="sade"
|
|
1537
|
+
).__aenter__()
|
|
1505
1538
|
|
|
1506
1539
|
assert _get_open_connections(self.client) == 0
|
|
1507
1540
|
|
|
1508
1541
|
@mock.patch("spitch._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout)
|
|
1509
1542
|
@pytest.mark.respx(base_url=base_url)
|
|
1510
|
-
async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter) -> None:
|
|
1543
|
+
async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncSpitch) -> None:
|
|
1511
1544
|
respx_mock.post("/v1/speech").mock(return_value=httpx.Response(500))
|
|
1512
1545
|
|
|
1513
1546
|
with pytest.raises(APIStatusError):
|
|
1514
|
-
await
|
|
1515
|
-
"
|
|
1516
|
-
|
|
1517
|
-
object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
|
|
1518
|
-
),
|
|
1519
|
-
cast_to=httpx.Response,
|
|
1520
|
-
options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
|
|
1521
|
-
)
|
|
1522
|
-
|
|
1547
|
+
await async_client.speech.with_streaming_response.generate(
|
|
1548
|
+
language="yo", text="text", voice="sade"
|
|
1549
|
+
).__aenter__()
|
|
1523
1550
|
assert _get_open_connections(self.client) == 0
|
|
1524
1551
|
|
|
1525
1552
|
@pytest.mark.parametrize("failures_before_success", [0, 2, 4])
|
|
@@ -1650,3 +1677,52 @@ class TestAsyncSpitch:
|
|
|
1650
1677
|
raise AssertionError("calling get_platform using asyncify resulted in a hung process")
|
|
1651
1678
|
|
|
1652
1679
|
time.sleep(0.1)
|
|
1680
|
+
|
|
1681
|
+
async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
1682
|
+
# Test that the proxy environment variables are set correctly
|
|
1683
|
+
monkeypatch.setenv("HTTPS_PROXY", "https://example.org")
|
|
1684
|
+
|
|
1685
|
+
client = DefaultAsyncHttpxClient()
|
|
1686
|
+
|
|
1687
|
+
mounts = tuple(client._mounts.items())
|
|
1688
|
+
assert len(mounts) == 1
|
|
1689
|
+
assert mounts[0][0].pattern == "https://"
|
|
1690
|
+
|
|
1691
|
+
@pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning")
|
|
1692
|
+
async def test_default_client_creation(self) -> None:
|
|
1693
|
+
# Ensure that the client can be initialized without any exceptions
|
|
1694
|
+
DefaultAsyncHttpxClient(
|
|
1695
|
+
verify=True,
|
|
1696
|
+
cert=None,
|
|
1697
|
+
trust_env=True,
|
|
1698
|
+
http1=True,
|
|
1699
|
+
http2=False,
|
|
1700
|
+
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
|
|
1701
|
+
)
|
|
1702
|
+
|
|
1703
|
+
@pytest.mark.respx(base_url=base_url)
|
|
1704
|
+
async def test_follow_redirects(self, respx_mock: MockRouter) -> None:
|
|
1705
|
+
# Test that the default follow_redirects=True allows following redirects
|
|
1706
|
+
respx_mock.post("/redirect").mock(
|
|
1707
|
+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
|
1708
|
+
)
|
|
1709
|
+
respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"}))
|
|
1710
|
+
|
|
1711
|
+
response = await self.client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response)
|
|
1712
|
+
assert response.status_code == 200
|
|
1713
|
+
assert response.json() == {"status": "ok"}
|
|
1714
|
+
|
|
1715
|
+
@pytest.mark.respx(base_url=base_url)
|
|
1716
|
+
async def test_follow_redirects_disabled(self, respx_mock: MockRouter) -> None:
|
|
1717
|
+
# Test that follow_redirects=False prevents following redirects
|
|
1718
|
+
respx_mock.post("/redirect").mock(
|
|
1719
|
+
return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"})
|
|
1720
|
+
)
|
|
1721
|
+
|
|
1722
|
+
with pytest.raises(APIStatusError) as exc_info:
|
|
1723
|
+
await self.client.post(
|
|
1724
|
+
"/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response
|
|
1725
|
+
)
|
|
1726
|
+
|
|
1727
|
+
assert exc_info.value.response.status_code == 302
|
|
1728
|
+
assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected"
|
|
@@ -21,3 +21,14 @@ def test_recursive_proxy() -> None:
|
|
|
21
21
|
assert dir(proxy) == []
|
|
22
22
|
assert type(proxy).__name__ == "RecursiveLazyProxy"
|
|
23
23
|
assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_isinstance_does_not_error() -> None:
|
|
27
|
+
class AlwaysErrorProxy(LazyProxy[Any]):
|
|
28
|
+
@override
|
|
29
|
+
def __load__(self) -> Any:
|
|
30
|
+
raise RuntimeError("Mocking missing dependency")
|
|
31
|
+
|
|
32
|
+
proxy = AlwaysErrorProxy()
|
|
33
|
+
assert not isinstance(proxy, dict)
|
|
34
|
+
assert isinstance(proxy, LazyProxy)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|