batchwork-ai 0.2.2__tar.gz → 0.3.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.
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/PKG-INFO +34 -13
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/README.md +33 -12
- batchwork_ai-0.3.0/pyproject.toml +98 -0
- batchwork_ai-0.2.2/pyproject.toml → batchwork_ai-0.3.0/pyproject.toml.orig +1 -1
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/__init__.py +30 -0
- batchwork_ai-0.3.0/src/batchwork/_base_url.py +56 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_limits.py +1 -0
- batchwork_ai-0.3.0/src/batchwork/_read_retry.py +89 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/body.py +430 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_commands.py +589 -3
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_config.py +8 -30
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_contract.py +60 -3
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_failures.py +11 -1
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_human.py +20 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_input.py +438 -6
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_lifecycle.py +6 -77
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_registry.py +34 -8
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_submit_text.py +614 -15
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_volume.py +3 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/client.py +388 -67
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/errors.py +1 -1
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/job.py +18 -6
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/media.py +9 -1
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/__init__.py +3 -0
- batchwork_ai-0.3.0/src/batchwork/providers/_capabilities.py +34 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/anthropic.py +3 -1
- batchwork_ai-0.3.0/src/batchwork/providers/azure.py +76 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/google.py +2 -1
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/mistral.py +32 -18
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/openai_compatible.py +58 -24
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/shared.py +101 -2
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/xai.py +107 -9
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/types.py +174 -2
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/LICENSE +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_anthropic_serialization.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_bounded_json.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_compatible_media.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_google_schema.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_google_serialization.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_network.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_provider_failure.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_serialization.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_text_validation.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/_typing.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/__init__.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_materialize.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_output.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/cli/_state.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/_image.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/adapter.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/groq.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/ids.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/openai.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/providers/together.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/py.typed +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/__init__.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/models.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/poller.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/signing.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/server/transport.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/__init__.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/base.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/memory.py +0 -0
- {batchwork_ai-0.2.2 → batchwork_ai-0.3.0}/src/batchwork/stores/redis.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: batchwork-ai
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Unified async batch API for AI providers
|
|
5
5
|
Keywords: ai,anthropic,batch,gemini,llm,openai
|
|
6
6
|
Author: Ajanraj
|
|
@@ -34,10 +34,10 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
|
|
35
35
|

