Metadata-Version: 2.4
Name: intellidoc-sdk
Version: 0.3.0
Summary: Python SDK for IntelliDoc document extraction service
Author: NIA
Requires-Python: >=3.13
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27

# IntelliDoc SDK

SDK Python para o serviço de extração de texto IntelliDoc.

## Instalação

```bash
pip install intellidoc-sdk
```

Requisitos: Python 3.13+

## Migração 0.2.0 → 0.3.0

A versão 0.3.0 expõe os campos completos de extração que a API já produzia, e renomeia dois campos do `DocumentResult` para alinhar com a nomenclatura do servidor.

**Breaking changes:**

- `r.id` foi renomeado para `r.document_id`.
- `r.text` foi renomeado para `r.full_text`.
- O invariante mudou de `text XOR error` para `full_text XOR error`.

**Novos campos em `DocumentResult` (não-breaking, opcionais — `None` em falha):**

- `r.mime_type`, `r.overall_quality`, `r.total_pages`, `r.processing_time_ms`, `r.pages`.

**Novos tipos públicos:**

- `PageResult` — resultado por página, com `extraction_method`, `quality_score` e (para planilhas) `sheet_name`/`sheet_index`.
- `ExtractionMethod` — enum string com o método utilizado para extrair cada página.

**Antes / depois:**

```python
# 0.2.0
print(r.text or r.error)
print(r.id)

# 0.3.0
print(r.full_text or r.error)
print(r.document_id)
if r.full_text:
    print(f"Qualidade: {r.overall_quality}")
    for p in r.pages:
        print(p.extraction_method)
```

A SDK ainda está em piloto e sem clientes em produção; este bump minor pré-1.0 carrega breaking changes intencionalmente, sem alias de compatibilidade.

## Uso

Você tem **dois métodos**:

- `extract(documento)` — extrai **1 documento** e retorna o resultado direto.
- `extract_batch(documentos)` — extrai **vários documentos**, retorna um iterator que entrega cada um conforme termina.

### 1 documento

```python
from intellidoc_sdk import IntelliDocClient

client = IntelliDocClient(url="http://intellidoc:8000", api_key="sua-chave")

r = client.extract("/dados/contrato.pdf")

print(r.full_text or r.error)
```

### Vários documentos

```python
from intellidoc_sdk import IntelliDocClient

client = IntelliDocClient(url="http://intellidoc:8000", api_key="sua-chave")

for r in client.extract_batch(["/dados/contrato.pdf", "/dados/cert.html"]):
    print(r.filename, r.full_text or r.error)
```

Resultados saem conforme cada documento termina — você não espera o batch inteiro. Falhas em um documento não interrompem os outros (chegam em `r.error`).

## O objeto retornado (DocumentResult)

Ambos os métodos retornam objetos `DocumentResult`. Os quatro primeiros campos são o contrato mínimo; os cinco últimos carregam metadados de extração e só vêm preenchidos em sucesso.

| Campo | Tipo | Quando vem populado |
|---|---|---|
| `r.document_id` | `str \| None` | sempre que o documento foi aceito (rejeição no upload deixa `None`) |
| `r.filename` | `str` | sempre |
| `r.full_text` | `str \| None` | **só em sucesso** (`None` em qualquer falha) |
| `r.error` | `str \| None` | **só em falha** (`None` em sucesso) |
| `r.mime_type` | `str \| None` | só em sucesso |
| `r.overall_quality` | `float \| None` | só em sucesso |
| `r.total_pages` | `int \| None` | só em sucesso |
| `r.processing_time_ms` | `int \| None` | só em sucesso |
| `r.pages` | `list[PageResult] \| None` | `None` em falha; `list` (eventualmente vazia) em sucesso |

**Invariante:** ou tem `full_text` ou tem `error`, nunca os dois ao mesmo tempo.

```python
if r.error:
    # falhou — r.full_text é None, r.error tem a mensagem
    print(f"{r.filename} falhou: {r.error}")
else:
    # deu certo — r.full_text tem o texto extraído, r.error é None
    print(f"{r.filename}: {len(r.full_text)} caracteres")
```

**Distinção semântica de `pages`:**

- `r.pages is None` — extração falhou. Nenhuma informação por página está disponível.
- `r.pages == []` — extração teve sucesso mas o servidor reportou zero páginas (caso degenerado mas válido, ex. documento vazio).

## Páginas e método de extração

Cada `DocumentResult` em sucesso traz `r.pages: list[PageResult]`. Um `PageResult` descreve uma página com:

| Campo | Tipo | Descrição |
|---|---|---|
| `page.page_number` | `int` | número da página (1-indexed) |
| `page.content` | `str` | texto extraído daquela página |
| `page.extraction_method` | `ExtractionMethod` | método usado para extrair (PDF nativo, OCR, etc.) |
| `page.quality_score` | `float` | qualidade da extração no intervalo `[0.0, 1.0]` |
| `page.sheet_name` | `str \| None` | nome da aba (somente planilhas) |
| `page.sheet_index` | `int \| None` | índice 0-based da aba (somente planilhas) |

Iteração padrão:

```python
r = client.extract("/dados/contrato.pdf")
if r.full_text:
    print(f"{r.mime_type}, qualidade {r.overall_quality:.2f}, {r.total_pages} páginas")
    for page in r.pages:
        print(f"  página {page.page_number} ({page.extraction_method.value}): {len(page.content)} chars")
```

Em planilhas (XLSX/XLS/ODS) as páginas correspondem às abas e ganham `sheet_name`/`sheet_index`:

