Metadata-Version: 2.4
Name: vautra-sdk
Version: 1.0.0
Summary: Official Python SDK for Vautra projects, buckets and VS3 object storage.
Author: Vautra Technologies Inc.
Maintainer: Vautra Technologies Inc.
License: MIT
Project-URL: Homepage, https://github.com/KWP-inc/Vautra-python-sdk
Project-URL: Documentation, https://core-docs.vautra.com
Project-URL: Repository, https://github.com/KWP-inc/Vautra-python-sdk
Project-URL: Issues, https://github.com/KWP-inc/Vautra-python-sdk/issues
Project-URL: Changelog, https://github.com/KWP-inc/Vautra-python-sdk/blob/main/CHANGELOG.md
Keywords: vautra,vs3,object-storage,storage,sdk,upload
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Archiving
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: types-requests>=2.31; extra == "dev"
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# Vautra Python SDK

Python SDK for Vautra projects, buckets, and object storage.

The SDK signs every request and sends it to `https://app.vautra.com/api` by default. Keep the secret access key on a trusted server; it is a credential, not a public identifier.

## Requirements

- Python 3.9 or newer.
- `requests` 2.31 or newer (installed automatically).

## Installation

```bash
pip install vautra-sdk
```

The distribution is `vautra-sdk`; the import name is `vautra`. The npm package of
the same name is the separate Node.js SDK.

## Authentication

Create an access key in the Vautra dashboard, then build one client and reuse it — each client owns a connection pool:

```python
from vautra import Vautra

vautra = Vautra(
    access_key_id="...",
    secret_access_key="...",
)
```

Credentials fall back to the `VAUTRA_ACCESS_KEY_ID` and `VAUTRA_SECRET_ACCESS_KEY` environment variables, and `api_url` to `VAUTRA_API_URL`, when the corresponding argument is omitted:

```python
from vautra import Vautra

with Vautra() as vautra:              # reads the environment
    page = vautra.projects.list()
```

Every request carries a fresh nonce, including retries, so signed headers are never replayed. Access-key permissions and project or bucket restrictions are enforced by the Vautra backend.

Invalid configuration raises `VautraConfigError` immediately, before any request is sent. `api_url` must use `https://` unless it points at a loopback address, so signed credentials never travel over plaintext.

### Client options

| Option | Default | Description |
| --- | --- | --- |
| `access_key_id` | `$VAUTRA_ACCESS_KEY_ID` | Required. |
| `secret_access_key` | `$VAUTRA_SECRET_ACCESS_KEY` | Required. Never logged; the client redacts it in `repr()`. |
| `api_url` | `$VAUTRA_API_URL` or `https://app.vautra.com/api` | Must be `https://` for remote hosts. |
| `timeout` | `30.0` | Seconds, for non-upload requests. Use `0` or `None` to disable. |
| `session` | new session | Supply your own `requests.Session` to control proxies or TLS verification. |

Every method also accepts a per-call `timeout=` override and a `cancel_event=` (a `threading.Event`).

`Vautra` is a context manager, and `close()` releases the connection pool:

```python
with Vautra(access_key_id="...", secret_access_key="...") as vautra:
    ...
```

## Object keys

Vautra object keys are **flat file names**, not S3-style paths:

- No `/` or `\` separators — `documents/report.pdf` is rejected.
- Maximum 255 bytes (UTF-8), no control characters, no trailing period.
- The key must end in a supported extension.

Supported extensions are currently: `txt`, `csv`, `json`, `xml`, `html`, `css`, `js`, `ts`, `md`, `pdf`, `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, `zip`, `gz`, `doc`, `docx`, `xlsx`, `pptx`, `mp3`, `mp4`, `webm`, `wav`, `ogg`. Files whose contents do not match their extension are rejected on upload.

Invalid keys raise `VautraError` immediately, before an upload session is created.

## Projects

```python
page = vautra.projects.list(
    page=1,
    page_size=20,
    search="production",
    key_management="managed",
)

project = vautra.projects.get("project-id")
```

List methods return `{"data": [...], "meta": {"page", "pageSize", "total", "totalPages"}}`.

## Buckets

```python
page = vautra.buckets.list(project_id="project-id", page=1, page_size=20)

bucket = vautra.buckets.get("bucket-id")

created = vautra.buckets.create(
    project_id="project-id",
    name="documents",
    versioning_enabled=True,
)
```

Bucket deletion is intentionally not exposed by the SDK.

## List objects

```python
page = vautra.objects.list("bucket-id", page=1, page_size=20, search="invoice")
```

## Upload objects

For large local files, pass a path. The SDK reads only the active chunks instead of loading the whole file into memory:

```python
obj = vautra.objects.upload(
    bucket_id="bucket-id",
    key="movie.mp4",
    body="C:/videos/movie.mp4",     # str, pathlib.Path, or {"path": ...}
    content_type="video/mp4",
)
```

For content already in memory:

```python
vautra.objects.upload(
    bucket_id="bucket-id",
    key="hello.txt",
    body=b"Hello from Vautra",
    content_type="text/plain",
)
```

When `content_type` is omitted, Vautra derives the stored MIME type from the key's extension.

### Supported body types