|
|
36
36
|
|
|
37
|
-
Unified async batch API for OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
|
|
37
|
+
Unified async batch API for OpenAI, Azure OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
|
|
38
38
|
|
|
39
39
|
- One typed API for provider-native batch jobs
|
|
40
|
-
- Text, embedding,
|
|
40
|
+
- Text, embedding, image, image-edit, moderation, transcription, translation, and video workloads
|
|
41
41
|
- Normalized jobs, results, usage, and errors
|
|
42
42
|
- Messages, tools, structured content, and remote media
|
|
43
43
|
- Polling, persistent stores, and signed webhooks
|
|
@@ -129,21 +129,38 @@ asyncio.run(main())
|
|
|
129
129
|
|
|
130
130
|
Submitting returns a `BatchJob` immediately. Provider processing is asynchronous and may take minutes or, depending on the provider and workload, up to 24 hours.
|
|
131
131
|
|
|
132
|
+
Beyond text, the same client covers seven more modalities — for example hosted-audio transcription:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from batchwork import BatchTranscriptionRequest
|
|
136
|
+
|
|
137
|
+
job = await client.batch_transcriptions(
|
|
138
|
+
model="groq/whisper-large-v3",
|
|
139
|
+
requests=[
|
|
140
|
+
BatchTranscriptionRequest(
|
|
141
|
+
custom_id="call-1",
|
|
142
|
+
audio_url="https://example.com/call-1.mp3",
|
|
143
|
+
)
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
132
148
|
Models use `provider/model` form. Results are correlated with requests through `custom_id`; provider output order is not guaranteed.
|
|
133
149
|
|
|
134
150
|
## Providers
|
|
135
151
|
|
|
136
152
|
Output workloads supported by Batchwork:
|
|
137
153
|
|
|
138
|
-
| Provider
|
|
139
|
-
|
|
|
140
|
-
| OpenAI
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
154
|
+
| Provider | Text | Embeddings | Image generation | Image edits | Moderation | Transcription | Translation | Video |
|
|
155
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
156
|
+
| OpenAI | Yes | Yes | Yes | Yes | Yes | No | No | No |
|
|
157
|
+
| Azure OpenAI | Yes | No | No | No | No | No | No | No |
|
|
158
|
+
| Anthropic | Yes | No | No | No | No | No | No | No |
|
|
159
|
+
| Google Gemini | Yes | Yes | Yes | No | No | No | No | No |
|
|
160
|
+
| Groq | Yes | No | No | No | No | Yes | Yes | No |
|
|
161
|
+
| Mistral | Yes | Yes | No | No | Yes | Yes | No | No |
|
|
162
|
+
| Together AI | Yes | No | No | No | No | Yes | Yes | No |
|
|
163
|
+
| xAI | Yes | No | Yes | Yes | No | No | No | Yes |
|
|
147
164
|
|
|
148
165
|
Image, PDF, text-file, and audio inputs for text requests vary separately from output modalities. Provider APIs may also impose model-specific limits. See the [provider overview](https://batchwork.ajanraj.com/docs/providers) for input support, submission transport, credentials, and restrictions.
|
|
149
166
|
|
|
@@ -165,7 +182,7 @@ The base package does not import or require `upstash-redis`.
|
|
|
165
182
|
- [Configuration](https://batchwork.ajanraj.com/docs/configuration)
|
|
166
183
|
- [Jobs](https://batchwork.ajanraj.com/docs/guides/jobs)
|
|
167
184
|
- [Results](https://batchwork.ajanraj.com/docs/guides/results)
|
|
168
|
-
- [Text, embeddings, and
|
|
185
|
+
- [Text, embeddings, and other modalities](https://batchwork.ajanraj.com/docs/modalities/text)
|
|
169
186
|
- [Provider overview](https://batchwork.ajanraj.com/docs/providers)
|
|
170
187
|
- [Polling and webhooks](https://batchwork.ajanraj.com/docs/guides/server)
|
|
171
188
|
- [Stores](https://batchwork.ajanraj.com/docs/guides/stores)
|
|
@@ -174,6 +191,10 @@ The base package does not import or require `upstash-redis`.
|
|
|
174
191
|
- [Public API](https://batchwork.ajanraj.com/docs/api)
|
|
175
192
|
- [FAQ](https://batchwork.ajanraj.com/docs/faq)
|
|
176
193
|
|
|
194
|
+
## Built with Codex and GPT-5.6
|
|
195
|
+
|
|
196
|
+
Batchwork was built entirely in the OpenAI Codex CLI with GPT-5.6 during [OpenAI Build Week](https://openai.devpost.com/) (July 2026) — the Python library, the `batchwork` CLI with its machine output contract and local job registry, the Agent Skill, the test suite, and the documentation site. Codex planned the design through a decision interview, implemented the seven provider adapters across three modalities, hardened security through repeated review loops, verified everything against live provider APIs, and drove the PyPI and documentation releases. The full collaboration story, plus a self-contained demo project for judges, lives in [`hackathon/`](hackathon/README.md).
|
|
197
|
+
|
|
177
198
|
## License
|
|
178
199
|
|
|
179
200
|
[MIT](https://opensource.org/licenses/MIT) © [Ajanraj](https://github.com/ajanraj)
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Unified async batch API for OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
|
|
5
|
+
Unified async batch API for OpenAI, Azure OpenAI, Anthropic, Google Gemini, Groq, Mistral, Together AI, and xAI. Providers price eligible batch requests up to 50% below standard synchronous calls, and Batchwork gives you that pricing through one typed Python interface and one CLI. It handles provider-specific serialization, submission, polling, and result parsing so you do not have to. Pricing and eligibility remain provider- and model-specific.
|
|
6
6
|
|
|
7
7
|
- One typed API for provider-native batch jobs
|
|
8
|
-
- Text, embedding,
|
|
8
|
+
- Text, embedding, image, image-edit, moderation, transcription, translation, and video workloads
|
|
9
9
|
- Normalized jobs, results, usage, and errors
|
|
10
10
|
- Messages, tools, structured content, and remote media
|
|
11
11
|
- Polling, persistent stores, and signed webhooks
|
|
@@ -97,21 +97,38 @@ asyncio.run(main())
|
|
|
97
97
|
|
|
98
98
|
Submitting returns a `BatchJob` immediately. Provider processing is asynchronous and may take minutes or, depending on the provider and workload, up to 24 hours.
|
|
99
99
|
|
|
100
|
+
Beyond text, the same client covers seven more modalities — for example hosted-audio transcription:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from batchwork import BatchTranscriptionRequest
|
|
104
|
+
|
|
105
|
+
job = await client.batch_transcriptions(
|
|
106
|
+
model="groq/whisper-large-v3",
|
|
107
|
+
requests=[
|
|
108
|
+
BatchTranscriptionRequest(
|
|
109
|
+
custom_id="call-1",
|
|
110
|
+
audio_url="https://example.com/call-1.mp3",
|
|
111
|
+
)
|
|
112
|
+
],
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
100
116
|
Models use `provider/model` form. Results are correlated with requests through `custom_id`; provider output order is not guaranteed.
|
|
101
117
|
|
|
102
118
|
## Providers
|
|
103
119
|
|
|
104
120
|
Output workloads supported by Batchwork:
|
|
105
121
|
|
|
106
|
-
| Provider
|
|
107
|
-
|
|
|
108
|
-
| OpenAI
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
|
114
|
-
|
|
|
122
|
+
| Provider | Text | Embeddings | Image generation | Image edits | Moderation | Transcription | Translation | Video |
|
|
123
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
124
|
+
| OpenAI | Yes | Yes | Yes | Yes | Yes | No | No | No |
|
|
125
|
+
| Azure OpenAI | Yes | No | No | No | No | No | No | No |
|
|
126
|
+
| Anthropic | Yes | No | No | No | No | No | No | No |
|
|
127
|
+
| Google Gemini | Yes | Yes | Yes | No | No | No | No | No |
|
|
128
|
+
| Groq | Yes | No | No | No | No | Yes | Yes | No |
|
|
129
|
+
| Mistral | Yes | Yes | No | No | Yes | Yes | No | No |
|
|
130
|
+
| Together AI | Yes | No | No | No | No | Yes | Yes | No |
|
|
131
|
+
| xAI | Yes | No | Yes | Yes | No | No | No | Yes |
|
|
115
132
|
|
|
116
133
|
Image, PDF, text-file, and audio inputs for text requests vary separately from output modalities. Provider APIs may also impose model-specific limits. See the [provider overview](https://batchwork.ajanraj.com/docs/providers) for input support, submission transport, credentials, and restrictions.
|
|
117
134
|
|
|
@@ -133,7 +150,7 @@ The base package does not import or require `upstash-redis`.
|
|
|
133
150
|
- [Configuration](https://batchwork.ajanraj.com/docs/configuration)
|
|
134
151
|
- [Jobs](https://batchwork.ajanraj.com/docs/guides/jobs)
|
|
135
152
|
- [Results](https://batchwork.ajanraj.com/docs/guides/results)
|
|
136
|
-
- [Text, embeddings, and
|
|
153
|
+
- [Text, embeddings, and other modalities](https://batchwork.ajanraj.com/docs/modalities/text)
|
|
137
154
|
- [Provider overview](https://batchwork.ajanraj.com/docs/providers)
|
|
138
155
|
- [Polling and webhooks](https://batchwork.ajanraj.com/docs/guides/server)
|
|
139
156
|
- [Stores](https://batchwork.ajanraj.com/docs/guides/stores)
|
|
@@ -142,6 +159,10 @@ The base package does not import or require `upstash-redis`.
|
|
|
142
159
|
- [Public API](https://batchwork.ajanraj.com/docs/api)
|
|
143
160
|
- [FAQ](https://batchwork.ajanraj.com/docs/faq)
|
|
144
161
|
|
|
162
|
+
## Built with Codex and GPT-5.6
|
|
163
|
+
|
|
164
|
+
Batchwork was built entirely in the OpenAI Codex CLI with GPT-5.6 during [OpenAI Build Week](https://openai.devpost.com/) (July 2026) — the Python library, the `batchwork` CLI with its machine output contract and local job registry, the Agent Skill, the test suite, and the documentation site. Codex planned the design through a decision interview, implemented the seven provider adapters across three modalities, hardened security through repeated review loops, verified everything against live provider APIs, and drove the PyPI and documentation releases. The full collaboration story, plus a self-contained demo project for judges, lives in [`hackathon/`](hackathon/README.md).
|
|
165
|
+
|
|
145
166
|
## License
|
|
146
167
|
|
|
147
168
|
[MIT](https://opensource.org/licenses/MIT) © [Ajanraj](https://github.com/ajanraj)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.29,<0.12"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "batchwork-ai"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Unified async batch API for AI providers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = [
|
|
14
|
+
"ai",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"batch",
|
|
17
|
+
"gemini",
|
|
18
|
+
"llm",
|
|
19
|
+
"openai",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Framework :: AsyncIO",
|
|
24
|
+
"Intended Audience :: Developers",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"click>=8.4,<9",
|
|
34
|
+
"httpcore>=1,<2",
|
|
35
|
+
"httpx>=0.28,<1",
|
|
36
|
+
"pydantic>=2.11,<3",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[[project.authors]]
|
|
40
|
+
name = "Ajanraj"
|
|
41
|
+
email = "hey@ajanraj.com"
|
|
42
|
+
|
|
43
|
+
[[project.maintainers]]
|
|
44
|
+
name = "Ajanraj"
|
|
45
|
+
email = "hey@ajanraj.com"
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
batchwork = "batchwork.cli:main"
|
|
49
|
+
|
|
50
|
+
[project.optional-dependencies]
|
|
51
|
+
redis = ["upstash-redis>=1.4,<2"]
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Documentation = "https://batchwork.ajanraj.com"
|
|
55
|
+
Issues = "https://github.com/ajanraj/batchwork-ai/issues"
|
|
56
|
+
Repository = "https://github.com/ajanraj/batchwork-ai"
|
|
57
|
+
|
|
58
|
+
[dependency-groups]
|
|
59
|
+
dev = [
|
|
60
|
+
"jsonschema>=4.25,<5",
|
|
61
|
+
"pytest>=8.4,<9",
|
|
62
|
+
"pytest-asyncio>=1,<2",
|
|
63
|
+
"pytest-cov>=6.2,<7",
|
|
64
|
+
"ruff>=0.12,<1",
|
|
65
|
+
"twine>=6.1,<7",
|
|
66
|
+
"ty==0.0.59",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.uv.build-backend]
|
|
70
|
+
module-name = "batchwork"
|
|
71
|
+
module-root = "src"
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
addopts = "-q"
|
|
75
|
+
asyncio_mode = "auto"
|
|
76
|
+
testpaths = ["tests"]
|
|
77
|
+
markers = [
|
|
78
|
+
"live: requires real provider credentials",
|
|
79
|
+
"redis: requires Upstash Redis credentials",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[tool.ruff]
|
|
83
|
+
line-length = 100
|
|
84
|
+
target-version = "py311"
|
|
85
|
+
|
|
86
|
+
[tool.ruff.lint]
|
|
87
|
+
select = [
|
|
88
|
+
"E",
|
|
89
|
+
"F",
|
|
90
|
+
"I",
|
|
91
|
+
"UP",
|
|
92
|
+
"B",
|
|
93
|
+
"ASYNC",
|
|
94
|
+
"RUF",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ty.src]
|
|
98
|
+
include = ["src"]
|
|
@@ -57,8 +57,15 @@ from .types import (
|
|
|
57
57
|
BatchEmbeddingRequestSettings,
|
|
58
58
|
BatchImage,
|
|
59
59
|
BatchImageDefaults,
|
|
60
|
+
BatchImageEditDefaults,
|
|
61
|
+
BatchImageEditRequest,
|
|
62
|
+
BatchImageFileRef,
|
|
63
|
+
BatchImageRef,
|
|
60
64
|
BatchImageRequest,
|
|
65
|
+
BatchImageUrlRef,
|
|
61
66
|
BatchLimits,
|
|
67
|
+
BatchModeration,
|
|
68
|
+
BatchModerationRequest,
|
|
62
69
|
BatchProvider,
|
|
63
70
|
BatchRef,
|
|
64
71
|
BatchRequest,
|
|
@@ -69,7 +76,15 @@ from .types import (
|
|
|
69
76
|
BatchResultStatus,
|
|
70
77
|
BatchSnapshot,
|
|
71
78
|
BatchStatus,
|
|
79
|
+
BatchTranscriptionDefaults,
|
|
80
|
+
BatchTranscriptionRequest,
|
|
81
|
+
BatchTranscriptionSegment,
|
|
82
|
+
BatchTranslationDefaults,
|
|
83
|
+
BatchTranslationRequest,
|
|
72
84
|
BatchUsage,
|
|
85
|
+
BatchVideo,
|
|
86
|
+
BatchVideoDefaults,
|
|
87
|
+
BatchVideoRequest,
|
|
73
88
|
ContentToolOutput,
|
|
74
89
|
CustomPart,
|
|
75
90
|
DeprecatedToolOutputFileDataPart,
|
|
@@ -147,9 +162,16 @@ __all__ = [
|
|
|
147
162
|
"BatchEmbeddingRequestSettings",
|
|
148
163
|
"BatchImage",
|
|
149
164
|
"BatchImageDefaults",
|
|
165
|
+
"BatchImageEditDefaults",
|
|
166
|
+
"BatchImageEditRequest",
|
|
167
|
+
"BatchImageFileRef",
|
|
168
|
+
"BatchImageRef",
|
|
150
169
|
"BatchImageRequest",
|
|
170
|
+
"BatchImageUrlRef",
|
|
151
171
|
"BatchJob",
|
|
152
172
|
"BatchLimits",
|
|
173
|
+
"BatchModeration",
|
|
174
|
+
"BatchModerationRequest",
|
|
153
175
|
"BatchPoller",
|
|
154
176
|
"BatchProvider",
|
|
155
177
|
"BatchRef",
|
|
@@ -164,7 +186,15 @@ __all__ = [
|
|
|
164
186
|
"BatchStatus",
|
|
165
187
|
"BatchStore",
|
|
166
188
|
"BatchTimeoutError",
|
|
189
|
+
"BatchTranscriptionDefaults",
|
|
190
|
+
"BatchTranscriptionRequest",
|
|
191
|
+
"BatchTranscriptionSegment",
|
|
192
|
+
"BatchTranslationDefaults",
|
|
193
|
+
"BatchTranslationRequest",
|
|
167
194
|
"BatchUsage",
|
|
195
|
+
"BatchVideo",
|
|
196
|
+
"BatchVideoDefaults",
|
|
197
|
+
"BatchVideoRequest",
|
|
168
198
|
"BatchWebhookEvent",
|
|
169
199
|
"Batchwork",
|
|
170
200
|
"BatchworkError",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Provider base URL validation and normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ipaddress import ip_address
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseUrlError(ValueError):
|
|
10
|
+
"""A provider base URL violates the outbound credential policy."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _policy_error(label: str) -> BaseUrlError:
|
|
14
|
+
return BaseUrlError(
|
|
15
|
+
f"{label} must be absolute HTTPS (HTTP allowed for loopback), without "
|
|
16
|
+
"userinfo, query, or fragment."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_base_url(value: str, label: str = "base URL") -> str:
|
|
21
|
+
"""Validate a provider endpoint and remove trailing path slashes."""
|
|
22
|
+
raw_scheme, separator, remainder = value.partition("://")
|
|
23
|
+
authority = remainder
|
|
24
|
+
for delimiter in "/?#":
|
|
25
|
+
authority = authority.partition(delimiter)[0]
|
|
26
|
+
try:
|
|
27
|
+
parsed = urlsplit(value)
|
|
28
|
+
hostname = parsed.hostname
|
|
29
|
+
except ValueError as error:
|
|
30
|
+
raise _policy_error(label) from error
|
|
31
|
+
try:
|
|
32
|
+
_ = parsed.port
|
|
33
|
+
except ValueError as error:
|
|
34
|
+
raise BaseUrlError(f"{label} contains an invalid port.") from error
|
|
35
|
+
|
|
36
|
+
loopback = hostname == "localhost"
|
|
37
|
+
if hostname is not None:
|
|
38
|
+
try:
|
|
39
|
+
loopback = loopback or ip_address(hostname).is_loopback
|
|
40
|
+
except ValueError:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
not separator
|
|
45
|
+
or raw_scheme.lower() != parsed.scheme
|
|
46
|
+
or not hostname
|
|
47
|
+
or any(character.isspace() for character in authority)
|
|
48
|
+
or parsed.username is not None
|
|
49
|
+
or parsed.password is not None
|
|
50
|
+
or "?" in value
|
|
51
|
+
or "#" in value
|
|
52
|
+
or (parsed.scheme != "https" and not (parsed.scheme == "http" and loopback))
|
|
53
|
+
):
|
|
54
|
+
raise _policy_error(label)
|
|
55
|
+
|
|
56
|
+
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", ""))
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Provider-neutral retry policy for safe read operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import random
|
|
7
|
+
import ssl
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
from typing import TypeVar
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ._provider_failure import ProviderFailureError, ProviderFailureKind
|
|
15
|
+
|
|
16
|
+
_READ_ATTEMPTS = 3
|
|
17
|
+
_RETRYABLE_HTTP_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
18
|
+
_MAX_RETRY_DELAY_SECONDS = 60.0
|
|
19
|
+
_ReadResult = TypeVar("_ReadResult")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _ReadRetryDeadlineExceeded(TimeoutError):
|
|
23
|
+
"""The local read-retry deadline expired before another attempt."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cause_chain(error: BaseException) -> list[BaseException]:
|
|
27
|
+
causes: list[BaseException] = []
|
|
28
|
+
current: BaseException | None = error
|
|
29
|
+
while current is not None and current not in causes:
|
|
30
|
+
causes.append(current)
|
|
31
|
+
current = current.__cause__ or current.__context__
|
|
32
|
+
return causes
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _retryable_read_failure(error: BaseException) -> bool:
|
|
36
|
+
if not isinstance(error, ProviderFailureError):
|
|
37
|
+
return False
|
|
38
|
+
failure = error.failure
|
|
39
|
+
if failure.kind is ProviderFailureKind.UNAVAILABLE:
|
|
40
|
+
return failure.status_code in _RETRYABLE_HTTP_STATUSES
|
|
41
|
+
if failure.kind is not ProviderFailureKind.TRANSPORT:
|
|
42
|
+
return False
|
|
43
|
+
causes = _cause_chain(error)
|
|
44
|
+
if any(isinstance(cause, ssl.SSLError) for cause in causes):
|
|
45
|
+
return False
|
|
46
|
+
return any(
|
|
47
|
+
isinstance(
|
|
48
|
+
cause,
|
|
49
|
+
(
|
|
50
|
+
httpx.ConnectError,
|
|
51
|
+
httpx.ConnectTimeout,
|
|
52
|
+
httpx.ReadError,
|
|
53
|
+
httpx.ReadTimeout,
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
for cause in causes
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _retry_delay(error: BaseException, retry_index: int) -> float:
|
|
61
|
+
if isinstance(error, ProviderFailureError):
|
|
62
|
+
retry_after = error.failure.retry_after_seconds
|
|
63
|
+
if retry_after is not None:
|
|
64
|
+
return min(float(retry_after), _MAX_RETRY_DELAY_SECONDS)
|
|
65
|
+
ceiling = min(0.25 * (2**retry_index), _MAX_RETRY_DELAY_SECONDS)
|
|
66
|
+
return random.uniform(ceiling / 2, ceiling)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def _retry_read(
|
|
70
|
+
read: Callable[[], Awaitable[_ReadResult]],
|
|
71
|
+
*,
|
|
72
|
+
deadline: float | None = None,
|
|
73
|
+
) -> _ReadResult:
|
|
74
|
+
for attempt in range(_READ_ATTEMPTS):
|
|
75
|
+
try:
|
|
76
|
+
return await read()
|
|
77
|
+
except Exception as error:
|
|
78
|
+
if attempt + 1 == _READ_ATTEMPTS or not _retryable_read_failure(error):
|
|
79
|
+
raise
|
|
80
|
+
delay = _retry_delay(error, attempt)
|
|
81
|
+
if deadline is not None:
|
|
82
|
+
remaining = deadline - time.monotonic()
|
|
83
|
+
if remaining <= 0:
|
|
84
|
+
raise _ReadRetryDeadlineExceeded from None
|
|
85
|
+
if delay >= remaining:
|
|
86
|
+
await asyncio.sleep(remaining)
|
|
87
|
+
raise _ReadRetryDeadlineExceeded from None
|
|
88
|
+
await asyncio.sleep(delay)
|
|
89
|
+
raise RuntimeError("batchwork: exhausted retry loop")
|