Metadata-Version: 2.5
Name: forgefile
Version: 0.2.0
Summary: Python client for the ForgeFile REST API — translate, transcribe, convert, OCR, summarize and rewrite files.
Project-URL: Homepage, https://forgefile.com
Project-URL: Documentation, https://forgefile.com/docs
Project-URL: Source, https://github.com/ForgeFile/forgefile-python
Project-URL: Issues, https://github.com/ForgeFile/forgefile-python/issues
Project-URL: Changelog, https://github.com/ForgeFile/forgefile-python/blob/main/CHANGELOG.md
Author-email: ForgeFile <support@forgefile.com>
License: MIT
License-File: LICENSE
Keywords: api-client,file-conversion,forgefile,ocr,transcription,translation
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Description-Content-Type: text/markdown

# forgefile-python

Python client for the [ForgeFile](https://forgefile.com) REST API — translate, transcribe,
convert, OCR, summarize and rewrite files.

Requires Python 3.11+.

## Install

```bash
pip install forgefile
```

## Quick start

```python
from forgefile import ForgeFile

with ForgeFile("your-token") as api:
    job = api.jobs.translate("contract.pdf", target_language="es")
    api.files.wait(job.uuid)
    api.files.download(job.uuid, "contract.es.pdf")
```

The token is read from `FORGEFILE_API_KEY` when the first argument is omitted.
Public endpoints need no token at all:

```python
with ForgeFile() as api:
    for language in api.public.languages():
        print(language.code, language.name)
```

## Processing a file

Every job endpoint uploads the file and starts the work in one call, returning a `FileJob`
whose `uuid` identifies it from then on.

```python
api.jobs.translate("contract.pdf", target_language="de")  # source_language is optional
api.jobs.transcribe("interview.mp3")
api.jobs.convert("report.docx", to_format="pdf")
api.jobs.ocr("receipt.jpg")
api.jobs.summarize("paper.pdf")
api.jobs.rewrite("draft.docx")
api.jobs.compress("scan.pdf")
```

Uploads are capped at 10 MB. The client checks the size first and raises
`FileTooLargeError` rather than sending the bytes to be refused.

### Waiting for the result

`wait()` blocks until the job reaches a terminal state and raises `JobTimeoutError` if it
does not. The job keeps running server-side, so calling again resumes waiting.

```python
finished = api.files.wait(job.uuid, timeout=900, interval=5)

if finished.succeeded:
    transcript = api.files.result(job.uuid)  # structured output
    api.files.download(job.uuid, "interview.srt")
```

The gap between polls grows from `interval` up to `max_interval`, so an
hour-long transcription costs tens of requests, not hundreds.

`track()` yields every observed state instead, for progress reporting:

```python
for state in api.files.track(job.uuid, interval=5):
    print(state.status)
```

### Cancelling

A running job can be stopped, and the API refunds the credits it did not
consume. Each job type has its own route, and the client knows which verb each
one needs:

```python
api.translation.cancel(file_uuid)  # also .resume(file_uuid)
api.transcription.cancel(job_id)
api.ocr.cancel(job_id)
```

Translation addresses files by **UUID**; conversion and summarization use the
**numeric** job id the API returns. Passing the wrong shape is rejected by the
API before it reaches the handler.

Branch on `job.is_finished` and `job.succeeded` rather than comparing status strings — a
status this client does not recognise is never treated as finished, so a wait loop cannot
end early on a state it has not seen.

## Errors

Every failure raises a subclass of `ForgeFileError` carrying the API's stable `error_code`,
so you can branch on the code rather than on message text.

```python
from forgefile import RateLimitError, ValidationError

try:
    api.jobs.translate("contract.pdf", target_language="es")
except ValidationError as exc:
    print(exc.context)  # field errors
except RateLimitError as exc:
    print(f"retry in {exc.retry_after}s")
```

| Exception | Raised when |
|---|---|
| `AuthenticationError` | 401 — no token, or rejected |
| `ForbiddenError` | 403 — token lacks the right |
| `NotFoundError` | 404 |
| `ValidationError` | 422 — field errors in `context` |
| `RateLimitError` | 429 — `retry_after` in seconds |
| `ServerError` | 5xx — safe to retry with backoff |
| `TransportError` | no response at all: DNS, TLS, timeout |
| `JobTimeoutError` | `wait()` gave up; the job still runs |
| `FileTooLargeError` | the upload exceeds 10 MB; refused before sending |

### Retries

Transient failures are retried automatically with exponential backoff:

- **429** is always retried, on any method, honouring `Retry-After` — the
  request was refused before it ran, so nothing happened.
- **5xx and connection failures** are retried only for `GET`, `HEAD`, `PUT` and
  `DELETE`. A `POST` that timed out may already have created a job, and jobs
  cost credits.
- **Uploads are never retried**: the file handle is consumed by the first
  attempt and cannot be replayed.

```python
from forgefile import ForgeFile, RetryPolicy

ForgeFile(retries=RetryPolicy(attempts=5, backoff=1.0))
ForgeFile(retries=RetryPolicy(attempts=1))  # off
```

The API allows 60 requests per minute.

## Configuration

| Argument | Environment variable | Default |
|---|---|---|
| `api_key` | `FORGEFILE_API_KEY` | none — public endpoints only |
| `base_url` | `FORGEFILE_BASE_URL` | `https://forgefile.com/api/v1` |
| `timeout` | — | 60 seconds |
| `retries` | — | 3 attempts, 0.5 s backoff |

## Examples

Runnable scripts in [`examples/`](examples):

| File | Shows |
|---|---|
| `01_public_data.py` | reference data without a token |
| `02_translate_document.py` | submit, wait, download |
| `03_transcribe_with_progress.py` | streaming progress with `track()` |
| `04_convert_a_folder.py` | batching — submit all, then collect |
| `05_handling_errors.py` | every failure mode and its remedy |
| `06_custom_transport.py` | replacing the HTTP layer |
| `07_cancel_a_job.py` | cancelling a job that runs over budget |

## Architecture

| Module | Responsibility |
|---|---|
| `config` | where to connect and with what headers |
| `envelope` | the API's `{success, message, data}` wrapper — the only place that knows it |
| `errors` | turning a failed response into the right exception |
| `transport` | HTTP, as a `Protocol` plus an httpx implementation |
| `resources/` | one class per endpoint group: `system`, `public`, `account`, `files`, `jobs`, `translation`, `transcription`, `ocr`, `conversion` |

Resources depend on the `HTTPTransport` protocol, never on httpx, so the HTTP layer can be
replaced with a recorded fixture, a proxy or a different library — see
`examples/06_custom_transport.py`.

Response models accept unknown fields, which stay reachable through `model_extra`. Only
fields observed against the live API are typed explicitly; authenticated endpoints could not
be inspected without a token while this client was written, so their payloads are permissive
rather than guessed.

The package ships a `py.typed` marker, so your type checker sees these signatures.

## Development

```bash
git clone https://github.com/ForgeFile/forgefile-python
cd forgefile-python
uv sync

uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest
```

Tests make no network calls: the resource layer runs against a fake transport, the httpx
layer against `respx`. CI runs these commands on Python 3.11, 3.12 and 3.13.

## Links

- [ForgeFile](https://forgefile.com) — the product
- [API reference](https://forgefile.com/docs)
- [Source and issues](https://github.com/ForgeFile/forgefile-python)
- [ForgeFile on GitHub](https://github.com/ForgeFile)
- Support — <support@forgefile.com>

MIT licensed. See [LICENSE](LICENSE).
