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.
@@ -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
+ ]