Metadata-Version: 2.4
Name: quantahost-storage
Version: 0.1.0
Summary: Official Python SDK for QuantaHost object storage (QuantaStore). Uploads go direct to storage via presigned URLs; zero dependencies.
Author: Quantabyte (Pvt) Ltd
License: MIT
Project-URL: Homepage, https://quantahost.net
Project-URL: Repository, https://github.com/quantahost/quantahost-storage-python
Keywords: quantahost,object-storage,s3,cdn,file-upload,storage
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Development Status :: 4 - Beta
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# quantahost-storage (Python)

Official Python SDK for QuantaHost object storage (QuantaStore). Uploads go **direct to storage** via short-lived presigned URLs — file bytes never transit the QuantaHost API, and your secret stays on your server. **Zero dependencies** (standard library only). Python 3.8+.

```bash
pip install quantahost-storage
```

```python
from quantahost_storage import QuantaStorage
from datetime import timedelta

storage = QuantaStorage(api_key="qhk_...", api_secret="qhs_...")
# or QuantaStorage()  # reads QUANTAHOST_STORAGE_KEY / _SECRET from the env

# Upload a file (keeps its name) or raw bytes
result = storage.upload("cat.png")
result = storage.upload(photo_bytes, "avatars/user-42.png", "image/png")

# Persist result.object_path — the server randomizes the leaf name, so it's the
# only handle that fetches/deletes the file later.
print(result.object_path, result.size)

# Private bucket: hand the browser a short-lived signed link (do your auth first)
url = storage.signed_url(result.object_path, expires_in=300).url   # 5 min

# Public bucket: permanent URL
url = storage.public_url(result.object_path)

# List / delete
for obj in storage.list(prefix="avatars/").objects:
    print(obj.object_path, obj.size)
storage.delete(result.object_path)
```

### Django / Flask example

```python
# Flask
@app.post("/upload")
def upload():
    f = request.files["file"]
    r = storage.upload(f.read(), f"users/{g.user.id}/{f.filename}", f.mimetype)
    return {"path": r.object_path}

@app.get("/file/<path:object_path>")
def file(object_path):
    # authorize the user for this file FIRST, then sign
    return redirect(storage.signed_url(object_path, expires_in=300).url)
```

- Files ≤100 MB use a single PUT; larger (up to 1 GB) switch to multipart automatically.
- One `QuantaStorage` instance is thread-safe — create it once and reuse it.
- Errors raise `QuantaStorageError` (`.status`, `.code`, `.is_unauthorized`, `.is_not_found`, `.is_too_large`).

### Config
Pass credentials directly, or via env: `QUANTAHOST_STORAGE_KEY`, `QUANTAHOST_STORAGE_SECRET`, optional `QUANTAHOST_STORAGE_CLOUD`, `QUANTAHOST_API_URL`, or a single `QUANTASTORE_URL=quantastore://<key>:<secret>@<cloud>`. For an on-prem QuantaHost behind a private CA, pass `ca_cert="/path/ca.pem"`.

### Security
HTTPS only (plain http allowed for localhost dev), never follows redirects, validates object paths (no `..`/control chars), blocks header injection, bounds upload size, and never prints the secret (`repr` redacts it).

## Test / build
```bash
python3 tests/test_guards.py            # offline guard tests
python3 -m build                        # build sdist + wheel (needs: pip install build)
```

See the publishing guide for releasing to PyPI. Report security issues to security@quantahost.net.

## Web-framework uploads (FastAPI / Flask / Django)

```python
from quantahost_storage import QuantaStorage
from quantahost_storage.frameworks import upload_file

storage = QuantaStorage(api_key=..., api_secret=...)

# FastAPI
@app.post("/upload")
async def handle(file: UploadFile):
    r = upload_file(storage, file)            # sync; reads file.file
    return {"path": r.object_path, "url": r.public_url}

# Flask / Werkzeug
r = upload_file(storage, request.files["photo"])

# Django
r = upload_file(storage, request.FILES["photo"])
```

`upload_file` duck-types the upload object — no framework is imported, so the SDK stays
zero-dependency. Pass `object_path=` / `content_type=` to override; the filename is used
otherwise (the server randomizes the leaf name so uploads never collide).
