Metadata-Version: 2.4
Name: spreadspace
Version: 0.1.1
Summary: Official Python SDK for the SpreadSpace API.
Project-URL: Homepage, https://spreadspace.ai
Project-URL: Documentation, https://docs.spreadspace.ai
Project-URL: Source, https://github.com/spreadspace
Author: SpreadSpace
License: MIT
Keywords: api,document,extraction,lending,sdk,spreadspace
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# SpreadSpace Python SDK

Official Python client for the [SpreadSpace API](https://docs.spreadspace.ai) —
document extraction and financial spreading for lending.

> Some examples below require the generated core (built in CI) or a live sandbox
> key. They are marked **[needs sandbox key]** / **[needs generated core]**.

## Install

```bash
pip install spreadspace
```

Requires Python 3.9+.

## Authentication

```python
from spreadspace import SpreadSpace

client = SpreadSpace(api_key="ss_test_...")   # or omit and set SPREADSPACE_API_KEY
```

The key prefix selects the environment:

- `ss_test_...` — routes to your **sandbox tenant** (seed data, safe to experiment).
- `ss_live_...` — routes to your live tenant (real data).

If `api_key` is omitted, the client reads `SPREADSPACE_API_KEY` from the
environment. Never hard-code a live key; never commit any key.

## Client options

```python
client = SpreadSpace(
    api_key="ss_test_...",
    base_url="https://api.spreadspace.ai",   # override for a private deployment
    api_version="2026-05-03",                # pins the SpreadSpace-Version header
    timeout=60.0,                            # seconds, per request
    max_retries=2,                           # 429 + 5xx + transport errors
)
```

### API version pinning

Every request sends a dated `SpreadSpace-Version` header. The SDK pins a default
version per release (decoupled from the SDK's own semver). Pin it explicitly to
insulate your integration from server-side changes, and override per call when
you need a newer surface:

```python
client.borrowers.list(api_version="2026-06-01")   # one call on a newer version
```

## Pagination (lazy, auto cursor)

List endpoints return a lazy iterator that walks cursors for you — it fetches the
next page only as you consume it.

```python
for borrower in client.borrowers.list():
    print(borrower["id"])

# Filters pass straight through and persist across pages:
for job in client.jobs.list(status="completed", limit=50):
    print(job["id"])
```

## Async export + wait

Exports are asynchronous operations. `create` returns a handle; `wait` polls to
completion (or raises on timeout).

```python
op = client.exports.create(borrower_id="...", format="xlsx")
result = op.wait(timeout=300)   # seconds
print(result["download_url"])
```

## Upload a document + wait for processing

```python
job = client.documents.upload("statement.pdf", borrower_id="...")
final = job.wait(timeout=600)   # seconds
print(final["status"])
```

The upload helper requests a presigned URL, PUTs the file bytes directly to
storage with the matching `Content-Type` (part of the V4 signature), then returns
a job handle you can `wait` on.

## Error handling

All errors derive from `SpreadSpaceError`. Match on the typed subclass, never on
the message string:

```python
from spreadspace import (
    SpreadSpaceError,       # base
    NetworkError,           # transport failure, no HTTP response
    BadRequestError,        # 400
    AuthenticationError,    # 401
    PermissionDeniedError,  # 403
    NotFoundError,          # 404
    ConflictError,          # 409
    RateLimitError,         # 429
    InternalServerError,    # 5xx
)

try:
    client.borrowers.get("missing-id")
except RateLimitError as e:
    print("retry after", e.retry_after, "seconds")
except NotFoundError as e:
    print("not found")
except SpreadSpaceError as e:
    # Every error carries request_id — quote it in support tickets.
    print(e.message, e.status_code, e.request_id)
```

`request_id` comes from the `X-Request-ID` response header (falling back to the
error body). Transient failures (429, 5xx, transport errors) are retried
automatically up to `max_retries` with exponential backoff + full jitter,
honoring `Retry-After`.

## Money is exact

Monetary values decode as `decimal.Decimal`, not `float` — the SDK reads the
literal digits off the wire, so amounts are exact with no float rounding:

```python
b = client.borrowers.get("...")          # [needs sandbox key]
assert b["total_revenue"] == Decimal("1234.56")   # never 1234.5600000000001
```

## Development

The generated OpenAPI core lives in `src/spreadspace/_generated/` and is built in
CI (`scripts/generate.sh`, Java-based — not run locally). It is **gitignored and
must never be hand-edited**. The hand-written ergonomic layer (transport,
helpers, typed errors) is the only code committed by hand.

```bash
pip install -e '.[dev]'
pytest
```

## License

MIT
