Metadata-Version: 2.4
Name: harbor-oci-client
Version: 0.1.0
Summary: Harbor REST API and ORAS push/pull client for SuperNova artifacts
License-Expression: Apache-2.0
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.28
Requires-Dist: oras>=0.2.42
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
Requires-Dist: ruff>=0.8; extra == "dev"
Dynamic: license-file

# harbor-oci-client

Harbor REST API operations and ORAS push/pull convenience wrappers for SuperNova artifacts.

## Install

```bash
pip install harbor-oci-client
```

Or install directly from the repository:

```bash
pip install git+https://github.com/DamitDev/harbor-oci-client.git
```

Requires Python >= 3.12.

## Usage

### HarborClient (async)

Wraps Harbor's OCI Distribution API and Harbor v2.0 REST API with token caching and automatic retries.

```python
from harbor_oci_client import HarborClient

client = HarborClient(
    base_url="https://harbor.example.com",
    username="robot_myproject+ci",
    password="...",
)

# Verify an artifact exists (OCI Distribution HEAD)
info = await client.verify_artifact("harbor.example.com/myproject/my-model:v3")
# info.digest -> "sha256:abc123..."
# info.content_length -> 640

# Get rich metadata (Harbor v2.0 API)
detail = await client.get_artifact_info("harbor.example.com/myproject/my-model:v3")
# detail.digest, detail.size, detail.media_type, detail.push_time, detail.tags

# Delete an artifact (Harbor v2.0 API)
await client.delete_artifact("harbor.example.com/myproject/my-model:v3")

await client.close()
```

### OrasHelper (sync)

Thin wrapper around `oras-py` for push/pull. Async consumers should use `asyncio.to_thread()`.

```python
from harbor_oci_client import OrasHelper

oras = OrasHelper(
    hostname="harbor.example.com",
    username="robot_myproject+ci",
    password="...",
)

# Pull
files = oras.pull("harbor.example.com/myproject/my-model:v3", outdir="/tmp/model")

# Push (one layer per file)
result = oras.push(
    "harbor.example.com/myproject/my-model:v4",
    files=["config.json", "model.safetensors"],
    workdir="/workspace/output",
)

# Push with custom media types
result = oras.push_custom(
    "harbor.example.com/myproject/my-model:v4",
    config_path="/workspace/config.json",
    content_path="/workspace/model-dir",
    category="model",  # or "dataset"
)
```

### Ref Parsing

```python
from harbor_oci_client import parse_ref

ref = parse_ref("harbor.example.com/myproject/my-model:v3")
# ref.host, ref.repository, ref.reference, ref.project, ref.repo_name
```

### Media Types

```python
from harbor_oci_client import media_types

media_types.MODEL_CONFIG     # "application/vnd.supernova.model.config.v1+json"
media_types.MODEL_WEIGHTS    # "application/vnd.supernova.model.weights.v1+tar+gzip"
media_types.DATASET_CONFIG   # "application/vnd.supernova.dataset.config.v1+json"
media_types.DATASET_CONTENT  # "application/vnd.supernova.dataset.content.v1+tar+gzip"
media_types.OCI_MANIFEST     # "application/vnd.oci.image.manifest.v1+json"
```

### Exceptions

All inherit from `HarborError` and carry `detail: str` and `status_code: int | None`.

| Exception | Meaning |
|---|---|
| `HarborConnectionError` | Registry unreachable, DNS failure, timeout |
| `HarborAuthError` | 401/403 from token endpoint or API |
| `ArtifactNotFoundError` | 404 — also carries `.harbor_ref` |
| `HarborAPIError` | Any other unexpected HTTP status |

## Testing

```bash
# Unit tests (no Harbor needed)
pip install -e ".[dev]"
pytest

# Integration tests (live Harbor)
HARBOR_URL=https://harbor.example.com \
HARBOR_USERNAME=robot_myproject+ci \
HARBOR_PASSWORD=... \
pytest tests/test_integration.py --integration -v
```

## License

Apache 2.0
