spitch 1.19.0__tar.gz → 1.21.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.

Potentially problematic release.


This version of spitch might be problematic. Click here for more details.

Files changed (75) hide show
  1. spitch-1.21.0/.release-please-manifest.json +3 -0
  2. {spitch-1.19.0 → spitch-1.21.0}/CHANGELOG.md +49 -0
  3. {spitch-1.19.0 → spitch-1.21.0}/PKG-INFO +34 -7
  4. {spitch-1.19.0 → spitch-1.21.0}/README.md +32 -5
  5. {spitch-1.19.0 → spitch-1.21.0}/SECURITY.md +3 -3
  6. {spitch-1.19.0 → spitch-1.21.0}/mypy.ini +1 -1
  7. {spitch-1.19.0 → spitch-1.21.0}/pyproject.toml +5 -4
  8. {spitch-1.19.0 → spitch-1.21.0}/requirements-dev.lock +4 -5
  9. {spitch-1.19.0 → spitch-1.21.0}/requirements.lock +1 -2
  10. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_base_client.py +25 -65
  11. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_client.py +2 -2
  12. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_constants.py +1 -1
  13. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_files.py +1 -1
  14. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_models.py +16 -7
  15. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_response.py +9 -3
  16. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_sync.py +17 -2
  17. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_transform.py +11 -1
  18. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_version.py +1 -1
  19. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/resources/speech.py +6 -2
  20. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/resources/text.py +2 -2
  21. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/speech_generate_params.py +2 -0
  22. {spitch-1.19.0 → spitch-1.21.0}/tests/test_client.py +36 -15
  23. {spitch-1.19.0 → spitch-1.21.0}/tests/test_transform.py +10 -1
  24. spitch-1.19.0/.release-please-manifest.json +0 -3
  25. {spitch-1.19.0 → spitch-1.21.0}/.gitignore +0 -0
  26. {spitch-1.19.0 → spitch-1.21.0}/CONTRIBUTING.md +0 -0
  27. {spitch-1.19.0 → spitch-1.21.0}/LICENSE +0 -0
  28. {spitch-1.19.0 → spitch-1.21.0}/api.md +0 -0
  29. {spitch-1.19.0 → spitch-1.21.0}/bin/check-release-environment +0 -0
  30. {spitch-1.19.0 → spitch-1.21.0}/bin/publish-pypi +0 -0
  31. {spitch-1.19.0 → spitch-1.21.0}/examples/.keep +0 -0
  32. {spitch-1.19.0 → spitch-1.21.0}/examples/example.py +0 -0
  33. {spitch-1.19.0 → spitch-1.21.0}/noxfile.py +0 -0
  34. {spitch-1.19.0 → spitch-1.21.0}/release-please-config.json +0 -0
  35. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/__init__.py +0 -0
  36. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_compat.py +0 -0
  37. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_exceptions.py +0 -0
  38. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_qs.py +0 -0
  39. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_resource.py +0 -0
  40. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_streaming.py +0 -0
  41. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_types.py +0 -0
  42. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/__init__.py +0 -0
  43. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_logs.py +0 -0
  44. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_proxy.py +0 -0
  45. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_reflection.py +0 -0
  46. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_streams.py +0 -0
  47. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_typing.py +0 -0
  48. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/_utils/_utils.py +0 -0
  49. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/lib/.keep +0 -0
  50. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/py.typed +0 -0
  51. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/resources/__init__.py +0 -0
  52. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/__init__.py +0 -0
  53. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/speech_transcribe_params.py +0 -0
  54. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/speech_transcribe_response.py +0 -0
  55. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/text_tone_mark_params.py +0 -0
  56. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/text_tone_mark_response.py +0 -0
  57. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/text_translate_params.py +0 -0
  58. {spitch-1.19.0 → spitch-1.21.0}/src/spitch/types/text_translate_response.py +0 -0
  59. {spitch-1.19.0 → spitch-1.21.0}/tests/__init__.py +0 -0
  60. {spitch-1.19.0 → spitch-1.21.0}/tests/api_resources/__init__.py +0 -0
  61. {spitch-1.19.0 → spitch-1.21.0}/tests/api_resources/test_speech.py +0 -0
  62. {spitch-1.19.0 → spitch-1.21.0}/tests/api_resources/test_text.py +0 -0
  63. {spitch-1.19.0 → spitch-1.21.0}/tests/conftest.py +0 -0
  64. {spitch-1.19.0 → spitch-1.21.0}/tests/sample_file.txt +0 -0
  65. {spitch-1.19.0 → spitch-1.21.0}/tests/test_deepcopy.py +0 -0
  66. {spitch-1.19.0 → spitch-1.21.0}/tests/test_extract_files.py +0 -0
  67. {spitch-1.19.0 → spitch-1.21.0}/tests/test_files.py +0 -0
  68. {spitch-1.19.0 → spitch-1.21.0}/tests/test_models.py +0 -0
  69. {spitch-1.19.0 → spitch-1.21.0}/tests/test_qs.py +0 -0
  70. {spitch-1.19.0 → spitch-1.21.0}/tests/test_required_args.py +0 -0
  71. {spitch-1.19.0 → spitch-1.21.0}/tests/test_response.py +0 -0
  72. {spitch-1.19.0 → spitch-1.21.0}/tests/test_streaming.py +0 -0
  73. {spitch-1.19.0 → spitch-1.21.0}/tests/test_utils/test_proxy.py +0 -0
  74. {spitch-1.19.0 → spitch-1.21.0}/tests/test_utils/test_typing.py +0 -0
  75. {spitch-1.19.0 → spitch-1.21.0}/tests/utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "1.21.0"
