UploadKit
Framework-independent secure upload pipeline for Python.
What problem does this solve?
UploadKit Core owns validation orchestration, upload policies, storage provider interfaces, after-upload hooks, and a stable exception hierarchy — without depending on Django, FastAPI, Flask, or any storage SDK.
When to use it
Use Core when you need a reusable upload pipeline that you can plug into any framework and any object store.
When not to use it
- Do not put framework adapters here (see
uploadkit-django,uploadkit-fastapi, etc.). - Do not put MIME/filename/checksum validators here (see
uploadkit-security). - Do not put image/PDF/office-specific policies here (see feature packages).
Installation
Requires Python 3.10+.
pip install uploadkit uploadkit-security
uv add uploadkit uploadkit-security
poetry add uploadkit uploadkit-security
For the storage samples below (not package dependencies):
pip install boto3 # sync AWS S3 / MinIO
pip install aioboto3 # async AWS S3 / MinIO
Quick Start (sync)
from uploadkit import Uploader, UploadPolicy
from uploadkit_security import default_validators
policy = UploadPolicy(
max_size=5 * 1024 * 1024,
allowed_extensions=frozenset({"png"}),
allowed_mime_types=frozenset({"image/png"}),
validators=default_validators(),
)
result = Uploader(policy, storage).upload(
file, # UploadableFile
bucket="uploads",
object_name="2026/file.png",
)
# result.bucket, result.object_name, result.sha256, result.etag, …
Quick Start (async streaming)
from uploadkit import AsyncUploader, UploadPolicy
from uploadkit_security import default_async_validators
policy = UploadPolicy(
max_size=5 * 1024 * 1024,
allowed_extensions=frozenset({"png"}),
allowed_mime_types=frozenset({"image/png"}),
async_validators=default_async_validators(),
)
result = await AsyncUploader(policy, async_storage).upload(
source, # AsyncByteSource
bucket="uploads",
object_name="2026/file.png",
)
Storage examples (AWS S3 and MinIO)
UploadKit does not ship boto3/aioboto3. Implement the protocols once; the same classes work for AWS S3 (omit endpoint_url) and MinIO (set endpoint_url).
Sync — Boto3S3Storage (StorageProvider)
import boto3
from botocore.client import Config
class Boto3S3Storage:
"""S3-compatible sync storage for AWS S3 or MinIO."""
def __init__(
self,
*,
access_key: str,
secret_key: str,
region: str = "us-east-1",
endpoint_url: str | None = None,
) -> None:
kwargs: dict = {
"service_name": "s3",
"aws_access_key_id": access_key,
"aws_secret_access_key": secret_key,
"region_name": region,
"config": Config(signature_version="s3v4"),
}
if endpoint_url:
kwargs["endpoint_url"] = endpoint_url
self.client = boto3.client(**kwargs)
def put(self, *, bucket, object_name, body, content_type):
resp = self.client.put_object(
Bucket=bucket,
Key=object_name,
Body=body,
ContentType=content_type,
)
return resp.get("ETag")
AWS S3:
storage = Boto3S3Storage(
access_key="AKIA...",
secret_key="...",
region="eu-west-1",
)
MinIO (local default):
storage = Boto3S3Storage(
endpoint_url="http://127.0.0.1:9000",
access_key="minioadmin",
secret_key="minioadmin",
region="us-east-1",
)
Async — AsyncS3Storage (AsyncStorageProvider)
Multipart streaming writer (5 MiB part size — S3/MinIO rule except the last part):
from __future__ import annotations
import aioboto3
from botocore.client import Config
_PART_SIZE = 5 * 1024 * 1024 # 5 MiB
class AsyncS3Writer:
def __init__(self, client, *, bucket: str, object_name: str, content_type: str) -> None:
self._client = client
self._bucket = bucket
self._key = object_name
self._content_type = content_type
self._upload_id: str | None = None
self._parts: list[dict] = []
self._buffer = bytearray()
self._part_number = 1
async def _ensure_upload(self) -> None:
if self._upload_id is not None:
return
resp = await self._client.create_multipart_upload(
Bucket=self._bucket,
Key=self._key,
ContentType=self._content_type,
)
self._upload_id = resp["UploadId"]
async def _flush_part(self, data: bytes) -> None:
await self._ensure_upload()
assert self._upload_id is not None
resp = await self._client.upload_part(
Bucket=self._bucket,
Key=self._key,
PartNumber=self._part_number,
UploadId=self._upload_id,
Body=data,
)
self._parts.append({"ETag": resp["ETag"], "PartNumber": self._part_number})
self._part_number += 1
async def write(self, chunk: bytes) -> None:
self._buffer.extend(chunk)
while len(self._buffer) >= _PART_SIZE:
part = bytes(self._buffer[:_PART_SIZE])
del self._buffer[:_PART_SIZE]
await self._flush_part(part)
async def abort(self) -> None:
if self._upload_id is None:
return
await self._client.abort_multipart_upload(
Bucket=self._bucket,
Key=self._key,
UploadId=self._upload_id,
)
self._upload_id = None
async def complete(self) -> str | None:
if self._buffer:
await self._flush_part(bytes(self._buffer))
self._buffer.clear()
if self._upload_id is None:
# empty object
resp = await self._client.put_object(
Bucket=self._bucket,
Key=self._key,
Body=b"",
ContentType=self._content_type,
)
return resp.get("ETag")
resp = await self._client.complete_multipart_upload(
Bucket=self._bucket,
Key=self._key,
UploadId=self._upload_id,
MultipartUpload={"Parts": self._parts},
)
self._upload_id = None
return resp.get("ETag")
class AsyncS3Storage:
"""S3-compatible async storage for AWS S3 or MinIO."""
def __init__(
self,
*,
access_key: str,
secret_key: str,
region: str = "us-east-1",
endpoint_url: str | None = None,
) -> None:
self._session = aioboto3.Session()
self._client_kwargs: dict = {
"service_name": "s3",
"aws_access_key_id": access_key,
"aws_secret_access_key": secret_key,
"region_name": region,
"config": Config(signature_version="s3v4"),
}
if endpoint_url:
self._client_kwargs["endpoint_url"] = endpoint_url
self._cm = None
self._client = None
async def _get_client(self):
if self._client is None:
self._cm = self._session.client(**self._client_kwargs)
self._client = await self._cm.__aenter__()
return self._client
async def open_write(self, *, bucket: str, object_name: str, content_type: str):
client = await self._get_client()
return AsyncS3Writer(
client,
bucket=bucket,
object_name=object_name,
content_type=content_type,
)
AWS S3:
async_storage = AsyncS3Storage(
access_key="AKIA...",
secret_key="...",
region="eu-west-1",
)
MinIO:
async_storage = AsyncS3Storage(
endpoint_url="http://127.0.0.1:9000",
access_key="minioadmin",
secret_key="minioadmin",
)
Validators (uploadkit-security)
from uploadkit_security import default_validators, default_async_validators
validators = default_validators() # sync Uploader
async_validators = default_async_validators() # AsyncUploader
After-upload hooks
Pass an optional after_upload keyword to Uploader.upload / AsyncUploader.upload. The hook runs once after a successful store, before the method returns UploadResult. It does not run on validation or storage failure. Hook exceptions propagate (they are not swallowed).
Accepted shapes:
- Sync callback —
(result: UploadResult) -> None - Async callback — sync or
async defonAsyncUploader(awaited) - Celery-like — any object with
.delay(**kwargs); Core callsdelay(**result.as_task_kwargs())(no Celery import)
UploadResult.as_task_kwargs() is JSON-serializable: bucket, object_name, original_name, mime_type, extension, size, sha256, etag.
Sync callback
def notify(result):
# e.g. write an audit row, publish an event
...
result = Uploader(policy, storage).upload(
file,
bucket="uploads",
object_name="2026/file.png",
after_upload=notify,
)
Celery-like task
# myapp/tasks.py — Celery (or any .delay duck-type)
@app.task
def process_upload(bucket, object_name, original_name, mime_type,
extension, size, sha256, etag):
...
result = Uploader(policy, storage).upload(
file,
bucket="uploads",
object_name="2026/file.png",
after_upload=process_upload, # Core calls process_upload.delay(**as_task_kwargs())
)
Async callback
async def notify(result):
...
result = await AsyncUploader(policy, async_storage).upload(
source,
bucket="uploads",
object_name="2026/file.png",
after_upload=notify, # or a sync callback, or Celery-like .delay
)
FastAPI apps can also use background_after_upload from uploadkit-fastapi to schedule work via Starlette BackgroundTasks.
Architecture
Sync: Uploader.upload → validators → StorageProvider.put → UploadResult → after_upload
Async: AsyncUploader.upload → async validators feed → AsyncStorageProvider writer → UploadResult → after_upload
Public API
| Symbol | Kind |
|---|---|
Uploader / AsyncUploader |
Public |
UploadPolicy |
Public (validators / async_validators) |
UploadResult / UploadContext |
Public (UploadResult.as_task_kwargs) |
UploadableFile / AsyncByteSource |
Public (protocols) |
StorageProvider / AsyncStorageProvider / AsyncObjectWriter |
Public (protocols) |
Validator / AsyncStreamingValidator |
Public |
UploaderError and subclasses |
Public |
AfterUploadHook / AsyncAfterUploadHook |
Public |
Framework integrations
- Django:
uploadkit-django - FastAPI:
uploadkit-fastapi - aiohttp: use Core
AsyncByteSourcedirectly (see docs site)
Changelog
See CHANGELOG.md.
Contributing
See CONTRIBUTING.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file uploadkit-0.2.1.tar.gz.
File metadata
- Download URL: uploadkit-0.2.1.tar.gz
- Upload date:
- Size: 15.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be33813635f7952c350a356e769f9cdcf32c78ad0a6166ec63940b9a4a7be5ef
|
|
| MD5 |
705d8e4eb2fa8ba23ae8200ef47a05e5
|
|
| BLAKE2b-256 |
e5e61d165fdfd191316f8aaa7ab9e4aa3da65e3814de2d88f525059a7b32a0dd
|
File details
Details for the file uploadkit-0.2.1-py3-none-any.whl.
File metadata
- Download URL: uploadkit-0.2.1-py3-none-any.whl
- Upload date:
- Size: 18.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f2e5a574968995c5b913b53311a35402185901e9506717d2410e9bd4833497ca
|
|
| MD5 |
d2d3481f21c980d945f4f8a01f849627
|
|
| BLAKE2b-256 |
db4864d9ae0d1a993f204a7b541779f9817e4c5f0ca6780ea8cfdeab06feea90
|