hawkapi-storage
Pluggable file storage for HawkAPI. One Storage protocol, four backends: local filesystem, AWS S3 (and S3-compatible — MinIO, Wasabi, R2), Google Cloud Storage, Azure Blob Storage. Pre-signed URLs and streaming on all of them.
Install
pip install hawkapi-storage # local filesystem only
pip install 'hawkapi-storage[s3]' # + AWS S3
pip install 'hawkapi-storage[gcs]' # + Google Cloud Storage
pip install 'hawkapi-storage[azure]' # + Azure Blob Storage
Quickstart
from hawkapi import Depends, HawkAPI
from hawkapi_storage import LocalConfig, LocalStorage, Storage, get_storage, init_storage
app = HawkAPI()
init_storage(app, storage=LocalStorage(LocalConfig(root="/var/data", base_url="https://cdn.example")))
@app.put("/files/{key}")
async def upload(key: str, body: bytes, s: Storage = Depends(get_storage)):
obj = await s.put(key, body, content_type="application/octet-stream")
return {"key": obj.key, "size": obj.size}
@app.get("/files/{key}/url")
async def signed(key: str, s: Storage = Depends(get_storage)):
return {"url": await s.signed_url(key, expires_in=300)}
Swap LocalStorage for any other backend — every primitive is identical.
Backends
from hawkapi_storage import (
LocalStorage, LocalConfig,
S3Storage, S3Config, # extras: [s3]
GCSStorage, GCSConfig, # extras: [gcs]
AzureStorage, AzureConfig, # extras: [azure]
)
local = LocalStorage(LocalConfig(root="/var/data"))
s3 = S3Storage(S3Config(bucket="my-bucket", region="eu-west-1"))
minio = S3Storage(S3Config(bucket="mb", endpoint_url="https://minio.example", use_path_style=True))
gcs = GCSStorage(GCSConfig(bucket="my-bucket", project="my-project"))
azure = AzureStorage(AzureConfig(container="files", connection_string="..."))
The Storage protocol
class Storage(Protocol):
name: str
async def put(self, key, data, *, content_type=None, metadata=None) -> StoredObject: ...
async def get(self, key) -> bytes: ...
async def stream(self, key, *, chunk_size=65536) -> AsyncIterator[bytes]: ...
async def exists(self, key) -> bool: ...
async def delete(self, key) -> None: ...
async def head(self, key) -> StoredObject: ...
async def list(self, prefix="", *, limit=1000) -> AsyncIterator[StoredObject]: ...
async def signed_url(self, key, *, expires_in=3600, method="GET", content_type=None) -> str: ...
put() accepts bytes, a file-like object, or an AsyncIterator[bytes] (for streaming uploads).
Streaming downloads
@app.get("/download/{key}")
async def download(key: str, s: Storage = Depends(get_storage)):
return StreamingResponse(s.stream(key, chunk_size=65536),
media_type=(await s.head(key)).content_type)
Pre-signed URLs
Every backend supports signed_url(key, expires_in=..., method="GET" | "PUT"). For PUT/upload pre-signs, pass content_type= so the client must send the matching Content-Type header.
LocalStorage produces HMAC-signed URLs that you verify on download with local.verify_signed_url(key, expires, sig, method="GET") — useful when serving downloads through your own handler.
Local filesystem details
- Path traversal (
..) is rejected atput/gettime. LocalConfig(base_url=...)sets the prefix used bysigned_url()— pair it with a Nginx alias or a HawkAPI download handler.LocalConfig(signing_secret=...)lets you pin the HMAC secret (otherwise generated once at startup).
Errors
StorageError— base class.NotFoundError(key)—get/head/streamon a missing key.
Development
git clone https://github.com/Hawk-API/hawkapi-storage.git
cd hawkapi-storage
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/
License
MIT.
Metadata
Release files for hawkapi-storage 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hawkapi_storage-0.2.1.tar.gz | 50.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hawkapi_storage-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.0 kB
Release files / hawkapi_storage-0.2.1.tar.gz
| Download URL | hawkapi_storage-0.2.1.tar.gz |
|---|---|
| Size | 50.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
907c3f6199b8ec09bf29f4d3078d9238f7593de545692141bc77ea3a3ba1b872
|
|
BLAKE2b-256 checksum How to use checksums |
411b577c781c21aef5a65136ecd9739328d3b30bc91ecb668488332b0999f966
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency logRelease files / hawkapi_storage-0.2.1-py3-none-any.whl
| Download URL | hawkapi_storage-0.2.1-py3-none-any.whl |
|---|---|
| Size | 17.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5c34daad827e53514a7eb5b87e7dde2866e5be9315e4881dfb4cd3aad233b170
|
|
BLAKE2b-256 checksum How to use checksums |
0ba3ab89f06b62bc870cb9a875eabd000af399d6aa42f5952b1911d929483286
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 10, 2026.
Transparency log