3
+ }
@@ -1,5 +1,54 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.21.0 (2025-03-04)
4
+
5
+ Full Changelog: [v1.20.0...v1.21.0](https://github.com/spi-tch/spitch-python/compare/v1.20.0...v1.21.0)
6
+
7
+ ### Features
8
+
9
+ * **api:** update via SDK Studio ([#114](https://github.com/spi-tch/spitch-python/issues/114)) ([2cb4f31](https://github.com/spi-tch/spitch-python/commit/2cb4f316c23ec0ea1ca0388681e1b0ae20d8d160))
10
+
11
+ ## 1.20.0 (2025-03-04)
12
+
13
+ Full Changelog: [v1.19.0...v1.20.0](https://github.com/spi-tch/spitch-python/compare/v1.19.0...v1.20.0)
14
+
15
+ ### Features
16
+
17
+ * **api:** update via SDK Studio ([#109](https://github.com/spi-tch/spitch-python/issues/109)) ([6e29edd](https://github.com/spi-tch/spitch-python/commit/6e29eddfb89bf0512bd87cd712c29ba19e145b7a))
18
+ * **client:** allow passing `NotGiven` for body ([#103](https://github.com/spi-tch/spitch-python/issues/103)) ([6f2bdd1](https://github.com/spi-tch/spitch-python/commit/6f2bdd15f3f6089eaf79d0841cd013258c19eaa5))
19
+ * **client:** send `X-Stainless-Read-Timeout` header ([#97](https://github.com/spi-tch/spitch-python/issues/97)) ([5aa6876](https://github.com/spi-tch/spitch-python/commit/5aa687627fa5a4d9815b865144b084a70401205d))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * asyncify on non-asyncio runtimes ([#101](https://github.com/spi-tch/spitch-python/issues/101)) ([06e16f9](https://github.com/spi-tch/spitch-python/commit/06e16f920106f8f7f79ed13e9a5fe173c982381b))
25
+ * **client:** mark some request bodies as optional ([6f2bdd1](https://github.com/spi-tch/spitch-python/commit/6f2bdd15f3f6089eaf79d0841cd013258c19eaa5))
26
+ * **tests:** make test_get_platform less flaky ([#91](https://github.com/spi-tch/spitch-python/issues/91)) ([f0263dc](https://github.com/spi-tch/spitch-python/commit/f0263dc026f74d0f9a94857b084075fba05e1c78))
27
+
28
+
29
+ ### Chores
30
+
31
+ * **docs:** update client docstring ([#107](https://github.com/spi-tch/spitch-python/issues/107)) ([c08016e](https://github.com/spi-tch/spitch-python/commit/c08016e27757afb2408b0a60a3b91d59ab9cd674))
32
+ * **internal:** avoid pytest-asyncio deprecation warning ([#92](https://github.com/spi-tch/spitch-python/issues/92)) ([2a58481](https://github.com/spi-tch/spitch-python/commit/2a58481308d998c02cfb80723802b3d3d3508f11))
33
+ * **internal:** bummp ruff dependency ([#96](https://github.com/spi-tch/spitch-python/issues/96)) ([bac43d8](https://github.com/spi-tch/spitch-python/commit/bac43d88bf6c06fbc207149bc877bf4eb2b012b7))
34
+ * **internal:** change default timeout to an int ([#95](https://github.com/spi-tch/spitch-python/issues/95)) ([148bc16](https://github.com/spi-tch/spitch-python/commit/148bc16f85dcd9f8baea266a61a7aeb868858c97))
35
+ * **internal:** codegen related update ([#102](https://github.com/spi-tch/spitch-python/issues/102)) ([cca3a53](https://github.com/spi-tch/spitch-python/commit/cca3a532412587cc3fe38bfe565102b661152045))
36
+ * **internal:** codegen related update ([#88](https://github.com/spi-tch/spitch-python/issues/88)) ([3c27c0e](https://github.com/spi-tch/spitch-python/commit/3c27c0ecabaf2c48c43d941d52365375650c6a83))
37
+ * **internal:** codegen related update ([#93](https://github.com/spi-tch/spitch-python/issues/93)) ([9564863](https://github.com/spi-tch/spitch-python/commit/95648632bcbe45c844531fd591f3785df20109e2))
38
+ * **internal:** fix devcontainers setup ([#104](https://github.com/spi-tch/spitch-python/issues/104)) ([10214c2](https://github.com/spi-tch/spitch-python/commit/10214c2803d80f734c1aa4ebe5338bbf1b98a6ef))
39
+ * **internal:** fix type traversing dictionary params ([#98](https://github.com/spi-tch/spitch-python/issues/98)) ([2d78d6d](https://github.com/spi-tch/spitch-python/commit/2d78d6d92183b5487fe5518f84107e1705f1d178))
40
+ * **internal:** minor formatting changes ([#94](https://github.com/spi-tch/spitch-python/issues/94)) ([e3edcec](https://github.com/spi-tch/spitch-python/commit/e3edcecc0ba046c41e5d1aa0bdb7550278fb45c3))
41
+ * **internal:** minor type handling changes ([#99](https://github.com/spi-tch/spitch-python/issues/99)) ([9a472ea](https://github.com/spi-tch/spitch-python/commit/9a472ea5ad908b7ddf9944f9e8bf0a2b49710cbb))
42
+ * **internal:** properly set __pydantic_private__ ([#105](https://github.com/spi-tch/spitch-python/issues/105)) ([13df700](https://github.com/spi-tch/spitch-python/commit/13df700ecf4651a18b84757bd5602030f9ae8c35))
43
+ * **internal:** remove unused http client options forwarding ([#108](https://github.com/spi-tch/spitch-python/issues/108)) ([5e96250](https://github.com/spi-tch/spitch-python/commit/5e96250dc6b2cbfe81cda0f663324df6305ee144))
44
+ * **internal:** update client tests ([#100](https://github.com/spi-tch/spitch-python/issues/100)) ([53adf81](https://github.com/spi-tch/spitch-python/commit/53adf817e1d1df6ad125ab6a05c6c3c753367d1c))
45
+
46
+
47
+ ### Documentation
48
+
49
+ * **raw responses:** fix duplicate `the` ([#90](https://github.com/spi-tch/spitch-python/issues/90)) ([ad3e50f](https://github.com/spi-tch/spitch-python/commit/ad3e50fb36232cc0ecab6c4eb1f61176319b46a5))
50
+ * update URLs from stainlessapi.com to stainless.com ([#106](https://github.com/spi-tch/spitch-python/issues/106)) ([b7bedc2](https://github.com/spi-tch/spitch-python/commit/b7bedc2b02e508d317e57ba74c6396e2f8c2aa88))
51
+
3
52
  ## 1.19.0 (2025-01-14)
4
53
 
5
54
  Full Changelog: [v1.18.0...v1.19.0](https://github.com/spi-tch/spitch-python/compare/v1.18.0...v1.19.0)
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spitch
3
- Version: 1.19.0
3
+ Version: 1.21.0
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
7
- Author-email: Spitch <dev@spi-tch.com>
7
+ Author-email: Spitch <hello@spi-tch.com>
8
8
  License-Expression: Apache-2.0
9
9
  License-File: LICENSE
10
10
  Classifier: Intended Audience :: Developers
@@ -39,7 +39,7 @@ The Spitch Python library provides convenient access to the Spitch REST API from
39
39
  application. The library includes type definitions for all request params and response fields,
40
40
  and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
41
41
 
42
- It is generated with [Stainless](https://www.stainlessapi.com/).
42
+ It is generated with [Stainless](https://www.stainless.com/).
43
43
 
44
44
  ## Documentation
45
45
 
@@ -112,6 +112,24 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
112
112
 
113
113
  Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
114
114
 
115
+ ## File uploads
116
+
117
+ Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
118
+
119
+ ```python
120
+ from pathlib import Path
121
+ from spitch import Spitch
122
+
123
+ client = Spitch()
124
+
125
+ client.speech.transcribe(
126
+ language="yo",
127
+ content=Path("/path/to/file"),
128
+ )
129
+ ```
130
+
131
+ The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically.
132
+
115
133
  ## Handling errors
116
134
 
117
135
  When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `spitch.APIConnectionError` is raised.
@@ -144,7 +162,7 @@ except spitch.APIStatusError as e:
144
162
  print(e.response)
145
163
  ```
146
164
 
147
- Error codes are as followed:
165
+ Error codes are as follows:
148
166
 
149
167
  | Status Code | Error Type |
150
168
  | ----------- | -------------------------- |
@@ -289,8 +307,7 @@ If you need to access undocumented endpoints, params, or response properties, th
289
307
  #### Undocumented endpoints
290
308
 
291
309
  To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other
292
- http verbs. Options on the client will be respected (such as retries) will be respected when making this
293
- request.
310
+ http verbs. Options on the client will be respected (such as retries) when making this request.
294
311
 
295
312
  ```py
296
313
  import httpx
@@ -346,12 +363,22 @@ client.with_options(http_client=DefaultHttpxClient(...))
346
363
 
347
364
  By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
348
365
 
366
+ ```py
367
+ from spitch import Spitch
368
+
369
+ with Spitch() as client:
370
+ # make requests here
371
+ ...
372
+
373
+ # HTTP client is now closed
374
+ ```
375
+
349
376
  ## Versioning
350
377
 
351
378
  This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
352
379
 
353
380
  1. Changes that only affect static types, without breaking runtime behavior.
354
- 2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
381
+ 2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
355
382
  3. Changes that we do not expect to impact the vast majority of users in practice.
356
383
 
357
384
  We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
@@ -6,7 +6,7 @@ The Spitch Python library provides convenient access to the Spitch REST API from
6
6
  application. The library includes type definitions for all request params and response fields,
7
7
  and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx).
8
8
 
9
- It is generated with [Stainless](https://www.stainlessapi.com/).
9
+ It is generated with [Stainless](https://www.stainless.com/).
10
10
 
11
11
  ## Documentation
12
12
 
@@ -79,6 +79,24 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
79
79
 
80
80
  Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
81
81
 
82
+ ## File uploads
83
+
84
+ Request parameters that correspond to file uploads can be passed as `bytes`, a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
85
+
86
+ ```python
87
+ from pathlib import Path
88
+ from spitch import Spitch
89
+
90
+ client = Spitch()
91
+
92
+ client.speech.transcribe(
93
+ language="yo",
94
+ content=Path("/path/to/file"),
95
+ )
96
+ ```
97
+
98
+ The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically.
99
+
82
100
  ## Handling errors
83
101
 
84
102
  When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `spitch.APIConnectionError` is raised.
@@ -111,7 +129,7 @@ except spitch.APIStatusError as e:
111
129
  print(e.response)
112
130
  ```
113
131
 
114
- Error codes are as followed:
132
+ Error codes are as follows:
115
133
 
116
134
  | Status Code | Error Type |
117
135
  | ----------- | -------------------------- |
@@ -256,8 +274,7 @@ If you need to access undocumented endpoints, params, or response properties, th
256
274
  #### Undocumented endpoints
257
275
 
258
276
  To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other
259
- http verbs. Options on the client will be respected (such as retries) will be respected when making this
260
- request.
277
+ http verbs. Options on the client will be respected (such as retries) when making this request.
261
278
 
262
279
  ```py
263
280
  import httpx
@@ -313,12 +330,22 @@ client.with_options(http_client=DefaultHttpxClient(...))
313
330
 
314
331
  By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
315
332
 
333
+ ```py
334
+ from spitch import Spitch
335
+
336
+ with Spitch() as client:
337
+ # make requests here
338
+ ...
339
+
340
+ # HTTP client is now closed
341
+ ```
342
+
316
343
  ## Versioning
317
344
 
318
345
  This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
319
346
 
320
347
  1. Changes that only affect static types, without breaking runtime behavior.
321
- 2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
348
+ 2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
322
349
  3. Changes that we do not expect to impact the vast majority of users in practice.
323
350
 
324
351
  We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
@@ -2,9 +2,9 @@
2
2
 
3
3
  ## Reporting Security Issues
4
4
 
5
- This SDK is generated by [Stainless Software Inc](http://stainlessapi.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
5
+ This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
6
6
 
7
- To report a security issue, please contact the Stainless team at security@stainlessapi.com.
7
+ To report a security issue, please contact the Stainless team at security@stainless.com.
8
8
 
9
9
  ## Responsible Disclosure
10
10
 
@@ -20,7 +20,7 @@ or products provided by Spitch please follow the respective company's security r
20
20
 
21
21
  ### Spitch Terms and Policies
22
22
 
23
- Please contact dev@spi-tch.com for any questions or concerns regarding security of our services.
23
+ Please contact hello@spi-tch.com for any questions or concerns regarding security of our services.
24
24
 
25
25
  ---
26
26
 
@@ -38,7 +38,7 @@ cache_fine_grained = True
38
38
  # ```
39
39
  # Changing this codegen to make mypy happy would increase complexity
40
40
  # and would not be worth it.
41
- disable_error_code = func-returns-value
41
+ disable_error_code = func-returns-value,overload-cannot-match
42
42
 
43
43
  # https://github.com/python/mypy/issues/12162
44
44
  [mypy.overrides]
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "spitch"
3
- version = "1.19.0"
3
+ version = "1.21.0"
4
4
  description = "The official Python library for the spitch API"
5
5
  dynamic = ["readme"]
6
6
  license = "Apache-2.0"
7
7
  authors = [
8
- { name = "Spitch", email = "dev@spi-tch.com" },
8
+ { name = "Spitch", email = "hello@spi-tch.com" },
9
9
  ]
10
10
  dependencies = [
11
11
  "httpx>=0.23.0, <0.28.0",
@@ -55,7 +55,7 @@ dev-dependencies = [
55
55
  "dirty-equals>=0.6.0",
56
56
  "importlib-metadata>=6.7.0",
57
57
  "rich>=13.7.1",
58
- "nest_asyncio==1.6.0"
58
+ "nest_asyncio==1.6.0",
59
59
  ]
60
60
 
61
61
  [tool.rye.scripts]
@@ -130,6 +130,7 @@ testpaths = ["tests"]
130
130
  addopts = "--tb=short"
131
131
  xfail_strict = true
132
132
  asyncio_mode = "auto"
133
+ asyncio_default_fixture_loop_scope = "session"
133
134
  filterwarnings = [
134
135
  "error"
135
136
  ]
@@ -177,7 +178,7 @@ select = [
177
178
  "T201",
178
179
  "T203",
179
180
  # misuse of typing.TYPE_CHECKING
180
- "TCH004",
181
+ "TC004",
181
182
  # import rules
182
183
  "TID251",
183
184
  ]
@@ -35,7 +35,7 @@ h11==0.14.0
35
35
  # via httpcore
36
36
  httpcore==1.0.2
37
37
  # via httpx
38
- httpx==0.25.2
38
+ httpx==0.28.1
39
39
  # via respx
40
40
  # via spitch
41
41
  idna==3.4
@@ -48,7 +48,7 @@ markdown-it-py==3.0.0
48
48
  # via rich
49
49
  mdurl==0.1.2
50
50
  # via markdown-it-py
51
- mypy==1.13.0
51
+ mypy==1.14.1
52
52
  mypy-extensions==1.0.0
53
53
  # via mypy
54
54
  nest-asyncio==1.6.0
@@ -76,16 +76,15 @@ python-dateutil==2.8.2
76
76
  # via time-machine
77
77
  pytz==2023.3.post1
78
78
  # via dirty-equals
79
- respx==0.20.2
79
+ respx==0.22.0
80
80
  rich==13.7.1
81
- ruff==0.6.9
81
+ ruff==0.9.4
82
82
  setuptools==68.2.2
83
83
  # via nodeenv
84
84
  six==1.16.0
85
85
  # via python-dateutil
86
86
  sniffio==1.3.0
87
87
  # via anyio
88
- # via httpx
89
88
  # via spitch
90
89
  time-machine==2.9.0
91
90
  tomli==2.0.2
@@ -25,7 +25,7 @@ h11==0.14.0
25
25
  # via httpcore
26
26
  httpcore==1.0.2
27
27
  # via httpx
28
- httpx==0.25.2
28
+ httpx==0.28.1
29
29
  # via spitch
30
30
  idna==3.4
31
31
  # via anyio
@@ -36,7 +36,6 @@ pydantic-core==2.23.4
36
36
  # via pydantic
37
37
  sniffio==1.3.0
38
38
  # via anyio
39
- # via httpx
40
39
  # via spitch
41
40
  typing-extensions==4.12.2
42
41
  # via anyio
@@ -9,7 +9,6 @@ import asyncio
9
9
  import inspect
10
10
  import logging
11
11
  import platform
12
- import warnings
13
12
  import email.utils
14
13
  from types import TracebackType
15
14
  from random import random
@@ -36,7 +35,7 @@ import anyio
36
35
  import httpx
37
36
  import distro
38
37
  import pydantic
39
- from httpx import URL, Limits
38
+ from httpx import URL
40
39
  from pydantic import PrivateAttr
41
40
 
42
41
  from . import _exceptions
@@ -51,19 +50,16 @@ from ._types import (
51
50
  Timeout,
52
51
  NotGiven,
53
52
  ResponseT,
54
- Transport,
55
53
  AnyMapping,
56
54
  PostParser,
57
- ProxiesTypes,
58
55
  RequestFiles,
59
56
  HttpxSendArgs,
60
- AsyncTransport,
61
57
  RequestOptions,
62
58
  HttpxRequestFiles,
63
59
  ModelBuilderProtocol,
64
60
  )
65
61
  from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
66
- from ._compat import model_copy, model_dump
62
+ from ._compat import PYDANTIC_V2, model_copy, model_dump
67
63
  from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
68
64
  from ._response import (
69
65
  APIResponse,
@@ -207,6 +203,9 @@ class BaseSyncPage(BasePage[_T], Generic[_T]):
207
203
  model: Type[_T],
208
204
  options: FinalRequestOptions,
209
205
  ) -> None:
206
+ if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
207
+ self.__pydantic_private__ = {}
208
+
210
209
  self._model = model
211
210
  self._client = client
212
211
  self._options = options
@@ -292,6 +291,9 @@ class BaseAsyncPage(BasePage[_T], Generic[_T]):
292
291
  client: AsyncAPIClient,
293
292
  options: FinalRequestOptions,
294
293
  ) -> None:
294
+ if PYDANTIC_V2 and getattr(self, "__pydantic_private__", None) is None:
295
+ self.__pydantic_private__ = {}
296
+
295
297
  self._model = model
296
298
  self._client = client
297
299
  self._options = options
@@ -331,9 +333,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
331
333
  _base_url: URL
332
334
  max_retries: int
333
335
  timeout: Union[float, Timeout, None]
334
- _limits: httpx.Limits
335
- _proxies: ProxiesTypes | None
336
- _transport: Transport | AsyncTransport | None
337
336
  _strict_response_validation: bool
338
337
  _idempotency_header: str | None
339
338
  _default_stream_cls: type[_DefaultStreamT] | None = None
@@ -346,9 +345,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
346
345
  _strict_response_validation: bool,
347
346
  max_retries: int = DEFAULT_MAX_RETRIES,
348
347
  timeout: float | Timeout | None = DEFAULT_TIMEOUT,
349
- limits: httpx.Limits,
350
- transport: Transport | AsyncTransport | None,
351
- proxies: ProxiesTypes | None,
352
348
  custom_headers: Mapping[str, str] | None = None,
353
349
  custom_query: Mapping[str, object] | None = None,
354
350
  ) -> None:
@@ -356,9 +352,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
356
352
  self._base_url = self._enforce_trailing_slash(URL(base_url))
357
353
  self.max_retries = max_retries
358
354
  self.timeout = timeout
359
- self._limits = limits
360
- self._proxies = proxies
361
- self._transport = transport
362
355
  self._custom_headers = custom_headers or {}
363
356
  self._custom_query = custom_query or {}
364
357
  self._strict_response_validation = _strict_response_validation
@@ -418,10 +411,17 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
418
411
  if idempotency_header and options.method.lower() != "get" and idempotency_header not in headers:
419
412
  headers[idempotency_header] = options.idempotency_key or self._idempotency_key()
420
413
 
421
- # Don't set the retry count header if it was already set or removed by the caller. We check
414
+ # Don't set these headers if they were already set or removed by the caller. We check
422
415
  # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case.
423
- if "x-stainless-retry-count" not in (header.lower() for header in custom_headers):
416
+ lower_custom_headers = [header.lower() for header in custom_headers]
417
+ if "x-stainless-retry-count" not in lower_custom_headers:
424
418
  headers["x-stainless-retry-count"] = str(retries_taken)
419
+ if "x-stainless-read-timeout" not in lower_custom_headers:
420
+ timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout
421
+ if isinstance(timeout, Timeout):
422
+ timeout = timeout.read
423
+ if timeout is not None:
424
+ headers["x-stainless-read-timeout"] = str(timeout)
425
425
 
426
426
  return headers
427
427
 
@@ -511,7 +511,7 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
511
511
  # so that passing a `TypedDict` doesn't cause an error.
512
512
  # https://github.com/microsoft/pyright/issues/3526#event-6715453066
513
513
  params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
514
- json=json_data,
514
+ json=json_data if is_given(json_data) else None,
515
515
  files=files,
516
516
  **kwargs,
517
517
  )
@@ -767,6 +767,9 @@ else:
767
767
 
768
768
  class SyncHttpxClientWrapper(DefaultHttpxClient):
769
769
  def __del__(self) -> None:
770
+ if self.is_closed:
771
+ return
772
+
770
773
  try:
771
774
  self.close()
772
775
  except Exception:
@@ -784,43 +787,11 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
784
787
  base_url: str | URL,
785
788
  max_retries: int = DEFAULT_MAX_RETRIES,
786
789
  timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
787
- transport: Transport | None = None,
788
- proxies: ProxiesTypes | None = None,
789
- limits: Limits | None = None,
790
790
  http_client: httpx.Client | None = None,
791
791
  custom_headers: Mapping[str, str] | None = None,
792
792
  custom_query: Mapping[str, object] | None = None,
793
793
  _strict_response_validation: bool,
794
794
  ) -> None:
795
- if limits is not None:
796
- warnings.warn(
797
- "The `connection_pool_limits` argument is deprecated. The `http_client` argument should be passed instead",
798
- category=DeprecationWarning,
799
- stacklevel=3,
800
- )
801
- if http_client is not None:
802
- raise ValueError("The `http_client` argument is mutually exclusive with `connection_pool_limits`")
803
- else:
804
- limits = DEFAULT_CONNECTION_LIMITS
805
-
806
- if transport is not None:
807
- warnings.warn(
808
- "The `transport` argument is deprecated. The `http_client` argument should be passed instead",
809
- category=DeprecationWarning,
810
- stacklevel=3,
811
- )
812
- if http_client is not None:
813
- raise ValueError("The `http_client` argument is mutually exclusive with `transport`")
814
-
815
- if proxies is not None:
816
- warnings.warn(
817
- "The `proxies` argument is deprecated. The `http_client` argument should be passed instead",
818
- category=DeprecationWarning,
819
- stacklevel=3,
820
- )
821
- if http_client is not None:
822
- raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
823
-
824
795
  if not is_given(timeout):
825
796
  # if the user passed in a custom http client with a non-default
826
797
  # timeout set then we use that timeout.
@@ -841,12 +812,9 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
841
812
 
842
813
  super().__init__(
843
814
  version=version,
844
- limits=limits,
845
815
  # cast to a valid type because mypy doesn't understand our type narrowing
846
816
  timeout=cast(Timeout, timeout),
847
- proxies=proxies,
848
817
  base_url=base_url,
849
- transport=transport,
850
818
  max_retries=max_retries,
851
819
  custom_query=custom_query,
852
820
  custom_headers=custom_headers,
@@ -855,11 +823,7 @@ class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]):
855
823
  self._client = http_client or SyncHttpxClientWrapper(
856
824
  base_url=base_url,
857
825
  # cast to a valid type because mypy doesn't understand our type narrowing
858
- timeout=cast(Timeout, timeout),
859
- proxies=proxies,
860
- transport=transport,
861
- limits=limits,
862
- follow_redirects=True,
826
+ timeout=cast(Timeout, timeout)
863
827
  )
864
828
 
865
829
  def is_closed(self) -> bool:
@@ -1332,6 +1296,9 @@ else:
1332
1296
 
1333
1297
  class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):
1334
1298
  def __del__(self) -> None:
1299
+ if self.is_closed:
1300
+ return
1301
+
1335
1302
  try:
1336
1303
  # TODO(someday): support non asyncio runtimes here
1337
1304
  asyncio.get_running_loop().create_task(self.aclose())
@@ -1351,9 +1318,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1351
1318
  _strict_response_validation: bool,
1352
1319
  max_retries: int = DEFAULT_MAX_RETRIES,
1353
1320
  timeout: float | Timeout | None | NotGiven = NOT_GIVEN,
1354
- transport: AsyncTransport | None = None,
1355
- proxies: ProxiesTypes | None = None,
1356
- limits: Limits | None = None,
1357
1321
  http_client: httpx.AsyncClient | None = None,
1358
1322
  custom_headers: Mapping[str, str] | None = None,
1359
1323
  custom_query: Mapping[str, object] | None = None,
@@ -1386,7 +1350,6 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1386
1350
  )
1387
1351
  if http_client is not None:
1388
1352
  raise ValueError("The `http_client` argument is mutually exclusive with `proxies`")
1389
-
1390
1353
  if not is_given(timeout):
1391
1354
  # if the user passed in a custom http client with a non-default
1392
1355
  # timeout set then we use that timeout.
@@ -1408,11 +1371,8 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1408
1371
  super().__init__(
1409
1372
  version=version,
1410
1373
  base_url=base_url,
1411
- limits=limits,
1412
1374
  # cast to a valid type because mypy doesn't understand our type narrowing
1413
1375
  timeout=cast(Timeout, timeout),
1414
- proxies=proxies,
1415
- transport=transport,
1416
1376
  max_retries=max_retries,
1417
1377
  custom_query=custom_query,
1418
1378
  custom_headers=custom_headers,
@@ -77,7 +77,7 @@ class Spitch(SyncAPIClient):
77
77
  # part of our public interface in the future.
78
78
  _strict_response_validation: bool = False,
79
79
  ) -> None:
80
- """Construct a new synchronous spitch client instance.
80
+ """Construct a new synchronous Spitch client instance.
81
81
 
82
82
  This automatically infers the `api_key` argument from the `SPITCH_API_KEY` environment variable if it is not provided.
83
83
  """
@@ -247,7 +247,7 @@ class AsyncSpitch(AsyncAPIClient):
247
247
  # part of our public interface in the future.
248
248
  _strict_response_validation: bool = False,
249
249
  ) -> None:
250
- """Construct a new async spitch client instance.
250
+ """Construct a new async AsyncSpitch client instance.
251
251
 
252
252
  This automatically infers the `api_key` argument from the `SPITCH_API_KEY` environment variable if it is not provided.
253
253
  """
@@ -6,7 +6,7 @@ RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response"
6
6
  OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to"
7
7
 
8
8
  # default timeout is 1 minute
9
- DEFAULT_TIMEOUT = httpx.Timeout(timeout=60.0, connect=5.0)
9
+ DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0)
10
10
  DEFAULT_MAX_RETRIES = 2
11
11
  DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20)
12
12
 
@@ -34,7 +34,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None:
34
34
  if not is_file_content(obj):
35
35
  prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`"
36
36
  raise RuntimeError(
37
- f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead."
37
+ f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/spi-tch/spitch-python/tree/main#file-uploads"
38
38
  ) from None
39
39
 
40
40
 
@@ -171,21 +171,21 @@ class BaseModel(pydantic.BaseModel):
171
171
  @override
172
172
  def __str__(self) -> str:
173
173
  # mypy complains about an invalid self arg
174
- return f'{self.__repr_name__()}({self.__repr_str__(", ")})' # type: ignore[misc]
174
+ return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc]
175
175
 
176
176
  # Override the 'construct' method in a way that supports recursive parsing without validation.
177
177
  # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836.
178
178
  @classmethod
179
179
  @override
180
180
  def construct( # pyright: ignore[reportIncompatibleMethodOverride]
181
- cls: Type[ModelT],
181
+ __cls: Type[ModelT],
182
182
  _fields_set: set[str] | None = None,
183
183
  **values: object,
184
184
  ) -> ModelT:
185
- m = cls.__new__(cls)
185
+ m = __cls.__new__(__cls)
186
186
  fields_values: dict[str, object] = {}
187
187
 
188
- config = get_model_config(cls)
188
+ config = get_model_config(__cls)
189
189
  populate_by_name = (
190
190
  config.allow_population_by_field_name
191
191
  if isinstance(config, _ConfigProtocol)
@@ -195,7 +195,7 @@ class BaseModel(pydantic.BaseModel):
195
195
  if _fields_set is None:
196
196
  _fields_set = set()
197
197
 
198
- model_fields = get_model_fields(cls)
198
+ model_fields = get_model_fields(__cls)
199
199
  for name, field in model_fields.items():
200
200
  key = field.alias
201
201
  if key is None or (key not in values and populate_by_name):
@@ -425,6 +425,11 @@ def construct_type(*, value: object, type_: object) -> object:
425
425
 
426
426
  If the given value does not match the expected type then it is returned as-is.
427
427
  """
428
+
429
+ # store a reference to the original type we were given before we extract any inner
430
+ # types so that we can properly resolve forward references in `TypeAliasType` annotations
431
+ original_type = None
432
+
428
433
  # we allow `object` as the input type because otherwise, passing things like
429
434
  # `Literal['value']` will be reported as a type error by type checkers
430
435
  type_ = cast("type[object]", type_)
@@ -443,7 +448,7 @@ def construct_type(*, value: object, type_: object) -> object:
443
448
 
444
449
  if is_union(origin):
445
450
  try:
446
- return validate_type(type_=cast("type[object]", type_), value=value)
451
+ return validate_type(type_=cast("type[object]", original_type or type_), value=value)
447
452
  except Exception:
448
453
  pass
449
454
 
@@ -485,7 +490,11 @@ def construct_type(*, value: object, type_: object) -> object:
485
490
  _, items_type = get_args(type_) # Dict[_, items_type]
486
491
  return {key: construct_type(value=item, type_=items_type) for key, item in value.items()}
487
492
 
488
- if not is_literal_type(type_) and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)):
493
+ if (
494
+ not is_literal_type(type_)
495
+ and inspect.isclass(origin)
496
+ and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel))
497
+ ):
489
498
  if is_list(value):
490
499
  return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value]
491
500
 
@@ -130,6 +130,8 @@ class BaseAPIResponse(Generic[R]):
130
130
  if to and is_annotated_type(to):
131
131
  to = extract_type_arg(to, 0)
132
132
 
133
+ origin = get_origin(cast_to) or cast_to
134
+
133
135
  if self._is_sse_stream:
134
136
  if to:
135
137
  if not is_stream_class_type(to):
@@ -195,8 +197,6 @@ class BaseAPIResponse(Generic[R]):
195
197
  if cast_to == bool:
196
198
  return cast(R, response.text.lower() == "true")
197
199
 
198
- origin = get_origin(cast_to) or cast_to
199
-
200
200
  if origin == APIResponse:
201
201
  raise RuntimeError("Unexpected state - cast_to is `APIResponse`")
202
202
 
@@ -210,7 +210,13 @@ class BaseAPIResponse(Generic[R]):
210
210
  raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`")
211
211
  return cast(R, response)
212
212
 
213
- if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel):
213
+ if (
214
+ inspect.isclass(
215
+ origin # pyright: ignore[reportUnknownArgumentType]
216
+ )
217
+ and not issubclass(origin, BaseModel)
218
+ and issubclass(origin, pydantic.BaseModel)
219
+ ):
214
220
  raise TypeError("Pydantic models must subclass our base model type, e.g. `from spitch import BaseModel`")
215
221
 
216
222
  if (
@@ -7,16 +7,20 @@ import contextvars
7
7
  from typing import Any, TypeVar, Callable, Awaitable
8
8
  from typing_extensions import ParamSpec
9
9
 
10
+ import anyio
11
+ import sniffio
12
+ import anyio.to_thread
13
+
10
14
  T_Retval = TypeVar("T_Retval")
11
15
  T_ParamSpec = ParamSpec("T_ParamSpec")
12
16
 
13
17
 
14
18
  if sys.version_info >= (3, 9):
15
- to_thread = asyncio.to_thread
19
+ _asyncio_to_thread = asyncio.to_thread
16
20
  else:
17
21
  # backport of https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
18
22
  # for Python 3.8 support
19
- async def to_thread(
23
+ async def _asyncio_to_thread(
20
24
  func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
21
25
  ) -> Any:
22
26
  """Asynchronously run function *func* in a separate thread.
@@ -34,6 +38,17 @@ else:
34
38
  return await loop.run_in_executor(None, func_call)
35
39
 
36
40
 
41
+ async def to_thread(
42
+ func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs
43
+ ) -> T_Retval:
44
+ if sniffio.current_async_library() == "asyncio":
45
+ return await _asyncio_to_thread(func, *args, **kwargs)
46
+
47
+ return await anyio.to_thread.run_sync(
48
+ functools.partial(func, *args, **kwargs),
49
+ )
50
+
51
+
37
52
  # inspired by `asyncer`, https://github.com/tiangolo/asyncer
38
53
  def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]:
39
54
  """
@@ -25,7 +25,7 @@ from ._typing import (
25
25
  is_annotated_type,
26
26
  strip_annotated_type,
27
27
  )
28
- from .._compat import model_dump, is_typeddict
28
+ from .._compat import get_origin, model_dump, is_typeddict
29
29
 
30
30
  _T = TypeVar("_T")
31
31
 
@@ -164,9 +164,14 @@ def _transform_recursive(
164
164
  inner_type = annotation
165
165
 
166
166
  stripped_type = strip_annotated_type(inner_type)
167
+ origin = get_origin(stripped_type) or stripped_type
167
168
  if is_typeddict(stripped_type) and is_mapping(data):
168
169
  return _transform_typeddict(data, stripped_type)
169
170
 
171
+ if origin == dict and is_mapping(data):
172
+ items_type = get_args(stripped_type)[1]
173
+ return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
174
+
170
175
  if (
171
176
  # List[T]
172
177
  (is_list_type(stripped_type) and is_list(data))
@@ -307,9 +312,14 @@ async def _async_transform_recursive(
307
312
  inner_type = annotation
308
313
 
309
314
  stripped_type = strip_annotated_type(inner_type)
315
+ origin = get_origin(stripped_type) or stripped_type
310
316
  if is_typeddict(stripped_type) and is_mapping(data):
311
317
  return await _async_transform_typeddict(data, stripped_type)
312
318
 
319
+ if origin == dict and is_mapping(data):
320
+ items_type = get_args(stripped_type)[1]
321
+ return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
322
+
313
323
  if (
314
324
  # List[T]
315
325
  (is_list_type(stripped_type) and is_list(data))
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "spitch"
4
- __version__ = "1.19.0" # x-release-please-version
4
+ __version__ = "1.21.0" # x-release-please-version
@@ -41,7 +41,7 @@ class SpeechResource(SyncAPIResource):
41
41
  @cached_property
42
42
  def with_raw_response(self) -> SpeechResourceWithRawResponse:
43
43
  """
44
- This property can be used as a prefix for any HTTP method call to return the
44
+ This property can be used as a prefix for any HTTP method call to return
45
45
  the raw response object instead of the parsed content.
46
46
 
47
47
  For more information, see https://www.github.com/spi-tch/spitch-python#accessing-raw-response-data-eg-headers
@@ -79,6 +79,8 @@ class SpeechResource(SyncAPIResource):
79
79
  "lina",
80
80
  "john",
81
81
  "jude",
82
+ "henry",
83
+ "kani",
82
84
  ],
83
85
  stream: bool | NotGiven = NOT_GIVEN,
84
86
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -177,7 +179,7 @@ class AsyncSpeechResource(AsyncAPIResource):
177
179
  @cached_property
178
180
  def with_raw_response(self) -> AsyncSpeechResourceWithRawResponse:
179
181
  """
180
- This property can be used as a prefix for any HTTP method call to return the
182
+ This property can be used as a prefix for any HTTP method call to return
181
183
  the raw response object instead of the parsed content.
182
184
 
183
185
  For more information, see https://www.github.com/spi-tch/spitch-python#accessing-raw-response-data-eg-headers
@@ -215,6 +217,8 @@ class AsyncSpeechResource(AsyncAPIResource):
215
217
  "lina",
216
218
  "john",
217
219
  "jude",
220
+ "henry",
221
+ "kani",
218
222
  ],
219
223
  stream: bool | NotGiven = NOT_GIVEN,
220
224
  # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
@@ -31,7 +31,7 @@ class TextResource(SyncAPIResource):
31
31
  @cached_property
32
32
  def with_raw_response(self) -> TextResourceWithRawResponse:
33
33
  """
34
- This property can be used as a prefix for any HTTP method call to return the
34
+ This property can be used as a prefix for any HTTP method call to return
35
35
  the raw response object instead of the parsed content.
36
36
 
37
37
  For more information, see https://www.github.com/spi-tch/spitch-python#accessing-raw-response-data-eg-headers
@@ -132,7 +132,7 @@ class AsyncTextResource(AsyncAPIResource):
132
132
  @cached_property
133
133
  def with_raw_response(self) -> AsyncTextResourceWithRawResponse:
134
134
  """
135
- This property can be used as a prefix for any HTTP method call to return the
135
+ This property can be used as a prefix for any HTTP method call to return
136
136
  the raw response object instead of the parsed content.
137
137
 
138
138
  For more information, see https://www.github.com/spi-tch/spitch-python#accessing-raw-response-data-eg-headers
@@ -30,6 +30,8 @@ class SpeechGenerateParams(TypedDict, total=False):
30
30
  "lina",
31
31
  "john",
32
32
  "jude",
33
+ "henry",
34
+ "kani",
33
35
  ]
34
36
  ]
35
37
 
@@ -6,6 +6,7 @@ import gc
6
6
  import os
7
7
  import sys
8
8
  import json
9
+ import time
9
10
  import asyncio
10
11
  import inspect
11
12
  import subprocess
@@ -22,10 +23,12 @@ from pydantic import ValidationError
22
23
 
23
24
  from spitch import Spitch, AsyncSpitch, APIResponseValidationError
24
25
  from spitch._types import Omit
26
+ from spitch._utils import maybe_transform
25
27
  from spitch._models import BaseModel, FinalRequestOptions
26
28
  from spitch._constants import RAW_RESPONSE_HEADER
27
29
  from spitch._exceptions import SpitchError, APIStatusError, APITimeoutError, APIResponseValidationError
28
30
  from spitch._base_client import DEFAULT_TIMEOUT, HTTPX_DEFAULT_TIMEOUT, BaseClient, make_request_options
31
+ from spitch.types.speech_generate_params import SpeechGenerateParams
29
32
 
30
33
  from .utils import update_env
31
34
 
@@ -348,11 +351,11 @@ class TestSpitch:
348
351
  FinalRequestOptions(
349
352
  method="get",
350
353
  url="/foo",
351
- params={"foo": "baz", "query_param": "overriden"},
354
+ params={"foo": "baz", "query_param": "overridden"},
352
355
  )
353
356
  )
354
357
  url = httpx.URL(request.url)
355
- assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
358
+ assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
356
359
 
357
360
  def test_request_extra_json(self) -> None:
358
361
  request = self.client._build_request(
@@ -709,7 +712,9 @@ class TestSpitch:
709
712
  with pytest.raises(APITimeoutError):
710
713
  self.client.post(
711
714
  "/v1/speech",
712
- body=cast(object, dict(language="yo", text="text", voice="sade")),
715
+ body=cast(
716
+ object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
717
+ ),
713
718
  cast_to=httpx.Response,
714
719
  options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
715
720
  )
@@ -724,7 +729,9 @@ class TestSpitch:
724
729
  with pytest.raises(APIStatusError):
725
730
  self.client.post(
726
731
  "/v1/speech",
727
- body=cast(object, dict(language="yo", text="text", voice="sade")),
732
+ body=cast(
733
+ object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
734
+ ),
728
735
  cast_to=httpx.Response,
729
736
  options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
730
737
  )
@@ -1114,11 +1121,11 @@ class TestAsyncSpitch:
1114
1121
  FinalRequestOptions(
1115
1122
  method="get",
1116
1123
  url="/foo",
1117
- params={"foo": "baz", "query_param": "overriden"},
1124
+ params={"foo": "baz", "query_param": "overridden"},
1118
1125
  )
1119
1126
  )
1120
1127
  url = httpx.URL(request.url)
1121
- assert dict(url.params) == {"foo": "baz", "query_param": "overriden"}
1128
+ assert dict(url.params) == {"foo": "baz", "query_param": "overridden"}
1122
1129
 
1123
1130
  def test_request_extra_json(self) -> None:
1124
1131
  request = self.client._build_request(
@@ -1489,7 +1496,9 @@ class TestAsyncSpitch:
1489
1496
  with pytest.raises(APITimeoutError):
1490
1497
  await self.client.post(
1491
1498
  "/v1/speech",
1492
- body=cast(object, dict(language="yo", text="text", voice="sade")),
1499
+ body=cast(
1500
+ object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
1501
+ ),
1493
1502
  cast_to=httpx.Response,
1494
1503
  options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
1495
1504
  )
@@ -1504,7 +1513,9 @@ class TestAsyncSpitch:
1504
1513
  with pytest.raises(APIStatusError):
1505
1514
  await self.client.post(
1506
1515
  "/v1/speech",
1507
- body=cast(object, dict(language="yo", text="text", voice="sade")),
1516
+ body=cast(
1517
+ object, maybe_transform(dict(language="yo", text="text", voice="sade"), SpeechGenerateParams)
1518
+ ),
1508
1519
  cast_to=httpx.Response,
1509
1520
  options={"headers": {RAW_RESPONSE_HEADER: "stream"}},
1510
1521
  )
@@ -1622,10 +1633,20 @@ class TestAsyncSpitch:
1622
1633
  [sys.executable, "-c", test_code],
1623
1634
  text=True,
1624
1635
  ) as process:
1625
- try:
1626
- process.wait(2)
1627
- if process.returncode:
1628
- raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
1629
- except subprocess.TimeoutExpired as e:
1630
- process.kill()
1631
- raise AssertionError("calling get_platform using asyncify resulted in a hung process") from e
1636
+ timeout = 10 # seconds
1637
+
1638
+ start_time = time.monotonic()
1639
+ while True:
1640
+ return_code = process.poll()
1641
+ if return_code is not None:
1642
+ if return_code != 0:
1643
+ raise AssertionError("calling get_platform using asyncify resulted in a non-zero exit code")
1644
+
1645
+ # success
1646
+ break
1647
+
1648
+ if time.monotonic() - start_time > timeout:
1649
+ process.kill()
1650
+ raise AssertionError("calling get_platform using asyncify resulted in a hung process")
1651
+
1652
+ time.sleep(0.1)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import pathlib
5
- from typing import Any, List, Union, TypeVar, Iterable, Optional, cast
5
+ from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast
6
6
  from datetime import date, datetime
7
7
  from typing_extensions import Required, Annotated, TypedDict
8
8
 
@@ -388,6 +388,15 @@ async def test_iterable_of_dictionaries(use_async: bool) -> None:
388
388
  }
389
389
 
390
390
 
391
+ @parametrize
392
+ @pytest.mark.asyncio
393
+ async def test_dictionary_items(use_async: bool) -> None:
394
+ class DictItems(TypedDict):
395
+ foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")]
396
+
397
+ assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}}
398
+
399
+
391
400
  class TypedDictIterableUnionStr(TypedDict):
392
401
  foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")]
393
402
 
@@ -1,3 +0,0 @@
1
- {
2
- ".": "1.19.0"
3
- }
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