Metadata-Version: 2.5
Name: doc2doc-client
Version: 0.1.0
Summary: Async Python client for the doc2doc conversion service
Author-email: Marc Meese <marc@marcmeese.de>
License-Expression: MIT
Classifier: Framework :: AsyncIO
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Description-Content-Type: text/markdown

# doc2doc-client

Async-Python-Client fuer den Konvertierungsdienst doc2doc. Er laedt eine Datei hoch, wartet
auf das Ende der Konvertierung und liefert das Ergebnis – ohne dass der Aufrufer die
REST-API selbst ansprechen muss.

- einzige Abhaengigkeit: `httpx`
- Python 3.11 oder neuer
- vollstaendig typisiert (`py.typed`)

## Installation

```bash
pip install doc2doc-client
```

## Beispiel

```python
import os
from pathlib import Path

from doc2doc_client import (
    AssetMode,
    Doc2DocClient,
    idempotency_key_for,
    strip_page_markers,
)


async def extract_text(path: Path) -> str:
    data = path.read_bytes()
    async with Doc2DocClient(os.environ["DOC2DOC_URL"], os.environ["DOC2DOC_API_KEY"]) as client:
        result = await client.convert(
            data,
            file_name=path.name,
            target_format="md",
            asset_mode=AssetMode.DISCARD,
            allow_partial=True,
            languages=["deu", "eng"],
            idempotency_key=idempotency_key_for(data),
        )
    if result.is_partial:
        print(f"{result.missing_units} Seiten fehlen")
    return strip_page_markers(result.text)
```

`convert()` laedt hoch, fragt den Status ab, bis der Job fertig ist, und laedt das Ergebnis
herunter. Zwischen zwei Abfragen wartet der Client so lange, wie der Dienst es ueber
`polling.recommendedAfterSeconds` oder `Retry-After` empfiehlt. Ein `429` und ein kurzer
Verbindungsabbruch werden innerhalb der Frist wiederholt.

`idempotency_key_for()` bildet den SHA-256 der Datei. Wird dieselbe Datei mit denselben
Angaben erneut geschickt, antwortet der Dienst mit dem bestehenden Job, statt einen zweiten
anzulegen – auch dann, wenn dieser Job bereits `FAILED` ist. Ein neuer Versuch fuer eine
gescheiterte Datei braucht deshalb einen anderen Schluessel. Weichen die Angaben bei gleichem
Schluessel ab, antwortet der Dienst `409 IDEMPOTENCY_CONFLICT`.

`strip_page_markers()` entfernt die Seitenmarker (`<!-- page N -->`, `--- page N ---`) und die
Platzhalter fehlender Seiten (`[Missing page N: extraction failed]`). Mit
`keep_missing_placeholders=True` bleiben die Platzhalter stehen.

## Konfiguration

```python
Doc2DocClient(
    base_url,  # z. B. "http://doc2doc:8000"
    api_key,  # Header X-API-Key
    timeout=30.0,  # Sekunden je HTTP-Anfrage
    max_wait_seconds=3600.0,  # Standardfrist fuer convert() und wait_for_completion()
    http_client=None,  # eigener httpx.AsyncClient; wird dann nicht geschlossen
)
```

## Methoden

| Methode | Route |
|---|---|
| `submit(source, *, target_format, ...)` | `POST /v1/conversions` |
| `get_status(id)` | `GET /v1/conversions/{id}` |
| `cancel(id)` | `DELETE /v1/conversions/{id}` |
| `download_result(id)` | `GET /v1/conversions/{id}/result`, in den Speicher |
| `download_result_to(id, pfad)` | dasselbe, atomar in eine Datei |
| `get_manifest(id)` | `GET /v1/conversions/{id}/manifest` |
| `get_formats()` | `GET /v1/formats` |
| `health_live()`, `health_ready()` | `GET /health/live`, `GET /health/ready` |
| `wait_for_completion(id, ...)` | Polling bis zum Endzustand |
| `convert(source, *, target_format, ...)` | Upload, Polling, Download |

`source` ist `bytes` (dann ist `file_name` Pflicht) oder ein Pfad. Optionale Felder
(`quality_mode`, `ocr_mode`, `languages`, `page_range`, `asset_mode`, `allow_partial`,
`options`, `password`) werden nur gesendet, wenn sie gesetzt sind; sonst gilt der Standard des
Dienstes.

## Fehler

Alle Exceptions erben von `Doc2DocError`.

```python
from doc2doc_client import (
    ConversionFailedError,
    ConversionTimeoutError,
    Doc2DocApiError,
    RateLimitedError,
)

try:
    result = await client.convert(data, file_name="scan.pdf", target_format="md")
except ConversionFailedError as error:
    print(error.code)  # z. B. PDF_PAGE_EXTRACTION_FAILED
except ConversionTimeoutError as error:
    print(error.conversion_id)  # Job laeuft weiter, sofern nicht cancel_on_timeout=True
except RateLimitedError as error:
    print(error.retry_after)  # Dienst ausgelastet, Frist reichte nicht zum Warten
except Doc2DocApiError as error:
    print(error.status_code, error.code, error.request_id)
```

| Exception | Anlass |
|---|---|
| `ConversionFailedError` | Job endete `FAILED`; `code` ist der stabile Job-Fehlercode |
| `ConversionCancelledError` | Job endete `CANCELLED` |
| `ConversionExpiredError` | Job ist `EXPIRED` oder Download antwortet `410` |
| `ConversionTimeoutError` | Frist abgelaufen, bevor der Job fertig war |
| `AuthenticationError` | `401 INVALID_API_KEY` |
| `RequestRejectedError` | `413`, `415`, `422` – Upload oder Feld abgelehnt |
| `ConversionNotFoundError` | `404 CONVERSION_NOT_FOUND` |
| `IdempotencyConflictError` | `409 IDEMPOTENCY_CONFLICT` |
| `ConversionNotFinishedError` | `409 CONVERSION_NOT_FINISHED` |
| `RateLimitedError` | `429`; `retry_after` in Sekunden |
| `Doc2DocApiError` | jede andere Fehlerantwort; `code` ist `UNEXPECTED_RESPONSE`, wenn keine Fehlerhuelle kam |
| `Doc2DocConnectionError` | Dienst nicht erreichbar oder Zeitueberschreitung |
| `IntegrityError` | heruntergeladene Bytes passen nicht zum SHA-256 aus `ETag` |

Weder API-Key noch Passwort erscheinen in einer Exception oder in `repr()`.

## Lizenz

MIT
