hawkapi-storage 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hawkapi_storage-0.1.0/.github/workflows/ci.yml +52 -0
- hawkapi_storage-0.1.0/.github/workflows/release.yml +25 -0
- hawkapi_storage-0.1.0/.gitignore +35 -0
- hawkapi_storage-0.1.0/CHANGELOG.md +15 -0
- hawkapi_storage-0.1.0/LICENSE +21 -0
- hawkapi_storage-0.1.0/PKG-INFO +167 -0
- hawkapi_storage-0.1.0/README.md +111 -0
- hawkapi_storage-0.1.0/pyproject.toml +73 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/__init__.py +43 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/_azure.py +195 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/_base.py +88 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/_gcs.py +154 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/_local.py +185 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/_plugin.py +49 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/_s3.py +180 -0
- hawkapi_storage-0.1.0/src/hawkapi_storage/py.typed +0 -0
- hawkapi_storage-0.1.0/tests/__init__.py +0 -0
- hawkapi_storage-0.1.0/tests/test_local.py +113 -0
- hawkapi_storage-0.1.0/tests/test_plugin.py +84 -0
- hawkapi_storage-0.1.0/tests/test_s3.py +112 -0
- hawkapi_storage-0.1.0/uv.lock +724 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
lint:
|
|
10
|
+
name: Lint
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v4
|
|
15
|
+
with:
|
|
16
|
+
enable-cache: true
|
|
17
|
+
- name: Install dependencies
|
|
18
|
+
run: uv sync --extra dev
|
|
19
|
+
- name: ruff check
|
|
20
|
+
run: uv run ruff check .
|
|
21
|
+
- name: ruff format check
|
|
22
|
+
run: uv run ruff format --check .
|
|
23
|
+
|
|
24
|
+
typecheck:
|
|
25
|
+
name: Typecheck
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: astral-sh/setup-uv@v4
|
|
30
|
+
with:
|
|
31
|
+
enable-cache: true
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync --extra dev
|
|
34
|
+
- name: pyright
|
|
35
|
+
run: uv run pyright src/
|
|
36
|
+
|
|
37
|
+
test:
|
|
38
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
strategy:
|
|
41
|
+
matrix:
|
|
42
|
+
python-version: ["3.12", "3.13"]
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: astral-sh/setup-uv@v4
|
|
46
|
+
with:
|
|
47
|
+
enable-cache: true
|
|
48
|
+
python-version: ${{ matrix.python-version }}
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: uv sync --extra dev
|
|
51
|
+
- name: Run tests
|
|
52
|
+
run: uv run pytest tests/ -q
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-and-publish:
|
|
9
|
+
name: Build and publish to PyPI
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: release
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for trusted publishing
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v4
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- name: Build package
|
|
21
|
+
run: uv build
|
|
22
|
+
- name: Publish to PyPI
|
|
23
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
24
|
+
with:
|
|
25
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.egg
|
|
9
|
+
.eggs/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
.env
|
|
14
|
+
*.log
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.pyright/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
htmlcov/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
coverage.xml
|
|
23
|
+
*.cover
|
|
24
|
+
.hypothesis/
|
|
25
|
+
.tox/
|
|
26
|
+
.nox/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
*~
|
|
30
|
+
.DS_Store
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
.history/
|
|
34
|
+
site/
|
|
35
|
+
.remember/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-05-16
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `Storage` protocol — single async interface across every backend.
|
|
8
|
+
- Backends: `LocalStorage` (always), `S3Storage` (`[s3]`), `GCSStorage` (`[gcs]`), `AzureStorage` (`[azure]`). S3 backend speaks to MinIO / Wasabi / R2 via `endpoint_url=` + `use_path_style=`.
|
|
9
|
+
- `put` accepts `bytes`, file-like, or `AsyncIterator[bytes]` for streaming uploads.
|
|
10
|
+
- `stream(key, chunk_size=...)` for streaming downloads on every backend.
|
|
11
|
+
- Pre-signed URLs (GET/PUT/DELETE where supported) — `signed_url(key, expires_in=...)`.
|
|
12
|
+
- HMAC-signed local URLs with `verify_signed_url()` for self-hosted download endpoints.
|
|
13
|
+
- Path traversal rejected at `put`/`get` time.
|
|
14
|
+
- `init_storage(app, storage=...)` + `Depends(get_storage)`.
|
|
15
|
+
- `NotFoundError` / `StorageError` for clean handler-level error handling.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HawkAPI Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hawkapi-storage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: File storage for HawkAPI — local, S3, GCS, Azure backends, pre-signed URLs, streaming uploads
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/hawkapi-storage/
|
|
6
|
+
Project-URL: Repository, https://github.com/ashimov/hawkapi-storage
|
|
7
|
+
Project-URL: Issues, https://github.com/ashimov/hawkapi-storage/issues
|
|
8
|
+
Author-email: HawkAPI Contributors <hawkapi@users.noreply.github.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 HawkAPI Contributors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: azure,files,gcs,hawkapi,s3,storage,upload
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Framework :: AsyncIO
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Topic :: System :: Filesystems
|
|
40
|
+
Classifier: Typing :: Typed
|
|
41
|
+
Requires-Python: >=3.12
|
|
42
|
+
Requires-Dist: hawkapi>=0.1.7
|
|
43
|
+
Provides-Extra: azure
|
|
44
|
+
Requires-Dist: azure-storage-blob>=12.19; extra == 'azure'
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: boto3>=1.34; extra == 'dev'
|
|
47
|
+
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
48
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
51
|
+
Provides-Extra: gcs
|
|
52
|
+
Requires-Dist: google-cloud-storage>=2.14; extra == 'gcs'
|
|
53
|
+
Provides-Extra: s3
|
|
54
|
+
Requires-Dist: boto3>=1.34; extra == 's3'
|
|
55
|
+
Description-Content-Type: text/markdown
|
|
56
|
+
|
|
57
|
+
# hawkapi-storage
|
|
58
|
+
|
|
59
|
+
Pluggable file storage for [HawkAPI](https://github.com/ashimov/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.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install hawkapi-storage # local filesystem only
|
|
65
|
+
pip install 'hawkapi-storage[s3]' # + AWS S3
|
|
66
|
+
pip install 'hawkapi-storage[gcs]' # + Google Cloud Storage
|
|
67
|
+
pip install 'hawkapi-storage[azure]' # + Azure Blob Storage
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quickstart
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from hawkapi import Depends, HawkAPI
|
|
74
|
+
from hawkapi_storage import LocalConfig, LocalStorage, Storage, get_storage, init_storage
|
|
75
|
+
|
|
76
|
+
app = HawkAPI()
|
|
77
|
+
init_storage(app, storage=LocalStorage(LocalConfig(root="/var/data", base_url="https://cdn.example")))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.put("/files/{key}")
|
|
81
|
+
async def upload(key: str, body: bytes, s: Storage = Depends(get_storage)):
|
|
82
|
+
obj = await s.put(key, body, content_type="application/octet-stream")
|
|
83
|
+
return {"key": obj.key, "size": obj.size}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.get("/files/{key}/url")
|
|
87
|
+
async def signed(key: str, s: Storage = Depends(get_storage)):
|
|
88
|
+
return {"url": await s.signed_url(key, expires_in=300)}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Swap `LocalStorage` for any other backend — every primitive is identical.
|
|
92
|
+
|
|
93
|
+
## Backends
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from hawkapi_storage import (
|
|
97
|
+
LocalStorage, LocalConfig,
|
|
98
|
+
S3Storage, S3Config, # extras: [s3]
|
|
99
|
+
GCSStorage, GCSConfig, # extras: [gcs]
|
|
100
|
+
AzureStorage, AzureConfig, # extras: [azure]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
local = LocalStorage(LocalConfig(root="/var/data"))
|
|
104
|
+
s3 = S3Storage(S3Config(bucket="my-bucket", region="eu-west-1"))
|
|
105
|
+
minio = S3Storage(S3Config(bucket="mb", endpoint_url="https://minio.example", use_path_style=True))
|
|
106
|
+
gcs = GCSStorage(GCSConfig(bucket="my-bucket", project="my-project"))
|
|
107
|
+
azure = AzureStorage(AzureConfig(container="files", connection_string="..."))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## The `Storage` protocol
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class Storage(Protocol):
|
|
114
|
+
name: str
|
|
115
|
+
|
|
116
|
+
async def put(self, key, data, *, content_type=None, metadata=None) -> StoredObject: ...
|
|
117
|
+
async def get(self, key) -> bytes: ...
|
|
118
|
+
async def stream(self, key, *, chunk_size=65536) -> AsyncIterator[bytes]: ...
|
|
119
|
+
async def exists(self, key) -> bool: ...
|
|
120
|
+
async def delete(self, key) -> None: ...
|
|
121
|
+
async def head(self, key) -> StoredObject: ...
|
|
122
|
+
async def list(self, prefix="", *, limit=1000) -> AsyncIterator[StoredObject]: ...
|
|
123
|
+
async def signed_url(self, key, *, expires_in=3600, method="GET", content_type=None) -> str: ...
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`put()` accepts `bytes`, a file-like object, or an `AsyncIterator[bytes]` (for streaming uploads).
|
|
127
|
+
|
|
128
|
+
## Streaming downloads
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
@app.get("/download/{key}")
|
|
132
|
+
async def download(key: str, s: Storage = Depends(get_storage)):
|
|
133
|
+
return StreamingResponse(s.stream(key, chunk_size=65536),
|
|
134
|
+
media_type=(await s.head(key)).content_type)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Pre-signed URLs
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
`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.
|
|
142
|
+
|
|
143
|
+
## Local filesystem details
|
|
144
|
+
|
|
145
|
+
- Path traversal (`..`) is rejected at `put`/`get` time.
|
|
146
|
+
- `LocalConfig(base_url=...)` sets the prefix used by `signed_url()` — pair it with a Nginx alias or a HawkAPI download handler.
|
|
147
|
+
- `LocalConfig(signing_secret=...)` lets you pin the HMAC secret (otherwise generated once at startup).
|
|
148
|
+
|
|
149
|
+
## Errors
|
|
150
|
+
|
|
151
|
+
- `StorageError` — base class.
|
|
152
|
+
- `NotFoundError(key)` — `get` / `head` / `stream` on a missing key.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
git clone https://github.com/ashimov/hawkapi-storage.git
|
|
158
|
+
cd hawkapi-storage
|
|
159
|
+
uv sync --extra dev
|
|
160
|
+
uv run pytest -q
|
|
161
|
+
uv run ruff check . && uv run ruff format --check .
|
|
162
|
+
uv run pyright src/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# hawkapi-storage
|
|
2
|
+
|
|
3
|
+
Pluggable file storage for [HawkAPI](https://github.com/ashimov/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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install hawkapi-storage # local filesystem only
|
|
9
|
+
pip install 'hawkapi-storage[s3]' # + AWS S3
|
|
10
|
+
pip install 'hawkapi-storage[gcs]' # + Google Cloud Storage
|
|
11
|
+
pip install 'hawkapi-storage[azure]' # + Azure Blob Storage
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from hawkapi import Depends, HawkAPI
|
|
18
|
+
from hawkapi_storage import LocalConfig, LocalStorage, Storage, get_storage, init_storage
|
|
19
|
+
|
|
20
|
+
app = HawkAPI()
|
|
21
|
+
init_storage(app, storage=LocalStorage(LocalConfig(root="/var/data", base_url="https://cdn.example")))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.put("/files/{key}")
|
|
25
|
+
async def upload(key: str, body: bytes, s: Storage = Depends(get_storage)):
|
|
26
|
+
obj = await s.put(key, body, content_type="application/octet-stream")
|
|
27
|
+
return {"key": obj.key, "size": obj.size}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.get("/files/{key}/url")
|
|
31
|
+
async def signed(key: str, s: Storage = Depends(get_storage)):
|
|
32
|
+
return {"url": await s.signed_url(key, expires_in=300)}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Swap `LocalStorage` for any other backend — every primitive is identical.
|
|
36
|
+
|
|
37
|
+
## Backends
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from hawkapi_storage import (
|
|
41
|
+
LocalStorage, LocalConfig,
|
|
42
|
+
S3Storage, S3Config, # extras: [s3]
|
|
43
|
+
GCSStorage, GCSConfig, # extras: [gcs]
|
|
44
|
+
AzureStorage, AzureConfig, # extras: [azure]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
local = LocalStorage(LocalConfig(root="/var/data"))
|
|
48
|
+
s3 = S3Storage(S3Config(bucket="my-bucket", region="eu-west-1"))
|
|
49
|
+
minio = S3Storage(S3Config(bucket="mb", endpoint_url="https://minio.example", use_path_style=True))
|
|
50
|
+
gcs = GCSStorage(GCSConfig(bucket="my-bucket", project="my-project"))
|
|
51
|
+
azure = AzureStorage(AzureConfig(container="files", connection_string="..."))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## The `Storage` protocol
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
class Storage(Protocol):
|
|
58
|
+
name: str
|
|
59
|
+
|
|
60
|
+
async def put(self, key, data, *, content_type=None, metadata=None) -> StoredObject: ...
|
|
61
|
+
async def get(self, key) -> bytes: ...
|
|
62
|
+
async def stream(self, key, *, chunk_size=65536) -> AsyncIterator[bytes]: ...
|
|
63
|
+
async def exists(self, key) -> bool: ...
|
|
64
|
+
async def delete(self, key) -> None: ...
|
|
65
|
+
async def head(self, key) -> StoredObject: ...
|
|
66
|
+
async def list(self, prefix="", *, limit=1000) -> AsyncIterator[StoredObject]: ...
|
|
67
|
+
async def signed_url(self, key, *, expires_in=3600, method="GET", content_type=None) -> str: ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`put()` accepts `bytes`, a file-like object, or an `AsyncIterator[bytes]` (for streaming uploads).
|
|
71
|
+
|
|
72
|
+
## Streaming downloads
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
@app.get("/download/{key}")
|
|
76
|
+
async def download(key: str, s: Storage = Depends(get_storage)):
|
|
77
|
+
return StreamingResponse(s.stream(key, chunk_size=65536),
|
|
78
|
+
media_type=(await s.head(key)).content_type)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Pre-signed URLs
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
`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.
|
|
86
|
+
|
|
87
|
+
## Local filesystem details
|
|
88
|
+
|
|
89
|
+
- Path traversal (`..`) is rejected at `put`/`get` time.
|
|
90
|
+
- `LocalConfig(base_url=...)` sets the prefix used by `signed_url()` — pair it with a Nginx alias or a HawkAPI download handler.
|
|
91
|
+
- `LocalConfig(signing_secret=...)` lets you pin the HMAC secret (otherwise generated once at startup).
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
- `StorageError` — base class.
|
|
96
|
+
- `NotFoundError(key)` — `get` / `head` / `stream` on a missing key.
|
|
97
|
+
|
|
98
|
+
## Development
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/ashimov/hawkapi-storage.git
|
|
102
|
+
cd hawkapi-storage
|
|
103
|
+
uv sync --extra dev
|
|
104
|
+
uv run pytest -q
|
|
105
|
+
uv run ruff check . && uv run ruff format --check .
|
|
106
|
+
uv run pyright src/
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hawkapi-storage"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "File storage for HawkAPI — local, S3, GCS, Azure backends, pre-signed URLs, streaming uploads"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "HawkAPI Contributors", email = "hawkapi@users.noreply.github.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["hawkapi", "storage", "s3", "gcs", "azure", "upload", "files"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Framework :: AsyncIO",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: System :: Filesystems",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["hawkapi>=0.1.7"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
s3 = ["boto3>=1.34"]
|
|
31
|
+
gcs = ["google-cloud-storage>=2.14"]
|
|
32
|
+
azure = ["azure-storage-blob>=12.19"]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-asyncio>=0.24",
|
|
36
|
+
"boto3>=1.34",
|
|
37
|
+
"ruff>=0.8",
|
|
38
|
+
"pyright>=1.1",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://pypi.org/project/hawkapi-storage/"
|
|
43
|
+
Repository = "https://github.com/ashimov/hawkapi-storage"
|
|
44
|
+
Issues = "https://github.com/ashimov/hawkapi-storage/issues"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/hawkapi_storage"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
asyncio_mode = "auto"
|
|
52
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py312"
|
|
56
|
+
line-length = 100
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint]
|
|
59
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "S"]
|
|
60
|
+
ignore = ["S101", "S110", "B008", "SIM105", "SIM108", "SIM113"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint.per-file-ignores]
|
|
63
|
+
"tests/**" = ["S"]
|
|
64
|
+
|
|
65
|
+
[tool.pyright]
|
|
66
|
+
pythonVersion = "3.12"
|
|
67
|
+
typeCheckingMode = "strict"
|
|
68
|
+
reportUnknownVariableType = false
|
|
69
|
+
reportUnknownMemberType = false
|
|
70
|
+
reportUnknownArgumentType = false
|
|
71
|
+
reportMissingTypeStubs = false
|
|
72
|
+
reportUntypedFunctionDecorator = false
|
|
73
|
+
reportGeneralTypeIssues = false
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""hawkapi-storage — pluggable file storage for HawkAPI.
|
|
2
|
+
|
|
3
|
+
Backends: local filesystem, AWS S3 (extras ``[s3]``), Google Cloud Storage
|
|
4
|
+
(extras ``[gcs]``), Azure Blob Storage (extras ``[azure]``). Single
|
|
5
|
+
:class:`Storage` protocol — swap backends freely.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ._azure import AzureConfig, AzureStorage
|
|
11
|
+
from ._base import (
|
|
12
|
+
NotFoundError,
|
|
13
|
+
Storage,
|
|
14
|
+
StorageError,
|
|
15
|
+
StoredObject,
|
|
16
|
+
guess_content_type,
|
|
17
|
+
)
|
|
18
|
+
from ._gcs import GCSConfig, GCSStorage
|
|
19
|
+
from ._local import LocalConfig, LocalStorage
|
|
20
|
+
from ._plugin import get_storage, init_storage, resolve_storage
|
|
21
|
+
from ._s3 import S3Config, S3Storage
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AzureConfig",
|
|
27
|
+
"AzureStorage",
|
|
28
|
+
"GCSConfig",
|
|
29
|
+
"GCSStorage",
|
|
30
|
+
"LocalConfig",
|
|
31
|
+
"LocalStorage",
|
|
32
|
+
"NotFoundError",
|
|
33
|
+
"S3Config",
|
|
34
|
+
"S3Storage",
|
|
35
|
+
"Storage",
|
|
36
|
+
"StorageError",
|
|
37
|
+
"StoredObject",
|
|
38
|
+
"__version__",
|
|
39
|
+
"get_storage",
|
|
40
|
+
"guess_content_type",
|
|
41
|
+
"init_storage",
|
|
42
|
+
"resolve_storage",
|
|
43
|
+
]
|