| Body | Behaviour |
| --- | --- |
| `bytes`, `bytearray`, `memoryview` | Used directly. |
| `str` | Encoded as UTF-8. |
| `pathlib.Path`, `os.PathLike`, `{"path": "..."}` | Opened and read by range; memory stays flat. |
| A seekable binary file object | Read by range from its current position. The SDK does not close a handle it did not open. |
| A non-seekable stream (pipe, socket, generator of `bytes`) | Spooled to a temporary file so failed parts can be retried, then cleaned up. |
| Any object with `size` and `read(start, end)` | Used as a random-access source. |

A stream cannot be rewound, so a failed part could not otherwise be retried. Spooling keeps memory flat regardless of stream size. Prefer a path when the data is already on disk.

Text-mode file objects are rejected — open files with `"rb"`.

### Upload options

```python
import threading

cancel = threading.Event()

vautra.objects.upload(
    bucket_id="bucket-id",
    key="archive.zip",
    body="./archive.zip",
    concurrency=2,            # 1-4
    max_attempts=3,           # 1-5
    attempt_timeout=180.0,    # seconds per request
    cancel_event=cancel,
    on_started=lambda upload_id: print("session", upload_id),
    on_progress=lambda p: print(p["stage"], p["loadedBytes"], p["totalBytes"]),
)
```

Progress stages are `starting`, `uploading`, `finalizing`, and `success`. `loaded_bytes` never goes backwards and a retried part is counted once.

Part and completion requests retry network failures, HTTP `408`, `429`, and `5xx`. Retry delays are one second then three seconds. Other `4xx` responses are terminal, as are errors raised locally by the SDK.

If an upload fails after its session is created, the SDK makes a best-effort request to cancel and clean up that session. Secure cross-process resume is not supported.

SDK access keys work only with Vautra-managed projects and their buckets. Customer-managed projects are intentionally excluded from SDK access-key scopes.

## Upload status and cancellation

```python
status = vautra.objects.upload_status("upload-id")
vautra.objects.cancel_upload("upload-id")
```

`upload_status()` is for inspection only; it does not enable resume.

To cancel a running upload, set the `threading.Event` you passed as `cancel_event`:

```python
import threading

cancel = threading.Event()
worker = threading.Thread(
    target=vautra.objects.upload,
    kwargs={
        "bucket_id": "bucket-id",
        "key": "large.zip",
        "body": "./large.zip",
        "cancel_event": cancel,
    },
)
worker.start()
cancel.set()
```

The upload raises `VautraCancelledError`, and the session is cleaned up. An already-set event raises before any request is sent.

Because `requests` is synchronous, cancellation takes effect between parts and between retry attempts — it does not interrupt a request already on the wire. `attempt_timeout` bounds that window.

## Download objects

Return the object as `bytes`:

```python
data = vautra.objects.download("object-id")
```

Buffered downloads are capped at 256 MiB so a large object cannot exhaust memory. Pass `max_bytes` to change the cap, or `0` to disable it. Objects above the cap raise `VautraError` with status `413`.

Write directly to a file, which streams and is not capped:

```python
vautra.objects.download("object-id", destination="./report.pdf")
```

The file is written to a temporary sibling and renamed into place, so an interrupted download never leaves a truncated file at `destination`.

Or consume the stream yourself:

```python
with vautra.objects.download_stream("object-id") as stream:
    print(stream.content_type, stream.content_length)
    for chunk in stream:
        sink.write(chunk)
```

Downloads through the SDK are available only for Vautra-managed projects.

## Delete objects

```python
vautra.objects.delete("object-id")
```

Deletion is subject to access-key permissions and backend object-state rules.

## Errors

Every SDK error derives from `VautraError`:

```python
from vautra import VautraError

try:
    vautra.buckets.get("missing-bucket")
except VautraError as error:
    print(error.status, error.code, error.message)
```

| Exception | Raised when |
| --- | --- |
| `VautraError` | The API returned a failure. Carries `status`, `code`, `details`. |
| `VautraConfigError` | Client configuration is unusable. Raised before any request. |
| `VautraTimeoutError` | A request exceeded its time budget (`status` 408). |
| `VautraConnectionError` | The API could not be reached (`status` 0). |
| `VautraCancelledError` | The caller's `cancel_event` was set. |

Errors also expose `retryable`, which the upload retry loop uses to avoid burning attempts on failures that would repeat identically.

The SDK never follows redirects: signed credentials are not forwarded to another host, and an unexpected `3xx` surfaces as a `VautraError` with code `unexpected_redirect`.

## Thread safety

A `Vautra` client is safe to share across threads. Uploads use a bounded worker pool internally and read bodies under a lock.

## Development

```bash
python -m venv .venv && .venv/Scripts/activate    # or source .venv/bin/activate
pip install -e ".[dev]"
```

```bash
pytest
```

```bash
ruff check . && mypy
```

The test suite runs against a mock Vautra API whose signature verification is a port of the backend's verifier, so a passing test means the SDK interoperates with the real service rather than with a permissive stub.

To try a real upload:

```bash
python examples/upload_file.py ./examples/sample.txt --bucket-id BUCKET --key sample.txt
```

## License

MIT. See [LICENSE](LICENSE).
