Metadata-Version: 2.4
Name: quantumapi
Version: 0.25.0
Summary: Official Python SDK for QuantumAPI - Quantum-safe encryption and identity platform
Author-email: QuantumAPI Team <developers@quantumapi.eu>
License-Expression: MIT
Project-URL: Homepage, https://quantumapi.eu
Project-URL: Documentation, https://docs.quantumapi.eu/sdk/python
Project-URL: Repository, https://github.com/victorZKov/quantumapi
Project-URL: Bug Tracker, https://github.com/victorZKov/quantumapi/issues
Project-URL: Changelog, https://github.com/victorZKov/quantumapi/blob/main/src/sdk/python-quantumapi/CHANGELOG.md
Keywords: quantum,encryption,post-quantum,cryptography,security,api,sdk,ml-kem,ml-dsa,pqc
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
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: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1.0.0,>=0.27.0
Requires-Dist: pydantic<3.0.0,>=2.0.0
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
Requires-Dist: cryptography<43.0.0,>=42.0.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
Requires-Dist: pytest-httpx>=0.30.0; extra == "dev"
Requires-Dist: black>=24.0.0; extra == "dev"
Requires-Dist: isort>=5.13.0; extra == "dev"
Requires-Dist: mypy>=1.8.0; extra == "dev"
Requires-Dist: flake8>=7.0.0; extra == "dev"
Requires-Dist: respx>=0.21.0; extra == "dev"
Dynamic: license-file

# QuantumAPI Python SDK