```python
r = client.extract("/dados/vendas.xlsx")
for page in r.pages:
    if page.sheet_name:
        print(f"Aba '{page.sheet_name}' (index {page.sheet_index}): {page.content[:80]}")
```

### `ExtractionMethod`

Enum string exportado em `intellidoc_sdk`. Valores possíveis:

| Membro | Valor | Quando aparece |
|---|---|---|
| `ExtractionMethod.PDF_NATIVE` | `"pdf_native"` | PDF com texto embutido |
| `ExtractionMethod.OCR_AZURE` | `"ocr_azure"` | imagens e PDFs escaneados (OCR) |
| `ExtractionMethod.HTML_PARSER` | `"html_parser"` | HTML |
| `ExtractionMethod.XML_PARSER` | `"xml_parser"` | XML |
| `ExtractionMethod.TEXT_DECODE` | `"text_decode"` | TXT (decodificação direta) |
| `ExtractionMethod.DOCX_PARSER` | `"docx_parser"` | DOCX |
| `ExtractionMethod.SPREADSHEET_PARSER` | `"spreadsheet_parser"` | XLSX, XLS, ODS, CSV |
| `ExtractionMethod.UNKNOWN` | `"unknown"` | fallback quando a SDK recebe um valor que não conhece |

`UNKNOWN` é o fallback local da SDK quando o servidor envia um método novo (não conhecido por esta versão da SDK). Geralmente é sinal de que vale atualizar a SDK:

```python
from intellidoc_sdk import ExtractionMethod

if page.extraction_method is ExtractionMethod.UNKNOWN:
    logger.warning("Método de extração desconhecido — considere atualizar a SDK")
```

Padrão recomendado de tratamento por método (pattern matching):

```python
from intellidoc_sdk import ExtractionMethod

match page.extraction_method:
    case ExtractionMethod.PDF_NATIVE:
        ...
    case ExtractionMethod.OCR_AZURE:
        ...
    case ExtractionMethod.UNKNOWN:
        ...
```

## Tipos de input

Tanto `extract` quanto `extract_batch` aceitam os mesmos formatos. A diferença é que `extract` recebe **um** desses, e `extract_batch` recebe uma **lista**.

### Arquivo no disco

Passa o path como `str`. O filename vem do basename:

```python
client.extract("/dados/contrato.pdf")                  # filename = "contrato.pdf"

client.extract_batch([
    "/dados/contrato.pdf",                              # filename = "contrato.pdf"
    "/dados/cert.html",                                 # filename = "cert.html"
])
```

### Bytes em memória

Dict com `filename` obrigatório e `content` em `bytes`:

```python
pdf_bytes = ...  # vindo de upload, S3, geração programática, etc

client.extract({"filename": "contrato.pdf", "content": pdf_bytes})

client.extract_batch([
    {"filename": "a.pdf", "content": pdf_bytes_a},
    {"filename": "b.pdf", "content": pdf_bytes_b},
])
```

### Base64 string

Dict com `filename` obrigatório e `content` como `str` (assumido base64):

```python
client.extract({"filename": "contrato.pdf", "content": "JVBERi0xLjQK..."})
```

Use quando o documento já chegou como base64 (de fila, banco, payload JSON).

### Misturando formatos

`extract_batch` aceita formatos diferentes na mesma chamada:

```python
client.extract_batch([
    "/dados/contrato.pdf",                              # disco
    {"filename": "doc.pdf", "content": pdf_bytes},      # bytes
    {"filename": "fila.pdf", "content": b64_string},    # base64
])
```

## Filename: quando é obrigatório

| Input | Filename |
|---|---|
| Path (`str`) | automático — vem do basename |
| Dict (bytes ou base64) | **obrigatório** — você fornece em `"filename"` |

Se omitir o filename num dict, a SDK levanta `ValidationError` antes de qualquer requisição HTTP.

## Tratamento de erros

Erros **por documento** vêm em `r.error`. Em `extract_batch`, não interrompem os outros.

Erros que afetam a **requisição inteira** ou seu uso da SDK levantam exceção em ambos os métodos:

```python
from intellidoc_sdk import (
    IntelliDocClient,
    AuthenticationError,
    ValidationError,
    ServiceUnavailableError,
)

client = IntelliDocClient(url="...", api_key="...")

try:
    r = client.extract("/dados/contrato.pdf")
    print(r.full_text or r.error)
except AuthenticationError:
    # API key inválida ou ausente (HTTP 401/403)
    ...
except ValidationError as e:
    # batch rejeitado (HTTP 400/422) ou input mal-formado pré-HTTP
    # (dict sem filename, tipo de input não suportado, etc)
    print(f"Requisição rejeitada: {e}")
except ServiceUnavailableError:
    # IntelliDoc fora do ar ou erro de rede
    ...
```

## Uso em código async (FastAPI, aiohttp, Starlette)

Importa de `intellidoc_sdk.aio` em vez do namespace raiz. Mesma API, com `async`/`await`:

```python
from intellidoc_sdk.aio import IntelliDocClient

async with IntelliDocClient(url="...", api_key="...") as client:
    # 1 documento
    r = await client.extract("/dados/contrato.pdf")
    print(r.full_text or r.error)

    # vários documentos
    async for r in client.extract_batch(["a.pdf", "b.pdf"]):
        print(r.filename, r.full_text or r.error)
```

## Formatos suportados

| Categoria | Formatos |
|---|---|
| Documentos | PDF, DOCX |
| Planilhas | XLSX, XLS, ODS, CSV |
| Texto | HTML, XML, TXT |
| Imagens | JPEG, PNG, TIFF, BMP, WEBP, HEIF/HEIC |
