forgefile 0.1.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.
- forgefile-0.1.0/.github/workflows/ci.yml +37 -0
- forgefile-0.1.0/.github/workflows/release.yml +51 -0
- forgefile-0.1.0/.gitignore +29 -0
- forgefile-0.1.0/CHANGELOG.md +22 -0
- forgefile-0.1.0/LICENSE +21 -0
- forgefile-0.1.0/PKG-INFO +199 -0
- forgefile-0.1.0/README.md +170 -0
- forgefile-0.1.0/examples/01_public_data.py +24 -0
- forgefile-0.1.0/examples/02_translate_document.py +36 -0
- forgefile-0.1.0/examples/03_transcribe_with_progress.py +41 -0
- forgefile-0.1.0/examples/04_convert_a_folder.py +59 -0
- forgefile-0.1.0/examples/05_handling_errors.py +42 -0
- forgefile-0.1.0/examples/06_custom_transport.py +58 -0
- forgefile-0.1.0/pyproject.toml +72 -0
- forgefile-0.1.0/src/forgefile/__init__.py +52 -0
- forgefile-0.1.0/src/forgefile/client.py +88 -0
- forgefile-0.1.0/src/forgefile/config.py +47 -0
- forgefile-0.1.0/src/forgefile/envelope.py +32 -0
- forgefile-0.1.0/src/forgefile/errors.py +146 -0
- forgefile-0.1.0/src/forgefile/models.py +97 -0
- forgefile-0.1.0/src/forgefile/py.typed +0 -0
- forgefile-0.1.0/src/forgefile/resources/__init__.py +9 -0
- forgefile-0.1.0/src/forgefile/resources/_base.py +52 -0
- forgefile-0.1.0/src/forgefile/resources/account.py +28 -0
- forgefile-0.1.0/src/forgefile/resources/files.py +96 -0
- forgefile-0.1.0/src/forgefile/resources/jobs.py +56 -0
- forgefile-0.1.0/src/forgefile/resources/public.py +32 -0
- forgefile-0.1.0/src/forgefile/resources/system.py +23 -0
- forgefile-0.1.0/src/forgefile/transport.py +133 -0
- forgefile-0.1.0/tests/__init__.py +0 -0
- forgefile-0.1.0/tests/fakes.py +50 -0
- forgefile-0.1.0/tests/test_envelope.py +30 -0
- forgefile-0.1.0/tests/test_errors.py +67 -0
- forgefile-0.1.0/tests/test_http.py +185 -0
- forgefile-0.1.0/tests/test_models.py +44 -0
- forgefile-0.1.0/tests/test_resources.py +211 -0
- forgefile-0.1.0/uv.lock +798 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync --python ${{ matrix.python-version }}
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: uv run ruff check .
|
|
29
|
+
|
|
30
|
+
- name: Check formatting
|
|
31
|
+
run: uv run ruff format --check .
|
|
32
|
+
|
|
33
|
+
- name: Type check
|
|
34
|
+
run: uv run mypy
|
|
35
|
+
|
|
36
|
+
- name: Test
|
|
37
|
+
run: uv run pytest
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- name: Install uv
|
|
14
|
+
uses: astral-sh/setup-uv@v5
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: true
|
|
17
|
+
|
|
18
|
+
- name: Verify the tag matches the packaged version
|
|
19
|
+
run: |
|
|
20
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
21
|
+
version="$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)"
|
|
22
|
+
if [ "$tag" != "$version" ]; then
|
|
23
|
+
echo "Tag $tag does not match pyproject version $version" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: uv build
|
|
29
|
+
|
|
30
|
+
- name: Check metadata
|
|
31
|
+
run: uvx twine check dist/*
|
|
32
|
+
|
|
33
|
+
- uses: actions/upload-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
|
|
38
|
+
publish:
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
environment: pypi
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write # required for trusted publishing; no token is stored
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- name: Publish to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# Environments
|
|
9
|
+
.venv/
|
|
10
|
+
.env
|
|
11
|
+
|
|
12
|
+
# Tooling caches
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
|
|
17
|
+
# Coverage
|
|
18
|
+
.coverage
|
|
19
|
+
.coverage.*
|
|
20
|
+
coverage.xml
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Interrupted downloads written by the client
|
|
24
|
+
*.part
|
|
25
|
+
|
|
26
|
+
# Editors and OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions
|
|
4
|
+
follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Initial client for the [ForgeFile](https://forgefile.com) REST API, with the `system`,
|
|
11
|
+
`public`, `account`, `files` and `jobs` resource groups.
|
|
12
|
+
- `HTTPTransport` protocol, so the HTTP layer can be replaced without touching the resources.
|
|
13
|
+
- Typed exception hierarchy carrying the API's `error_code`, plus `retry_after` on
|
|
14
|
+
`RateLimitError`.
|
|
15
|
+
- `files.wait()` to block until a job finishes, and `files.track()` to report progress while
|
|
16
|
+
it runs.
|
|
17
|
+
- Runnable examples under `examples/`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `subscription_plans()` moved from `public` to `account`: the public route does not
|
|
22
|
+
exist on the API, the authenticated one does.
|
forgefile-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ForgeFile
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
forgefile-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: forgefile
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the ForgeFile REST API — translate, transcribe, convert, OCR, summarize and rewrite files.
|
|
5
|
+
Project-URL: Homepage, https://forgefile.com
|
|
6
|
+
Project-URL: Documentation, https://forgefile.com/docs
|
|
7
|
+
Project-URL: Source, https://github.com/ForgeFile/forgefile-python
|
|
8
|
+
Project-URL: Issues, https://github.com/ForgeFile/forgefile-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/ForgeFile/forgefile-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: ForgeFile <support@forgefile.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api-client,file-conversion,forgefile,ocr,transcription,translation
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: pydantic>=2.7
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# forgefile-python
|
|
31
|
+
|
|
32
|
+
Python client for the [ForgeFile](https://forgefile.com) REST API — translate, transcribe,
|
|
33
|
+
convert, OCR, summarize and rewrite files.
|
|
34
|
+
|
|
35
|
+
Requires Python 3.11+.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install forgefile
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from forgefile import ForgeFile
|
|
47
|
+
|
|
48
|
+
with ForgeFile("your-token") as api:
|
|
49
|
+
job = api.jobs.translate("contract.pdf", target_language="es")
|
|
50
|
+
api.files.wait(job.uuid)
|
|
51
|
+
api.files.download(job.uuid, "contract.es.pdf")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The token is read from `FORGEFILE_API_KEY` when the first argument is omitted.
|
|
55
|
+
Public endpoints need no token at all:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
with ForgeFile() as api:
|
|
59
|
+
for language in api.public.languages():
|
|
60
|
+
print(language.code, language.name)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Processing a file
|
|
64
|
+
|
|
65
|
+
Every job endpoint uploads the file and starts the work in one call, returning a `FileJob`
|
|
66
|
+
whose `uuid` identifies it from then on.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
api.jobs.translate("contract.pdf", target_language="de") # source_language is optional
|
|
70
|
+
api.jobs.transcribe("interview.mp3")
|
|
71
|
+
api.jobs.convert("report.docx", to_format="pdf")
|
|
72
|
+
api.jobs.ocr("receipt.jpg")
|
|
73
|
+
api.jobs.summarize("paper.pdf")
|
|
74
|
+
api.jobs.rewrite("draft.docx")
|
|
75
|
+
api.jobs.compress("scan.pdf")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Uploads are capped at 10 MB by the API.
|
|
79
|
+
|
|
80
|
+
### Waiting for the result
|
|
81
|
+
|
|
82
|
+
`wait()` blocks until the job reaches a terminal state and raises `JobTimeoutError` if it
|
|
83
|
+
does not. The job keeps running server-side, so calling again resumes waiting.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
finished = api.files.wait(job.uuid, timeout=900, interval=5)
|
|
87
|
+
|
|
88
|
+
if finished.succeeded:
|
|
89
|
+
transcript = api.files.result(job.uuid) # structured output
|
|
90
|
+
api.files.download(job.uuid, "interview.srt")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`track()` yields every observed state instead, for progress reporting:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
for state in api.files.track(job.uuid, interval=5):
|
|
97
|
+
print(state.status)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Branch on `job.is_finished` and `job.succeeded` rather than comparing status strings — a
|
|
101
|
+
status this client does not recognise is never treated as finished, so a wait loop cannot
|
|
102
|
+
end early on a state it has not seen.
|
|
103
|
+
|
|
104
|
+
## Errors
|
|
105
|
+
|
|
106
|
+
Every failure raises a subclass of `ForgeFileError` carrying the API's stable `error_code`,
|
|
107
|
+
so you can branch on the code rather than on message text.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from forgefile import RateLimitError, ValidationError
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
api.jobs.translate("contract.pdf", target_language="es")
|
|
114
|
+
except ValidationError as exc:
|
|
115
|
+
print(exc.context) # field errors
|
|
116
|
+
except RateLimitError as exc:
|
|
117
|
+
print(f"retry in {exc.retry_after}s")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Exception | Raised when |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `AuthenticationError` | 401 — no token, or rejected |
|
|
123
|
+
| `ForbiddenError` | 403 — token lacks the right |
|
|
124
|
+
| `NotFoundError` | 404 |
|
|
125
|
+
| `ValidationError` | 422 — field errors in `context` |
|
|
126
|
+
| `RateLimitError` | 429 — `retry_after` in seconds |
|
|
127
|
+
| `ServerError` | 5xx — safe to retry with backoff |
|
|
128
|
+
| `TransportError` | no response at all: DNS, TLS, timeout |
|
|
129
|
+
| `JobTimeoutError` | `wait()` gave up; the job still runs |
|
|
130
|
+
|
|
131
|
+
The API allows 60 requests per minute.
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
| Argument | Environment variable | Default |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `api_key` | `FORGEFILE_API_KEY` | none — public endpoints only |
|
|
138
|
+
| `base_url` | `FORGEFILE_BASE_URL` | `https://forgefile.com/api/v1` |
|
|
139
|
+
| `timeout` | — | 60 seconds |
|
|
140
|
+
|
|
141
|
+
## Examples
|
|
142
|
+
|
|
143
|
+
Runnable scripts in [`examples/`](examples):
|
|
144
|
+
|
|
145
|
+
| File | Shows |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `01_public_data.py` | reference data without a token |
|
|
148
|
+
| `02_translate_document.py` | submit, wait, download |
|
|
149
|
+
| `03_transcribe_with_progress.py` | streaming progress with `track()` |
|
|
150
|
+
| `04_convert_a_folder.py` | batching — submit all, then collect |
|
|
151
|
+
| `05_handling_errors.py` | every failure mode and its remedy |
|
|
152
|
+
| `06_custom_transport.py` | replacing the HTTP layer |
|
|
153
|
+
|
|
154
|
+
## Architecture
|
|
155
|
+
|
|
156
|
+
| Module | Responsibility |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `config` | where to connect and with what headers |
|
|
159
|
+
| `envelope` | the API's `{success, message, data}` wrapper — the only place that knows it |
|
|
160
|
+
| `errors` | turning a failed response into the right exception |
|
|
161
|
+
| `transport` | HTTP, as a `Protocol` plus an httpx implementation |
|
|
162
|
+
| `resources/` | one class per endpoint group: `system`, `public`, `account`, `files`, `jobs` |
|
|
163
|
+
|
|
164
|
+
Resources depend on the `HTTPTransport` protocol, never on httpx, so the HTTP layer can be
|
|
165
|
+
replaced with a recorded fixture, a proxy or a different library — see
|
|
166
|
+
`examples/06_custom_transport.py`.
|
|
167
|
+
|
|
168
|
+
Response models accept unknown fields, which stay reachable through `model_extra`. Only
|
|
169
|
+
fields observed against the live API are typed explicitly; authenticated endpoints could not
|
|
170
|
+
be inspected without a token while this client was written, so their payloads are permissive
|
|
171
|
+
rather than guessed.
|
|
172
|
+
|
|
173
|
+
The package ships a `py.typed` marker, so your type checker sees these signatures.
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
git clone https://github.com/ForgeFile/forgefile-python
|
|
179
|
+
cd forgefile-python
|
|
180
|
+
uv sync
|
|
181
|
+
|
|
182
|
+
uv run ruff check .
|
|
183
|
+
uv run ruff format --check .
|
|
184
|
+
uv run mypy
|
|
185
|
+
uv run pytest
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Tests make no network calls: the resource layer runs against a fake transport, the httpx
|
|
189
|
+
layer against `respx`. CI runs these commands on Python 3.11, 3.12 and 3.13.
|
|
190
|
+
|
|
191
|
+
## Links
|
|
192
|
+
|
|
193
|
+
- [ForgeFile](https://forgefile.com) — the product
|
|
194
|
+
- [API reference](https://forgefile.com/docs)
|
|
195
|
+
- [Source and issues](https://github.com/ForgeFile/forgefile-python)
|
|
196
|
+
- [ForgeFile on GitHub](https://github.com/ForgeFile)
|
|
197
|
+
- Support — <support@forgefile.com>
|
|
198
|
+
|
|
199
|
+
MIT licensed. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# forgefile-python
|
|
2
|
+
|
|
3
|
+
Python client for the [ForgeFile](https://forgefile.com) REST API — translate, transcribe,
|
|
4
|
+
convert, OCR, summarize and rewrite files.
|
|
5
|
+
|
|
6
|
+
Requires Python 3.11+.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install forgefile
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from forgefile import ForgeFile
|
|
18
|
+
|
|
19
|
+
with ForgeFile("your-token") as api:
|
|
20
|
+
job = api.jobs.translate("contract.pdf", target_language="es")
|
|
21
|
+
api.files.wait(job.uuid)
|
|
22
|
+
api.files.download(job.uuid, "contract.es.pdf")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The token is read from `FORGEFILE_API_KEY` when the first argument is omitted.
|
|
26
|
+
Public endpoints need no token at all:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
with ForgeFile() as api:
|
|
30
|
+
for language in api.public.languages():
|
|
31
|
+
print(language.code, language.name)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Processing a file
|
|
35
|
+
|
|
36
|
+
Every job endpoint uploads the file and starts the work in one call, returning a `FileJob`
|
|
37
|
+
whose `uuid` identifies it from then on.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
api.jobs.translate("contract.pdf", target_language="de") # source_language is optional
|
|
41
|
+
api.jobs.transcribe("interview.mp3")
|
|
42
|
+
api.jobs.convert("report.docx", to_format="pdf")
|
|
43
|
+
api.jobs.ocr("receipt.jpg")
|
|
44
|
+
api.jobs.summarize("paper.pdf")
|
|
45
|
+
api.jobs.rewrite("draft.docx")
|
|
46
|
+
api.jobs.compress("scan.pdf")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Uploads are capped at 10 MB by the API.
|
|
50
|
+
|
|
51
|
+
### Waiting for the result
|
|
52
|
+
|
|
53
|
+
`wait()` blocks until the job reaches a terminal state and raises `JobTimeoutError` if it
|
|
54
|
+
does not. The job keeps running server-side, so calling again resumes waiting.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
finished = api.files.wait(job.uuid, timeout=900, interval=5)
|
|
58
|
+
|
|
59
|
+
if finished.succeeded:
|
|
60
|
+
transcript = api.files.result(job.uuid) # structured output
|
|
61
|
+
api.files.download(job.uuid, "interview.srt")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`track()` yields every observed state instead, for progress reporting:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
for state in api.files.track(job.uuid, interval=5):
|
|
68
|
+
print(state.status)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Branch on `job.is_finished` and `job.succeeded` rather than comparing status strings — a
|
|
72
|
+
status this client does not recognise is never treated as finished, so a wait loop cannot
|
|
73
|
+
end early on a state it has not seen.
|
|
74
|
+
|
|
75
|
+
## Errors
|
|
76
|
+
|
|
77
|
+
Every failure raises a subclass of `ForgeFileError` carrying the API's stable `error_code`,
|
|
78
|
+
so you can branch on the code rather than on message text.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from forgefile import RateLimitError, ValidationError
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
api.jobs.translate("contract.pdf", target_language="es")
|
|
85
|
+
except ValidationError as exc:
|
|
86
|
+
print(exc.context) # field errors
|
|
87
|
+
except RateLimitError as exc:
|
|
88
|
+
print(f"retry in {exc.retry_after}s")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Exception | Raised when |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `AuthenticationError` | 401 — no token, or rejected |
|
|
94
|
+
| `ForbiddenError` | 403 — token lacks the right |
|
|
95
|
+
| `NotFoundError` | 404 |
|
|
96
|
+
| `ValidationError` | 422 — field errors in `context` |
|
|
97
|
+
| `RateLimitError` | 429 — `retry_after` in seconds |
|
|
98
|
+
| `ServerError` | 5xx — safe to retry with backoff |
|
|
99
|
+
| `TransportError` | no response at all: DNS, TLS, timeout |
|
|
100
|
+
| `JobTimeoutError` | `wait()` gave up; the job still runs |
|
|
101
|
+
|
|
102
|
+
The API allows 60 requests per minute.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
| Argument | Environment variable | Default |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `api_key` | `FORGEFILE_API_KEY` | none — public endpoints only |
|
|
109
|
+
| `base_url` | `FORGEFILE_BASE_URL` | `https://forgefile.com/api/v1` |
|
|
110
|
+
| `timeout` | — | 60 seconds |
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
Runnable scripts in [`examples/`](examples):
|
|
115
|
+
|
|
116
|
+
| File | Shows |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `01_public_data.py` | reference data without a token |
|
|
119
|
+
| `02_translate_document.py` | submit, wait, download |
|
|
120
|
+
| `03_transcribe_with_progress.py` | streaming progress with `track()` |
|
|
121
|
+
| `04_convert_a_folder.py` | batching — submit all, then collect |
|
|
122
|
+
| `05_handling_errors.py` | every failure mode and its remedy |
|
|
123
|
+
| `06_custom_transport.py` | replacing the HTTP layer |
|
|
124
|
+
|
|
125
|
+
## Architecture
|
|
126
|
+
|
|
127
|
+
| Module | Responsibility |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `config` | where to connect and with what headers |
|
|
130
|
+
| `envelope` | the API's `{success, message, data}` wrapper — the only place that knows it |
|
|
131
|
+
| `errors` | turning a failed response into the right exception |
|
|
132
|
+
| `transport` | HTTP, as a `Protocol` plus an httpx implementation |
|
|
133
|
+
| `resources/` | one class per endpoint group: `system`, `public`, `account`, `files`, `jobs` |
|
|
134
|
+
|
|
135
|
+
Resources depend on the `HTTPTransport` protocol, never on httpx, so the HTTP layer can be
|
|
136
|
+
replaced with a recorded fixture, a proxy or a different library — see
|
|
137
|
+
`examples/06_custom_transport.py`.
|
|
138
|
+
|
|
139
|
+
Response models accept unknown fields, which stay reachable through `model_extra`. Only
|
|
140
|
+
fields observed against the live API are typed explicitly; authenticated endpoints could not
|
|
141
|
+
be inspected without a token while this client was written, so their payloads are permissive
|
|
142
|
+
rather than guessed.
|
|
143
|
+
|
|
144
|
+
The package ships a `py.typed` marker, so your type checker sees these signatures.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
git clone https://github.com/ForgeFile/forgefile-python
|
|
150
|
+
cd forgefile-python
|
|
151
|
+
uv sync
|
|
152
|
+
|
|
153
|
+
uv run ruff check .
|
|
154
|
+
uv run ruff format --check .
|
|
155
|
+
uv run mypy
|
|
156
|
+
uv run pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Tests make no network calls: the resource layer runs against a fake transport, the httpx
|
|
160
|
+
layer against `respx`. CI runs these commands on Python 3.11, 3.12 and 3.13.
|
|
161
|
+
|
|
162
|
+
## Links
|
|
163
|
+
|
|
164
|
+
- [ForgeFile](https://forgefile.com) — the product
|
|
165
|
+
- [API reference](https://forgefile.com/docs)
|
|
166
|
+
- [Source and issues](https://github.com/ForgeFile/forgefile-python)
|
|
167
|
+
- [ForgeFile on GitHub](https://github.com/ForgeFile)
|
|
168
|
+
- Support — <support@forgefile.com>
|
|
169
|
+
|
|
170
|
+
MIT licensed. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Reference data — the endpoints that need no token.
|
|
2
|
+
|
|
3
|
+
python examples/01_public_data.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from forgefile import ForgeFile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
with ForgeFile() as api:
|
|
13
|
+
print("service:", api.system.health())
|
|
14
|
+
|
|
15
|
+
languages = api.public.languages()
|
|
16
|
+
print(f"\n{len(languages)} languages available, first ten:")
|
|
17
|
+
for language in languages[:10]:
|
|
18
|
+
print(f" {language.code:<6} {language.name} ({language.native or '—'})")
|
|
19
|
+
|
|
20
|
+
print("\ncurrencies:", ", ".join(c.code or "?" for c in api.public.currencies()))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Translate a document and save the result next to the original.
|
|
2
|
+
|
|
3
|
+
export FORGEFILE_API_KEY=...
|
|
4
|
+
python examples/02_translate_document.py contract.pdf es
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from forgefile import ForgeFile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def translate(source: Path, target_language: str) -> Path | None:
|
|
16
|
+
with ForgeFile() as api:
|
|
17
|
+
job = api.jobs.translate(source, target_language=target_language)
|
|
18
|
+
print(f"submitted {job.uuid}")
|
|
19
|
+
|
|
20
|
+
finished = api.files.wait(job.uuid)
|
|
21
|
+
if not finished.succeeded:
|
|
22
|
+
print(f"job ended as {finished.status}", file=sys.stderr)
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
destination = source.with_suffix(f".{target_language}{source.suffix}")
|
|
26
|
+
return api.files.download(job.uuid, destination)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
if len(sys.argv) != 3:
|
|
31
|
+
print(__doc__, file=sys.stderr)
|
|
32
|
+
raise SystemExit(2)
|
|
33
|
+
|
|
34
|
+
result = translate(Path(sys.argv[1]), sys.argv[2])
|
|
35
|
+
print(f"saved {result}" if result else "translation failed")
|
|
36
|
+
raise SystemExit(0 if result else 1)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Transcribe media and report progress as it happens.
|
|
2
|
+
|
|
3
|
+
``track()`` yields every observed state instead of blocking silently, so a long
|
|
4
|
+
job can drive a progress line.
|
|
5
|
+
|
|
6
|
+
export FORGEFILE_API_KEY=...
|
|
7
|
+
python examples/03_transcribe_with_progress.py interview.mp3
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from forgefile import ForgeFile
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(source: Path) -> int:
|
|
19
|
+
with ForgeFile() as api:
|
|
20
|
+
job = api.jobs.transcribe(source)
|
|
21
|
+
|
|
22
|
+
for state in api.files.track(job.uuid, interval=5):
|
|
23
|
+
print(f"\r{state.status or 'unknown':<16}", end="", flush=True)
|
|
24
|
+
print()
|
|
25
|
+
|
|
26
|
+
final = api.files.get(job.uuid)
|
|
27
|
+
if not final.succeeded:
|
|
28
|
+
print(f"job ended as {final.status}", file=sys.stderr)
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
transcript = api.files.result(job.uuid)
|
|
32
|
+
print(transcript)
|
|
33
|
+
api.files.download(job.uuid, source.with_suffix(".srt"))
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
if len(sys.argv) != 2:
|
|
39
|
+
print(__doc__, file=sys.stderr)
|
|
40
|
+
raise SystemExit(2)
|
|
41
|
+
raise SystemExit(main(Path(sys.argv[1])))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Convert every file in a folder, submitting all jobs before waiting.
|
|
2
|
+
|
|
3
|
+
Jobs run server-side, so start them all first and collect the results after —
|
|
4
|
+
that turns N sequential waits into one.
|
|
5
|
+
|
|
6
|
+
export FORGEFILE_API_KEY=...
|
|
7
|
+
python examples/04_convert_a_folder.py ./invoices pdf
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from forgefile import FileJob, ForgeFile, ForgeFileError, JobTimeoutError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(folder: Path, to_format: str) -> int:
|
|
19
|
+
sources = sorted(p for p in folder.iterdir() if p.is_file())
|
|
20
|
+
if not sources:
|
|
21
|
+
print(f"nothing to convert in {folder}", file=sys.stderr)
|
|
22
|
+
return 1
|
|
23
|
+
|
|
24
|
+
with ForgeFile() as api:
|
|
25
|
+
submitted: dict[str, Path] = {}
|
|
26
|
+
for source in sources:
|
|
27
|
+
try:
|
|
28
|
+
job: FileJob = api.jobs.convert(source, to_format=to_format)
|
|
29
|
+
except ForgeFileError as exc:
|
|
30
|
+
print(f"skipped {source.name}: {exc}", file=sys.stderr)
|
|
31
|
+
continue
|
|
32
|
+
submitted[job.uuid] = source
|
|
33
|
+
print(f"submitted {source.name} -> {job.uuid}")
|
|
34
|
+
|
|
35
|
+
failures = 0
|
|
36
|
+
for uuid, source in submitted.items():
|
|
37
|
+
destination = source.with_suffix(f".{to_format}")
|
|
38
|
+
try:
|
|
39
|
+
finished = api.files.wait(uuid, timeout=900)
|
|
40
|
+
except JobTimeoutError as exc:
|
|
41
|
+
print(f"timeout {source.name}: {exc}", file=sys.stderr)
|
|
42
|
+
failures += 1
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if finished.succeeded:
|
|
46
|
+
api.files.download(uuid, destination)
|
|
47
|
+
print(f"saved {destination.name}")
|
|
48
|
+
else:
|
|
49
|
+
print(f"failed {source.name}: {finished.status}", file=sys.stderr)
|
|
50
|
+
failures += 1
|
|
51
|
+
|
|
52
|
+
return 1 if failures else 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
if len(sys.argv) != 3:
|
|
57
|
+
print(__doc__, file=sys.stderr)
|
|
58
|
+
raise SystemExit(2)
|
|
59
|
+
raise SystemExit(main(Path(sys.argv[1]), sys.argv[2]))
|