[![PyPI version](https://badge.fury.io/py/quantumapi.svg)](https://badge.fury.io/py/quantumapi)
[![Python Support](https://img.shields.io/pypi/pyversions/quantumapi)](https://pypi.org/project/quantumapi/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/quantumapi/quantumapi-python/blob/main/LICENSE)
[![Documentation](https://img.shields.io/badge/docs-quantumapi.eu-brightgreen)](https://docs.quantumapi.eu/sdk/python)

Official Python SDK for [QuantumAPI](https://quantumapi.eu) - The European quantum-safe encryption and identity platform.

## Features

- 🔐 **Quantum-Safe Encryption** - ML-KEM-768, ML-DSA-65, and hybrid encryption
- 🔑 **Key Management** - Generate, rotate, and manage cryptographic keys
- 🗄️ **Secret Vault** - Securely store and manage secrets with versioning
- 👤 **User Management** - Create and manage end-users with roles
- 🔍 **Audit Logs** - Query comprehensive audit trails
- 💳 **Billing Integration** - Access subscription and usage metrics
- 🚀 **Async/Await Support** - Full async support with httpx
- ⚡ **Automatic Retries** - Exponential backoff for transient failures
- 📊 **Rate Limiting** - Client-side rate limit tracking
- 🎯 **Type Safety** - Complete type hints with Pydantic models

## Installation

```bash
pip install quantumapi
```

For development:

```bash
pip install quantumapi[dev]
```

## Quick Start

```python
from quantumapi import QuantumAPIClient

# Initialize client with API key
client = QuantumAPIClient(api_key="qapi_xxx")

# Encrypt data
ciphertext = await client.encryption.encrypt(
    plaintext="secret data",
    key_id="550e8400-e29b-41d4-a716-446655440000"
)

# Store a secret
secret = await client.secrets.create(
    name="database_password",
    value="super_secret_password",
    labels=["production", "database"]
)

# Generate a new key pair
key = await client.keys.generate(
    name="encryption-key-2024",
    algorithm="ML-KEM-768",
    key_type="encryption"
)

# Retrieve secret
secret_value = await client.secrets.get(secret.id)
print(f"Secret: {secret_value.value}")
```

## Synchronous API

If you prefer synchronous code, use the `QuantumAPISyncClient`:

```python
from quantumapi import QuantumAPISyncClient

client = QuantumAPISyncClient(api_key="qapi_xxx")

# All methods work the same without await
ciphertext = client.encryption.encrypt(plaintext="secret data")
```

## Configuration

The SDK can be configured via environment variables or constructor arguments:

```python
client = QuantumAPIClient(
    api_key="qapi_xxx",              # Or set QUANTUMAPI_API_KEY
    base_url="https://api.quantumapi.eu",  # Optional, defaults to production
    timeout=30.0,                     # Request timeout in seconds
    max_retries=3,                    # Max retry attempts for failed requests
    enable_logging=True,              # Enable detailed logging
)
```

### Environment Variables

- `QUANTUMAPI_API_KEY` - Your API key
- `QUANTUMAPI_BASE_URL` - API base URL (default: https://api.quantumapi.eu)
- `QUANTUMAPI_TIMEOUT` - Request timeout in seconds (default: 30)
- `QUANTUMAPI_MAX_RETRIES` - Maximum retry attempts (default: 3)

## Core Modules

### Encryption Client

```python
# Encrypt data
result = await client.encryption.encrypt(
    plaintext="sensitive data",
    key_id="key-id",  # Optional, uses default key if not specified
    encoding="utf8"   # or "base64" for binary data
)

# Decrypt data
plaintext = await client.encryption.decrypt(
    encrypted_payload=result.encrypted_payload
)

# Sign data
signature = await client.encryption.sign(
    data="message to sign",
    key_id="signing-key-id"
)

# Verify signature
is_valid = await client.encryption.verify(
    data="message to sign",
    signature=signature.signature,
    key_id="signing-key-id"
)
```

### Audit Client

Requires an API key with the `audit:read` scope (403 otherwise) or a JWT owner/admin.

```python
logs = await client.audit.list(
    category="keys",
    action=["Encrypt", "Decrypt"],   # or "Encrypt,Decrypt"
    key_id="key-id",
    from_="2026-10-01T00:00:00Z",
    page=1,
    page_size=100,                   # max 200
)
for e in logs.items:
    print(e.timestamp, e.action, e.actor, e.outcome)
print(logs.total_count)
```

Creating or importing a key with a name already in use raises `ConflictError`
(`error_code == "key_name_conflict"`).

### Keys Client

```python
# Generate key pair
key = await client.keys.generate(
    name="my-encryption-key",
    key_type="encryption",
    algorithm="ML-KEM-768",
    set_as_default=True
)

# List keys
keys = await client.keys.list(
    key_type="encryption",
    page=1,
    page_size=50
)

# Get key details
key = await client.keys.get(key_id="key-id")

# Rotate key
new_key = await client.keys.rotate(key_id="key-id")

# Delete key
await client.keys.delete(key_id="key-id")

# Set default key
await client.keys.set_default(key_id="key-id", key_type="encryption")
```

### Secrets Client

```python
# Create secret
secret = await client.secrets.create(
    name="api-key",
    value="secret_value",
    content_type="api-key",
    labels=["prod", "stripe"],
    expires_at="2025-12-31T23:59:59Z"
)

# List secrets
secrets = await client.secrets.list(
    content_type="password",
    label="production",
    page=1
)

# Get secret
secret = await client.secrets.get(secret_id="secret-id")

# Update secret value
await client.secrets.update_value(
    secret_id="secret-id",
    value="new_secret_value"
)

# Update secret metadata
await client.secrets.update(
    secret_id="secret-id",
    name="new-name",
    labels=["staging"]
)

# Delete secret
await client.secrets.delete(secret_id="secret-id")

# Version history, newest first. Values are included, so this needs reveal
# permission. max_versions is 1-20 (None = the API default of 5).
history = await client.secrets.list_versions(secret_id, max_versions=10)
for v in history.versions:
    print(v.version, v.changed_by, v.created_at)

# The secret as it was at version 1
v1 = await client.secrets.get_version(secret_id, 1)

# Roll back to version 1: reads it and writes its value back as a new version,
# audited as a restore. Two requests (not atomic); only the value is restored.
await client.secrets.rollback(secret_id, 1)
```

#### Share links

```python
# Encrypted, time-limited link for someone outside the tenant
link = await client.secrets.create_shared_link(
    secret_id,
    expires_in_hours=48,    # 1-720, default 24
    max_access_count=5,     # None = unlimited until it expires
    password="optional-password",
)
# The token is returned only now: the server does not store it.
print(client.secrets.share_url(link.token))
# -> https://portal.quantumapi.eu/share/<token> (derived from base_url, or pass your portal URL)

# Links of a secret. status is a string (Active, Revoked, Expired, Exhausted, Retired, ...);
# token is always "" here.
links = await client.secrets.list_shared_links(secret_id)
await client.secrets.revoke_shared_link(links.links[0].id)

# Open a link as the recipient (public endpoint: no API key sent, never retried,
# counts one access). Errors: NotFoundError (unknown or expired), ValidationError with
# error_code password_required / invalid_password / link_exhausted, QuantumAPIError
# with error_code link_retired (HTTP 410).
opened = await client.secrets.open_shared_link(link.token, password="optional-password")
```

### Users Client

```python
# Create user
user = await client.users.create(
    email="user@example.com",
    name="John Doe",
    role="developer"
)

# List users
users = await client.users.list(active_only=True)

# Update user
await client.users.update(
    user_id="user-id",
    name="Jane Doe",
    role="admin"
)

# Deactivate user
await client.users.deactivate(user_id="user-id")
```

### End-Users Client

End-users are the people who sign in through one of your OIDC applications (not your tenant's team members).

```python
# List the users in an application's pool
pool = await client.end_users.list(app_id)

# Clear the automatic sign-in lockout (distinct from an administrative block)
await client.end_users.unlock(app_id, user_id)

# Generate a one-time temporary password; the user must change it at next login
temp = await client.end_users.generate_temporary_password(app_id, user_id)
print(f"Hand this to {temp.email} over a secure channel: {temp.temporary_password}")
# The plaintext is returned once and is never stored in clear text.
```

### Applications Client

```python
# Create OIDC application
app = await client.applications.create(
    name="My App",
    redirect_uris=["https://myapp.com/callback"],
    scopes=["openid", "profile", "email"]
)

# List applications
apps = await client.applications.list()

# Update application
await client.applications.update(
    app_id="app-id",
    redirect_uris=["https://myapp.com/callback", "https://myapp.com/callback2"]
)
```

### Audit Client

```python
# Query audit logs
logs = await client.audit.list(
    action="secret_accessed",
    resource_type="Secret",
    start_date="2024-01-01",
    end_date="2024-12-31",
    page=1
)

# Export audit logs
csv_data = await client.audit.export(
    format="csv",
    start_date="2024-01-01",
    end_date="2024-12-31"
)
```

### Billing Client

```python
# Get subscription details
subscription = await client.billing.get_subscription()

# Get usage metrics
usage = await client.billing.get_usage(
    start_date="2024-01-01",
    end_date="2024-01-31"
)

# List invoices
invoices = await client.billing.list_invoices()
```

### Health Client

```python
# Check API health
health = await client.health.check()
print(f"API Status: {health.status}")

# Get API version
version = await client.health.version()
print(f"API Version: {version.version}")

# Get rate limit status
rate_limit = await client.health.rate_limit()
print(f"Remaining: {rate_limit.remaining}/{rate_limit.limit}")
```

### Transit (encryption as a service)

`client.transit` encrypts, decrypts, rewraps, signs and MACs with named keys. Raw `bytes` are
base64-encoded for you; a `str` is sent as-is (the API wire format is base64).

```python
key = await client.transit.create_key("orders")                 # POST /api/v1/keys
enc = await client.transit.encrypt("orders", b"4111 1111 1111")  # vault:v1:...
dec = await client.transit.decrypt("orders", enc.ciphertext)
assert dec.plaintext_bytes == b"4111 1111 1111"

await client.transit.rotate_key(key.id)                          # new version
new = await client.transit.rewrap("orders", enc.ciphertext)      # moves to the latest version

dk = await client.transit.generate_data_key("orders", key_bytes=32)   # envelope encryption
mac = await client.transit.hmac("orders", b"payload", algorithm="sha256")
sig = await client.transit.sign("signing-key", b"payload")
await client.transit.set_min_decryption_version("orders", 2)      # older versions then raise KeyVersionBelowMinimumError
history = await client.keys.list_versions(key.id)                  # versions[] with state and dates
rows = await client.keys.list(all_versions=True)                   # per-version rows (default: one row per key)
ok = (await client.transit.verify("signing-key", b"payload", sig.signature)).valid
batch = await client.transit.batch_encrypt("orders", [b"a", b"b"])    # up to 100 items
```

Errors: `KeyRevokedError` (403 `key_revoked`) and `InsufficientScopeError` (403 `insufficient_scope`),
both subclasses of `PermissionError`.

### Compliance Client

Scores for GDPR, NIS2, DORA and eIDAS 2 (`gdpr`, `nis2`, `dora`, `eidas2`), plus risk
acceptances of failing or warning checks. Tenant owner/admin only. Risk acceptances need
platform 0.27.0 or later.

```python
from datetime import datetime, timezone
from quantumapi.models import ComplianceCheckStatus

overview = await client.compliance.get_overview()
for fw in overview.frameworks:
    print(fw.framework, fw.score, fw.fail_count, fw.risk_accepted_count)

report = await client.compliance.get_report("dora")
for check in report.checks:
    # status: Pass, Warning, Fail, NotApplicable or RiskAccepted (unknown future values stay strings).
    # evaluated_status is the status before applying a risk acceptance.
    print(check.id, check.status, check.evaluated_status)

# Accept the risk of a failing/warning check (justification 10-2000 chars, review_date in the future).
# Accepting again replaces the earlier acceptance. ConflictError (409) if the check is passing,
# NotFoundError (404) for an unknown check.
acceptance = await client.compliance.accept_risk(
    "dora",
    "operational-monitoring",
    "Logs are shipped by our on-prem collector; SIEM integration planned for Q1.",
    review_date=datetime(2027, 1, 31, tzinfo=timezone.utc),
)

# Revoke it (returns the revoked acceptance; NotFoundError if none is open)
revoked = await client.compliance.revoke_risk_acceptance("dora", "operational-monitoring")

# Full history, newest first, including revoked and expired acceptances
history = await client.compliance.list_risk_acceptances()
for a in history.items:
    print(a.check_id, a.accepted_by, a.is_active, a.is_expired)
```

### Log Streams Client

Streams authentication logs to a Webhook, Datadog, Splunk or the built-in File destination
(daily gzip NDJSON archives you download). Tenant owner/admin only. The File destination needs
platform 0.27.0 or later. `config_json` is a JSON-encoded string on the wire.

```python
from datetime import date
from quantumapi.models import LogStreamType, file_stream_config

# File destination: retention 7, 30 (default) or 365 days. file_stream_config raises ValueError otherwise.
stream = await client.log_streams.create("SIEM archive", LogStreamType.FILE, file_stream_config(365))
print(stream.retention_days)  # 365

# Other destinations take their own config string
await client.log_streams.create("SIEM", "Webhook", '{"url": "https://siem.example.com/ingest"}')

streams = await client.log_streams.list()
await client.log_streams.update(stream.id, "SIEM archive", "File", file_stream_config(30))
result = await client.log_streams.test(stream.id)          # File: checks the destination is writable
await client.log_streams.deactivate(stream.id)
await client.log_streams.activate(stream.id)

# Daily archives (UTC days), newest first
files = await client.log_streams.list_files(stream.id)
for d in files.days:
    print(d.day, d.line_count, d.size_bytes)

# Download one day as raw gzip bytes (not decompressed; may hold several gzip members,
# which gzip.decompress handles). day is a datetime.date or "YYYY-MM-DD".
data = await client.log_streams.download_file(stream.id, date(2026, 10, 24))

# Or write it to disk. A directory gets the default name auth-logs-<day>.ndjson.gz.
path = await client.log_streams.download_file_to(stream.id, "2026-10-24", "./archives")

await client.log_streams.delete(stream.id)
```

### Usage Client

Storage occupied by the tenant this month, in total and per category (`secrets`, `keys`,
`certificates`, `attachments`, `log_archives`). Any tenant role. Informative only: not invoiced
or enforced yet. Usage is priced by space occupied, never per user, key or secret. Needs platform
0.27.0 or later.

```python
storage = await client.usage.get_storage()
print(storage.period_start, storage.period_end, storage.as_of)
print(storage.total_bytes, storage.month_average_bytes, storage.gb_months_accrued)  # 1 GB = 2^30 bytes
for c in storage.categories:
    print(c.id, c.bytes, c.items, c.gb_months_accrued)
print(storage.category("log_archives"))
```

## Error Handling

The SDK provides typed exceptions for all error scenarios:

```python
from quantumapi import (
    QuantumAPIClient,
    AuthenticationError,
    PermissionError,
    NotFoundError,
    RateLimitError,
    ValidationError,
    ServerError,
    NetworkError,
)

try:
    secret = await client.secrets.get("invalid-id")
except NotFoundError as e:
    print(f"Secret not found: {e.message}")
except AuthenticationError as e:
    print(f"Authentication failed: {e.message}")
except RateLimitError as e:
    print(f"Rate limit exceeded. Retry after {e.retry_after} seconds")
except ServerError as e:
    print(f"Server error: {e.message}")
except NetworkError as e:
    print(f"Network error: {e.message}")
```

## Pagination

List methods support automatic pagination:

```python
# Manual pagination
secrets = await client.secrets.list(page=1, page_size=100)

# Iterate through all pages
all_secrets = []
page = 1
while True:
    result = await client.secrets.list(page=page, page_size=100)
    all_secrets.extend(result.secrets)
    if not result.has_more:
        break
    page += 1
```

## Webhook Verification

Verify webhook signatures from QuantumAPI:

```python
from quantumapi.webhooks import verify_signature

# Verify webhook payload
is_valid = verify_signature(
    payload=request.body,
    signature=request.headers["X-QuantumAPI-Signature"],
    secret="your_webhook_secret"
)

if is_valid:
    # Process webhook event
    pass
```

## Logging

Enable detailed logging for debugging:

```python
import logging

logging.basicConfig(level=logging.DEBUG)

client = QuantumAPIClient(
    api_key="qapi_xxx",
    enable_logging=True
)
```

## Examples

Check out the [examples/](./examples/) directory for more comprehensive examples:

**Basic Examples:**
- [01_encryption_basics.py](./examples/01_encryption_basics.py) - Basic encryption and decryption
- [02_secret_management.py](./examples/02_secret_management.py) - Secret CRUD operations
- [03_key_management.py](./examples/03_key_management.py) - Key generation and rotation
- [04_error_handling.py](./examples/04_error_handling.py) - Error handling patterns
- [05_health_monitoring.py](./examples/05_health_monitoring.py) - Health checks and monitoring
- [06_configuration.py](./examples/06_configuration.py) - Configuration options

**Advanced Secret Management:**
- [07_versioning.py](./examples/07_versioning.py) - Secret versioning and rotation
- [08_sharing.py](./examples/08_sharing.py) - Secure secret sharing with access controls
- [09_batch_operations.py](./examples/09_batch_operations.py) - Batch operations at scale

Run any example with:

```bash
export QUANTUMAPI_API_KEY="qapi_your_key_here"
python examples/01_encryption_basics.py
```

## Framework Integrations

### Django

```bash
pip install quantumapi[django]
```

```python
# settings.py
QUANTUMAPI = {
    'API_KEY': 'qapi_xxx',
    'BASE_URL': 'https://api.quantumapi.eu',
}

# Use in views
from quantumapi.integrations.django import get_client

async def my_view(request):
    client = get_client()
    secret = await client.secrets.get('secret-id')
    return JsonResponse({'value': secret.value})
```

### Flask

```bash
pip install quantumapi[flask]
```

```python
from flask import Flask
from quantumapi.integrations.flask import QuantumAPIFlask

app = Flask(__name__)
app.config['QUANTUMAPI_API_KEY'] = 'qapi_xxx'
qapi = QuantumAPIFlask(app)

@app.route('/secret/<secret_id>')
async def get_secret(secret_id):
    secret = await qapi.secrets.get(secret_id)
    return {'value': secret.value}
```

## Testing

Run tests:

```bash
pytest
```

Run tests with coverage:

```bash
pytest --cov=quantumapi --cov-report=html
```

## Development

```bash
# Clone repository
git clone https://github.com/quantumapi/quantumapi-python
cd quantumapi-python

# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Format code
black quantumapi tests
isort quantumapi tests

# Type checking
mypy quantumapi

# Linting
flake8 quantumapi tests
```

## Requirements

- Python 3.9 or higher
- httpx >= 0.27.0
- pydantic >= 2.0.0
- python-dotenv >= 1.0.0
- cryptography >= 42.0.0

## License

MIT License - see [LICENSE](LICENSE) file for details.

## Support

- 📖 [Documentation](https://docs.quantumapi.eu/sdk/python)
- 💬 [Discord Community](https://discord.gg/quantumapi)
- 🐛 [Issue Tracker](https://github.com/quantumapi/quantumapi-python/issues)
- 📧 [Email Support](mailto:support@quantumapi.eu)

## Contributing

Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for a history of changes to this SDK.

## Security

For security concerns, please email victorxata@icloud.com. Do not create public issues for security vulnerabilities.

## Links

- [QuantumAPI Website](https://quantumapi.eu)
- [API Documentation](https://docs.quantumapi.eu)
- [Python SDK Documentation](https://docs.quantumapi.eu/sdk/python)
- [GitHub Repository](https://github.com/quantumapi/quantumapi-python)
- [PyPI Package](https://pypi.org/project/quantumapi/)
