Metadata-Version: 2.4
Name: sonicium
Version: 0.1.0
Summary: Official client for the Sonicium Entropy API: quantum-seeded random bytes, attested reseed material and post-quantum keypairs, with offline attestation verification.
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/hussainbillah/Sonicium-Entropy
Project-URL: Source, https://github.com/hussainbillah/Sonicium-Entropy/tree/main/sdks/python
Keywords: quantum,random,entropy,qrng,drbg,post-quantum,ml-kem,ml-dsa
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: httpx2<3,>=2.0
Requires-Dist: cryptography>=42

# sonicium

The official Python client for the Sonicium Entropy API. Sync and asyncio,
fully typed, Python 3.12+.

```bash
pip install sonicium
```

## Draw random bytes

```python
import os
from sonicium import Sonicium

sonicium = Sonicium(os.environ["SONICIUM_API_KEY"])

draw = sonicium.random(32)
draw.data            # 32 bytes
draw.register        # "Q63" | "Q135" — fixed per key
draw.drbg_algorithm  # "CTR-DRBG-AES-256"
draw.health_check    # HealthCheck(rct="passed", apt="passed", excluded_channels=())
```

Bytes are NIST SP 800-90A DRBG output from a generator seeded only by the
platform's quantum hardware. They are requested as raw binary by default —
no encoding overhead — with the provenance read from the `X-Sonicium-*`
response headers. The platform never substitutes a non-quantum source:
when the pool cannot reseed you get a `PoolEmptyError`, not weaker bytes.

## Reseed your own DRBG

```python
from sonicium import verify_attestation

seed = sonicium.quantum.seed(bits=256)
seed.data  # 32 bytes

proof = verify_attestation(
    seed.attestation,
    material=seed.data,
    public_key_hex=os.environ["SONICIUM_ATTESTATION_PUBLIC_KEY"],  # pinned, obtained out of band
)
if not proof:
    raise RuntimeError(f"Attestation failed: {proof.reason}")
```

Verification is entirely offline — an Ed25519 signature over the seed's
digest, the DRBG's reseed counter and the issue time. Without a pinned
public key the check proves the certificate is untampered and internally
consistent; with one it proves this platform issued it. Pin it.

For a fleet, `sonicium.quantum.seeds(500)` returns 500 independent seeds,
each with its own attestation, in one call.

## Post-quantum keypairs

```python
kp = sonicium.quantum.pqc_keypair("ML-KEM-768")
kp.kind         # "kem" — ML-DSA-* return "signature"; the two are not interchangeable
kp.private_key  # returned once, never stored by the platform
```

## asyncio

```python
from sonicium import AsyncSonicium

async with AsyncSonicium(os.environ["SONICIUM_API_KEY"]) as sonicium:
    draw = await sonicium.random(32)
```

## Errors and retries

Every failure is a typed exception, split on what you should do about it
rather than on the status code:

| Exception                   | Status | What to do                                                        |
| --------------------------- | ------ | ----------------------------------------------------------------- |
| `RateLimitedError`          | 429    | Wait `retry_after_seconds`. Retried automatically.                |
| `QuotaExceededError`        | 429    | The monthly allowance is spent. Not retried.                      |
| `PoolEmptyError`            | 503    | The pool is refilling. `retry_after_seconds` is a floor. Retried. |
| `UpstreamError`             | 502    | The DRBG engine was unreachable. Retried.                         |
| `NetworkError`              | —      | No response at all. Retried.                                      |
| `PayloadTooLargeError`      | 413    | Over the key's per-call limit. Split the request.                 |
| `AuthenticationError`       | 401    | Bad, revoked or expired key.                                      |
| `IdempotencyMismatchError`  | 422    | The key was already used with a different body.                   |
| `IdempotencyExhaustedError` | 409    | The key has been served its maximum times. Use a new one.         |
| `ValidationError`           | 400    | The request itself was rejected.                                  |

All of them subclass `SoniciumError`. Transient failures are retried
(`max_retries`, default 2), honouring `Retry-After`. Every POST carries an
`Idempotency-Key` — yours or a generated UUID — so a retry is charged
once, never twice. The platform serves a retry with fresh material rather
than a copy: it stores no seeds or keys, so there is nothing to replay.

## Options

```python
Sonicium(
    api_key,                 # sk_… or a device token sk_dev_…
    base_url=...,            # your own deployment; defaults to the hosted platform
    timeout=30.0,            # per attempt, seconds
    max_retries=2,
    max_retry_delay=60.0,    # cap on one wait, seconds
)
```

The client holds a connection pool; make one per process and close it
(or use it as a context manager).

## Status

`sonicium.status()` returns the same snapshot as the public status page:
each component's live state and its measured availability.
