Metadata-Version: 2.4
Name: cavs
Version: 0.1.0
Summary: Official Python SDK for CAVS Hub — the deduplicating, content-addressed artifact store for AI, CI/CD and game pipelines.
Project-URL: Homepage, https://github.com/orelvis15/cavs-sdks
Project-URL: Contract, https://github.com/orelvis15/cavs-sdks/blob/main/CONTRACT.md
Project-URL: Source, https://github.com/orelvis15/cavs-sdks/tree/main/python
Author: CAVS
License: Apache-2.0
Keywords: artifacts,cavs,content-addressed,deduplication,mlops
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
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: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.24
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Description-Content-Type: text/markdown

# cavs — Python SDK for CAVS Hub

Official Python client for [CAVS Hub](https://github.com/orelvis15/cavs-hub), the
content-addressed, deduplicating artifact store for AI, CI/CD and game pipelines.
It speaks the same canonical API the CLI uses — one API, one permission model, one
data plane. The wire behaviour is defined in [`../CONTRACT.md`](../CONTRACT.md) and
pinned by the shared vectors in [`../spec/contract/`](../spec/contract/).

- Requires **Python 3.9+**.
- Only runtime dependency: [`httpx`](https://www.python-httpx.org/).

## Install

```bash
pip install cavs
```

From this monorepo checkout:

```bash
cd python
pip install -e ".[dev]"
```

## Quickstart

```python
from cavs import CAVS

client = CAVS.from_env()          # reads CAVS_TOKEN + CAVS_API

artifact = client.artifacts.upload(
    path="./model",
    project="vision-models",
    name="resnet50",
    kind="model",
    version="1.4.0",
    metadata={"framework": "pytorch", "accuracy": 0.942},
)
print(artifact.reference)          # cavs://acme-ai/vision-models/model/resnet50:1.4.0
print(artifact.deduplication_ratio)

client.artifacts.download(artifact.reference, dest="./restored")
```

Track training runs and lineage:

```python
run = client.runs.create(project="vision-models", name="nightly", system="mlflow")
client.runs.update("vision-models", run.id, status="completed")
```

## Authentication

Create a **service account** in the CAVS dashboard and export its key. Tokens are
resolved in this order: an explicit `token=` argument, then `$CAVS_TOKEN`.

```bash
export CAVS_TOKEN=cavs_sk_...
export CAVS_API=https://api.cavs.dev        # default; use http://localhost:8080 for dev
```

Other environment variables:

| Variable | Purpose |
|---|---|
| `CAVS_TOKEN` | Auth token (`cavs_sk_…`, `cavs_pat_…`, …) |
| `CAVS_API` | Base URL (default `https://api.cavs.dev`) |
| `CAVS_ORG` | Default organization slug |
| `CAVS_CA_BUNDLE` | Path to a custom CA bundle for TLS verification |

Or construct explicitly:

```python
client = CAVS(token="cavs_sk_...", api="https://api.cavs.dev", org="acme-ai")
```

### `project` and `org` resolution

`project` is an alias for a **repository**. It is resolved within an org by
matching the value against each repository's `id`, `slug` or `name`
(`GET /organizations/{org}/repositories`); if nothing matches, the value is used
verbatim as a repository id. The org comes from an explicit argument, then
`CAVS_ORG`, then the caller's default org from `GET /users/me`.

## Security

Following [`../CONTRACT.md`](../CONTRACT.md) §2, this SDK:

- **never** prints the token in `repr`, exceptions, or logs — it is redacted to
  e.g. `cavs_sk_…abcd`;
- never logs presigned URLs or full request headers;
- verifies TLS and honours `CAVS_CA_BUNDLE`; caps redirects at 5;
- always sets connect + read timeouts;
- verifies `sha256 == oid` on every download and refuses to write a mismatched
  file (`ChecksumError`), cleaning up the temp file;
- guards against path traversal when expanding directories / archive members;
- honours `Retry-After` and cleans up temp files on failure;
- supports cancellation via a `threading.Event` (or any object with `is_set()`).

## Uploads and downloads

Large files never travel through the control plane — they stream directly to
object storage via presigned URLs (CONTRACT §5).

- **Upload**: hash every file (streamed sha256, 64 KiB chunks) → open a session
  with an `Idempotency-Key` → authorize objects (the Hub reports which already
  `exists`, so dedup and resume are automatic) → `PUT` the missing bytes →
  `complete` each object → `finalize` (idempotent), which returns dedup stats.
- **Download**: authorize → stream to a deterministic temp file while hashing →
  verify `sha256 == oid` → atomically rename into place.

Progress and cancellation:

```python
import threading

cancel = threading.Event()
client.artifacts.upload(
    path="./big-dataset",
    project="datasets",
    name="imagenet",
    kind="dataset",
    version="2026.07.24",
    on_progress=lambda done, total: print(f"{done}/{total}"),
    cancel=cancel,
)
```

## `cavs://` URIs

```python
from cavs import parse, format_uri

p = parse("cavs://acme-ai/vision-models/model/resnet50:1.4.0")
p.org, p.name, p.version        # 'acme-ai', 'resnet50', '1.4.0'
format_uri(p)                    # round-trips to the canonical form
```

## Errors

All exceptions derive from `cavs.CAVSError`. The HTTP → exception mapping follows
CONTRACT §8:

| HTTP | Exception |
|---|---|
| 401 | `AuthenticationError` |
| 403 | `AuthorizationError` |
| 404 | `NotFoundError` |
| 409 | `ConflictError` |
| 413 / 402 | `QuotaExceededError` |
| 429 | `RateLimitError` (honours `Retry-After`) |
| 5xx / network | `TemporaryServiceError` (retried, ≤4 attempts) |
| client-side checksum mismatch | `ChecksumError` |
| upload / download transport | `UploadError` / `DownloadError` |

```python
from cavs import CAVS, NotFoundError, RateLimitError

try:
    client.artifacts.get("does-not-exist")
except NotFoundError as e:
    print(e.code, e.status)
except RateLimitError as e:
    print("retry after", e.retry_after)
```

## Development

```bash
cd python
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check . && mypy src && pytest --cov=cavs --cov-fail-under=90
```

Tests mock httpx with [`respx`](https://lundberg.github.io/respx/) — no network is
used. See [`../CONTRIBUTING.md`](../CONTRIBUTING.md) and the docs in
[`../docs/`](../docs/).

## License

Apache 2.0 — see [`../LICENSE`](../LICENSE).
