Metadata-Version: 2.4
Name: hawkapi-storage
Version: 0.2.1
Summary: File storage for HawkAPI — local, S3, GCS, Azure backends, pre-signed URLs, streaming uploads
Project-URL: Homepage, https://pypi.org/project/hawkapi-storage/
Project-URL: Repository, https://github.com/Hawk-API/hawkapi-storage
Project-URL: Issues, https://github.com/Hawk-API/hawkapi-storage/issues
Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
License: MIT License
        
        Copyright (c) 2026 HawkAPI Contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: azure,files,gcs,hawkapi,s3,storage,upload
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Filesystems
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: hawkapi>=0.1.7
Provides-Extra: azure
Requires-Dist: azure-storage-blob>=12.19; extra == 'azure'
Provides-Extra: dev
Requires-Dist: boto3>=1.34; extra == 'dev'
Requires-Dist: pyright>=1.1; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: gcs
Requires-Dist: google-cloud-storage>=2.14; extra == 'gcs'
Provides-Extra: s3
Requires-Dist: boto3>=1.34; extra == 's3'
Description-Content-Type: text/markdown

# hawkapi-storage

Pluggable file storage for [HawkAPI](https://github.com/Hawk-API/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

```bash
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

```python
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

```python
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

```python
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

```python
@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 at `put`/`get` time.
- `LocalConfig(base_url=...)` sets the prefix used by `signed_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` / `stream` on a missing key.

## Development

```bash
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.
