Fusion is a modern ASGI web framework for Python with built-in dependency injection, OpenAPI schema generation, and MCP support.
Project description
Fusion
A modern, async-first ASGI web framework for Python with type-safe dependency injection.
[!WARNING] This project is under active development and is not production-ready. APIs may change without notice between versions.
Overview
Fusion is a lightweight ASGI web framework built on two pillars:
- msgspec — high-performance JSON serialization and validation
- Type-hint-driven DI — declare dependencies as annotated fields; Fusion resolves them automatically at request time
It is designed for Python 3.12+ and is async-first throughout.
Installation
# From PyPI (once published)
pip install fusion
# From source
git clone https://github.com/okanakbulut/fusion.git
cd fusion
pip install -e .
Quick Start
# app.py
from fusion import Fusion, Get, Handler, Object, Request, Response
class Greeting(Object):
message: str
class HelloHandler(Handler):
async def handle(self, request: Request) -> Response[Greeting]:
return Response(Greeting(message="Hello, World!"))
app = Fusion(routes=[Get("/hello", handler=HelloHandler)])
Run it:
pip install uvicorn
uvicorn app:app
GET /hello → {"message": "Hello, World!"}
Core Concepts
Objects (Data Models)
Object is a msgspec.Struct-backed base class for all serializable data. Define fields with standard type annotations:
from fusion import Object
class User(Object):
id: int
name: str
email: str
Handlers
Subclass Handler and implement async def handle(self, request: SomeRequest) -> SomeResponse.
- The
requestparameter must be type-annotated withRequestor a subclass of it. - The return type determines how the response is serialized.
from fusion import Handler, Object, Request, Response
class Output(Object):
message: str
class MyHandler(Handler):
async def handle(self, request: Request) -> Response[Output]:
return Response(Output(message="ok"))
Routing
Use Route for explicit method lists, or the shorthand helpers Get, Post, Put, Delete, Patch:
from fusion import Fusion, Get, Post, Route
app = Fusion(routes=[
Get("/items", handler=ListItemsHandler),
Post("/items", handler=CreateItemHandler),
Route("/items/{id:int}", methods=["GET", "DELETE"], handler=ItemHandler),
])
Path parameter syntax:
| Pattern | Matches |
|---|---|
{name} |
any string segment |
{id:int} |
integer |
{id:uuid} |
UUID |
Request Parameters
Request-scoped parameters (QueryParam, PathParam, Header, Cookie, RequestBody) must be declared on a Request subclass — not directly on a Handler. The handler's handle method receives that subclass as its request argument.
Query Parameters
from fusion import Fusion, Get, Handler, Object, QueryParam, Request, Response
class SearchRequest(Request):
query: QueryParam[str]
page: QueryParam[int]
tags: QueryParam[list[str]] # ?tags:list=a,b,c
class SearchResult(Object):
query: str
page: int
class SearchHandler(Handler):
async def handle(self, request: SearchRequest) -> Response[SearchResult]:
return Response(SearchResult(query=request.query, page=request.page))
Headers
from fusion import Handler, Header, Object, Request, Response
class AuthRequest(Request):
authorization: Header[str]
user_id: Header[int] # header value is coerced to the declared type
class AuthHandler(Handler):
async def handle(self, request: AuthRequest) -> Response[Object]:
token = request.authorization # "Bearer ..."
...
Request Body
from fusion import Body, Handler, Object, Request, Response
class User(Object):
name: str
email: str
class CreateUserRequest(Request):
body: Body[User]
class CreateUserHandler(Handler):
async def handle(self, request: CreateUserRequest) -> Response[User]:
return Response(request.body)
Dependency Injection
Use @factory to register a factory function for any type (e.g. a database connection). Declare the type as a field on an Injectable; Fusion calls the factory automatically.
from fusion import Fusion, Get, Handler, Injectable, Object, Request, Response, factory
class Database:
def __init__(self, dsn: str) -> None:
self.dsn = dsn
@factory
async def database_factory() -> Database:
return Database("postgresql://localhost/mydb")
class StatusHandler(Handler):
db: Database # resolved from the factory above
async def handle(self, request: Request) -> Response[Object]:
# self.db is a Database instance
...
Lifecycle Management (async context managers)
Wrap a factory with @asynccontextmanager for per-request setup and teardown:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from fusion import factory
class Session:
pass
@factory
@asynccontextmanager
async def session_factory() -> AsyncIterator[Session]:
session = Session()
try:
yield session
finally:
await session.close() # runs after the handler returns
Middleware
Subclass BaseMiddleware, implement async def handle(self, request), and attach it to a route:
from fusion import Fusion, Get, Handler, Middleware, Object, Request, Response, Unauthorized
from fusion.middleware import BaseMiddleware
class AuthMiddleware(BaseMiddleware):
async def handle(self, request: Request) -> Unauthorized | Response:
token = request.headers.get("authorization", "")
if not token.startswith("Bearer "):
return Unauthorized(detail="Missing or invalid token")
return await self.app.handle(request)
class ProtectedHandler(Handler):
async def handle(self, request: Request) -> Response[Object]:
...
app = Fusion(routes=[
Get("/protected", handler=ProtectedHandler, middlewares=[Middleware(AuthMiddleware)])
])
Responses & Problem Details
Fusion follows RFC 9457 for error responses. All error types serialize to application/problem+json.
| Class | Status | Use case |
|---|---|---|
Response[T] |
200 | Success with body |
Created[T] |
201 | Resource created |
NoContent |
204 | Success, no body |
BadRequest |
400 | Invalid input |
Unauthorized |
401 | Authentication required |
Forbidden |
403 | Permission denied |
NotFound |
404 | Resource not found |
MethodNotAllowed |
405 | Wrong HTTP method |
InternalServerError |
500 | Unhandled error |
ValidationError |
400 | Field-level validation errors |
Returning errors from a handler
from fusion.responses import BadRequest, NotFound
class ItemHandler(Handler):
async def handle(self, request: Request) -> NotFound | Response[Item]:
item = db.get(item_id)
if item is None:
return NotFound(detail="Item not found")
return Response(item)
Field-level validation errors
from fusion.responses import FieldError, ValidationError
return ValidationError(
detail="Validation failed",
errors=[
FieldError(field="email", message="invalid format"),
FieldError(field="name", message="required"),
],
)
Custom problem types
import typing
from fusion.responses import Problem
class OutOfStockProblem(Problem):
type: typing.ClassVar[str] = "https://example.com/problems/out-of-stock"
status: typing.ClassVar[int] = 409
title: str = "Out of Stock"
# In a handler:
return OutOfStockProblem(detail="Item #42 is out of stock")
CLI
Fusion ships a fusion command for schema management and serving.
Schema management
# Snapshot a single module, a whole package, or multiple modules at once
fusion snapshot myapp.models
fusion snapshot myapp # walks the package recursively
fusion snapshot myapp.models myapp.auth.models
# Check for drift — exits 1 on changes, safe as a pre-commit hook
fusion check myapp
# Apply pending DDL to Postgres
fusion migrate myapp --dsn postgresql://user:pass@host/db
# Use POSTGRES_DSN env var instead of --dsn
POSTGRES_DSN=postgresql://user:pass@host/db fusion migrate myapp
# Allow DROP TABLE / DROP COLUMN
fusion migrate myapp --dsn postgresql://... --drop
All three commands accept one or more module/package arguments and --snapshot <path> to override the default migrations/snapshot.yaml location.
Running the server
# Start the app with uvicorn (requires: pip install uvicorn)
fusion serve myapp:app
# Custom host / port / auto-reload
fusion serve myapp:app --host 127.0.0.1 --port 9000 --reload
Requirements
- Python 3.12+
msgspec >= 0.21.1typedprotocol >= 0.1.0
License
MIT — see LICENSE.md.
Project details
Release history Release notifications | RSS feed
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 fusion-0.9.0.tar.gz.
File metadata
- Download URL: fusion-0.9.0.tar.gz
- Upload date:
- Size: 99.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7250d2a078de853939540d99f1707fa3f8c3e59b3ce978085540630de3eb0c32
|
|
| MD5 |
1a4aa2165350fe424991ffc444a5d2f1
|
|
| BLAKE2b-256 |
0fc435c3f10df501f38c56b5018c1aea756fe5d7820016ca79c7e5a3d0bb4403
|
Provenance
The following attestation bundles were made for fusion-0.9.0.tar.gz:
Publisher:
release.yml on okanakbulut/fusion
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fusion-0.9.0.tar.gz -
Subject digest:
7250d2a078de853939540d99f1707fa3f8c3e59b3ce978085540630de3eb0c32 - Sigstore transparency entry: 1409261920
- Sigstore integration time:
-
Permalink:
okanakbulut/fusion@0457046b2d8c0d77c168299b64efcb480376c5f9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/okanakbulut
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0457046b2d8c0d77c168299b64efcb480376c5f9 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file fusion-0.9.0-py3-none-any.whl.
File metadata
- Download URL: fusion-0.9.0-py3-none-any.whl
- Upload date:
- Size: 39.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
17540610331beb511a756a43917133df229485cf0020a7594c81677d65441153
|
|
| MD5 |
2a762bed912a128b1a959fb733e6ad69
|
|
| BLAKE2b-256 |
62758e9af8bec8698241a9a44f5660d1873f44d8ef156bbd02b66164d752b7be
|
Provenance
The following attestation bundles were made for fusion-0.9.0-py3-none-any.whl:
Publisher:
release.yml on okanakbulut/fusion
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
fusion-0.9.0-py3-none-any.whl -
Subject digest:
17540610331beb511a756a43917133df229485cf0020a7594c81677d65441153 - Sigstore transparency entry: 1409261947
- Sigstore integration time:
-
Permalink:
okanakbulut/fusion@0457046b2d8c0d77c168299b64efcb480376c5f9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/okanakbulut
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0457046b2d8c0d77c168299b64efcb480376c5f9 -
Trigger Event:
workflow_dispatch
-
Statement type: