Lightweight Dependency Injection for FastAPI - Type hint-based, zero dependencies
Project description
Eagle DI - Lightweight Dependency Injection for FastAPI
EAGLE DI
Type hint-based DI for FastAPI. Auto-inject services without explicit Depends().
A pure Python, zero-dependency DI mini utility built specifically for FastAPI applications.
๐ฆ Looking for more utilities? Check out the docs/ folder for additional features like Spring-style
@Transactionaldecorator andServerlessadapters.
๐ฅ Installation
Option A: Copy-Paste Ready (Original Philosophy)
Just copy the file(s) you need - zero pip install required:
# Core DI only (~1500 lines)
cp eagle_di.py your_project/core/
# + Transaction support (~500 lines)
cp transaction.py your_project/core/
# + Serverless adapters (~800 lines)
cp serverless.py your_project/core/
Option B: Install from PyPI
pip install eagle-di # Core only
pip install eagle-di[transaction] # + @Transactional
pip install eagle-di[aws] # + Lambda support
pip install eagle-di[serverless] # + All serverless
๐ v5.0.0 - Serverless Support
NEW in v5.0.0 - Deploy Eagle DI to AWS Lambda, Azure Functions, and Google Cloud Run!
What's New
Multi-cloud serverless adapters with cold start optimization and lifecycle hooks:
from fastapi import FastAPI
from app.core.serverless import LambdaAdapter, OnColdStart, Timeout
app = FastAPI()
@OnColdStart
async def init_db():
"""Runs once on cold start"""
await database.connect()
@app.get("/users/{id}")
@Timeout(25) # Graceful timeout (leave 5s buffer for Lambda's 30s limit)
async def get_user(id: int, service: UserService):
return await service.get_user(id)
# AWS Lambda handler
adapter = LambdaAdapter(app)
handler = adapter.handler
Supported Platforms
| Platform | Adapter | Template |
|---|---|---|
| AWS Lambda | LambdaAdapter |
templates/serverless/aws/ |
| Azure Functions | AzureFunctionsAdapter |
templates/serverless/azure/ |
| Google Cloud Run | CloudRunAdapter |
templates/serverless/gcp/ |
Key Features
- ๐ฅ
@OnColdStart- Initialize connections/resources on cold start - โก
@OnWarmUp- Provisioned concurrency warmup handler - โฑ๏ธ
@Timeout(seconds)- Graceful timeout with buffer - ๐
ServerlessDatabaseProvider- Small pool size, aggressive recycling
๐ See docs/SERVERLESS.md for full documentation.
๐จ v4.0.0 Breaking Changes
๐ v4.0.0 - Introducing
InjectableRouterfor automatic dependency injection!
What's New
InjectableRouter is a new FastAPI router that automatically handles dependency injection for all routes without needing @AutoInject decorator!
# โ OLD WAY (v3.x) - Required @AutoInject on every route
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/{id}")
@AutoInject
async def get_user(id: int, service: UserService):
return await service.get_user(id)
# โ
NEW WAY (v4.0) - No decorator needed!
from app.core.eagle_di import InjectableRouter
router = InjectableRouter()
@router.get("/users/{id}") # @AutoInject not needed!
async def get_user(id: int, service: UserService):
return await service.get_user(id)
Backward Compatibility
- โ
@AutoInjectwith@approutes still works (for simple CRUD) - โ All existing v3.x code continues to work
- โ Migration is optional but recommended
Migration Guide
- Replace
APIRouter()โInjectableRouter() - Remove
@AutoInjectfrom routes (InjectableRouter handles it automatically) - That's it! ๐
Rationale
The main reasons behind this DI framework design are:
- Zero external dependencies - Single file, copy-paste ready, no
pip installneeded - Type hint-based injection - Let Python's type system do the wiring
- FastAPI-native - Seamless integration with FastAPI's
Depends()system - Singleton by default - Optimized for web applications where services are stateless
โ When to use this DI
- You want a simple, drop-in DI solution for FastAPI
- You prefer convention over configuration (auto-inject by type)
- You need DI in background workers/Celery tasks via
get_service() - You want < 1000 LOC to understand, debug, and maintain
- You care about startup simplicity more than micro-optimizations
โ When NOT to use this DI
- You need transient/request scopes (this only supports singleton)
- You require Cython-level performance (use
dependency-injector) - You want advanced features like conditional providers, async factories
- You need multi-container isolation in the same process
- Your project has 500+ injectable classes (consider a compiled solution)
๐ Quick Start
Method 1๏ธโฃ: InjectableRouter (Recommended for Larger Projects)
Perfect for organized projects with multiple routes. No @AutoInject decorator needed!
from fastapi import FastAPI
from app.core.eagle_di import Injectable, InjectableRouter, process_async_inits
# 1. Define your services
@Injectable
class UserRepository:
async def async_init(self):
"""Called automatically during startup"""
print("๐ Connecting to database...")
async def get_user(self, user_id: int):
return {"id": user_id, "name": f"User{user_id}"}
@Injectable
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo # Auto-injected!
async def get_user(self, user_id: int):
return await self.repo.get_user(user_id)
# 2. Create InjectableRouter (NOT APIRouter!)
from app.core.eagle_di import InjectableRouter
router = InjectableRouter(prefix="/api")
# 3. Define routes - dependency injection happens automatically!
@router.get("/users/{user_id}")
async def get_user(user_id: int, service: UserService):
# UserService is auto-injected, no @AutoInject needed!
return await service.get_user(user_id)
@router.post("/users")
async def create_user(data: dict, service: UserService):
# Works with all FastAPI features: body, query params, headers, etc.
return {"created": True}
# 4. Setup FastAPI app
app = FastAPI()
app.include_router(router)
@app.on_event("startup")
async def startup():
await process_async_inits() # Initialize all services
Method 2๏ธโฃ: @AutoInject with @app Routes (For Simple CRUD)
Perfect for quick prototypes or simple services. Use @AutoInject decorator on routes.
from fastapi import FastAPI
from app.core.eagle_di import Injectable, AutoInject, process_async_inits
# 1. Define your services (same as above)
@Injectable
class UserService:
def get_user(self, user_id: int):
return {"id": user_id, "name": f"User{user_id}"}
# 2. Create regular FastAPI app
app = FastAPI()
# 3. Use @AutoInject decorator on routes
@app.get("/users/{user_id}")
@AutoInject # Add this decorator for DI
async def get_user(user_id: int, service: UserService):
return service.get_user(user_id)
@app.post("/users")
@AutoInject # Required for each route
async def create_user(data: dict, service: UserService):
return {"created": True}
@app.on_event("startup")
async def startup():
await process_async_inits()
Important: With @app routes, @AutoInject must be placed BELOW the route decorator:
# โ
CORRECT
@app.get("/users/{id}")
@AutoInject
def get_user(id: int, service: UserService):
pass
# โ WRONG - Won't work!
@AutoInject
@app.get("/users/{id}")
def get_user(id: int, service: UserService):
pass
Performance Benchmarks
| Scenario | Time | Notes |
|---|---|---|
| Small Project (20 classes) | 1.09ms | Registration |
| Medium Project (50 classes) | 1.25ms | Registration |
| Large Project (100 classes) | 3.22ms | Registration |
| Deep Dependencies (10 levels) | 0.05ms | Resolution |
| Singleton Cache Hit | 0.0004ms | Blazing fast |
| Concurrent (10 threads) | 2.08ms | Thread-safe โ |
| ColdโWarm Speedup | 94x | Cached singleton |
vs dependency-injector Library
| Feature | This DI | dependency-injector |
|---|---|---|
| Auto-inject by type hint | โ | โ Manual wiring |
| Singleton scope | โ Default | โ |
| Request/Transient scope | โ | โ |
| Lifecycle hooks | โ | โ |
| Circular deps | โ forwardRef | โ |
| Testing utilities | โ | โ |
| Zero dependencies | โ Pure Python | โ Cython |
| Copy-paste ready | โ 1 file | โ pip install |
| LOC | ~1200 | ~15,000+ |
Summary: 80% of features with 5% of complexity. Perfect for small-medium projects!
Speed Benchmark (honest comparison)
| Metric | This DI | dependency-injector | Winner |
|---|---|---|---|
| Registration (50 classes) | 1.49ms | 1.16ms | DI Library (1.3x) |
| Resolution (1000 cached) | 0.50ms | 0.24ms | DI Library (2.1x) |
| Deep chain (5 levels) | 0.022ms | 0.011ms | DI Library (2.0x) |
Why?
dependency-injectoruses Cython (compiled to C).Does it matter? Not really! DI only runs at startup (once). Your API response time won't be affected.
API Reference
| Function/Decorator | Purpose |
|---|---|
@Injectable |
Register a class for DI (singleton by default) |
InjectableRouter |
Register a router for DI (singleton by default) |
@AutoInject |
Auto-inject deps into FastAPI endpoint (Deprecated, now just use for @app with simple CRUD) |
@Controller(prefix, tags) |
Controller decorator (combines Injectable + routing for Nest/Spring fan, if you're not, just use InjectableRouter) |
Provide(cls) |
Explicit injection for edge cases |
get_service(cls) |
Get service instance programmatically |
forwardRef(lambda: Type) |
Lazy reference for circular deps (details below) |
Inject(forwardRef(...)) |
TRUE circular dependency (returns getter) (details below) |
Testing Utilities
| Function | Purpose |
|---|---|
override(cls, mock) |
Context manager to mock a provider |
test_container() |
Context manager for test isolation |
clear_registry() |
Clear all registrations (for testing) |
# example
@pytest.mark.asyncio
async def test_orbit_simulation_determinism_with_service():
"""
Test that the Orbit simulation produces deterministic results using the SimulationService.
Uses DI override system to mock database dependencies while keeping
real simulation logic intact.
"""
from app.services.simulation.database_provider import DatabaseProvider
from app.services.simulation.simulation_persistence_service import SimulationPersistenceService
from app.services.simulation.config_service import ConfigService
from app.services.simulation.here_routing_service import HereRoutingService
from app.services.v1.orbit.lead_filter_service import LeadFilterService
from app.services.v1.orbit.calendar_sync_service import CalendarSyncService
# Create mock objects
mock_db_provider = Mock(spec=DatabaseProvider)
@asynccontextmanager
async def mock_transaction(isolation_level=None):
mock_session = AsyncMock()
mock_session.flush = AsyncMock()
yield mock_session
mock_db_provider.transaction = mock_transaction
mock_db_provider.session = mock_transaction
mock_db_service = Mock(spec=SimulationPersistenceService)
mock_simulation_run = Mock()
mock_simulation_run.id = "test-run-id"
mock_db_service.save_simulation_run = AsyncMock(return_value=mock_simulation_run)
mock_db_service.update_simulation_status = AsyncMock()
mock_db_service.save_iterations = AsyncMock()
mock_db_service.extract_scheduled_visits = Mock(return_value=[])
mock_db_service.save_scheduled_visits = AsyncMock()
mock_config_service = Mock(spec=ConfigService)
mock_config = Mock()
mock_config.id = 1
mock_config.name = "config-1"
mock_config.config = {}
mock_config_service.get_config_by_id = AsyncMock(return_value=None)
mock_config_service.create_config = AsyncMock(return_value=mock_config)
mock_here_routing = Mock(spec=HereRoutingService)
mock_here_routing.get_polyline = AsyncMock(return_value=None)
mock_here_routing.get_route = AsyncMock(return_value=None)
# Mock new services (optional for basic simulation)
mock_lead_filter = Mock(spec=LeadFilterService)
mock_calendar_sync = Mock(spec=CalendarSyncService)
# Use DI override to inject mocks - now works because resolver skips overridden deps
with override(DatabaseProvider, mock_db_provider), \
override(SimulationPersistenceService, mock_db_service), \
override(ConfigService, mock_config_service), \
override(HereRoutingService, mock_here_routing), \
override(LeadFilterService, mock_lead_filter), \
override(CalendarSyncService, mock_calendar_sync):
# Get SimulationService with mocked dependencies
simulation_service = get_service(SimulationService)
# Create simulation config
config = SimulationConfig(
calendar_id="test@example.com",
start_date=datetime(2026, 1, 1).date(),
weeks=6,
max_iterations=30,
verbose=False,
leads_count=300
)
# Run simulation twice with the same configuration
result_1 = await simulation_service.run(config)
result_2 = await simulation_service.run(config)
# Assert that the results are identical
assert result_1.stats == result_2.stats, f"Simulation results differ: {result_1.stats} != {result_2.stats}"
# Log the results for debugging
print(f"Simulation results (Run 1): {result_1.stats}")
print(f"Simulation results (Run 2): {result_2.stats}")
Lifecycle
| Function | Purpose |
|---|---|
on_init() |
Method on service, called after instantiation |
on_destroy() |
Method on service, called during shutdown |
process_async_inits() |
NEW Await all queued async on_init() hooks |
async_shutdown_all() |
Call all on_destroy() hooks |
Singleton Scope (Default)
All @Injectable services are singletons by default:
@Injectable
class UserService:
pass
# Both get the SAME instance
service1 = get_service(UserService)
service2 = get_service(UserService)
assert service1 is service2 # โ
Same instance
Lifecycle Hooks
@Injectable
class CacheService:
async def on_init(self):
"""Called after instantiation"""
self.client = await connect_redis()
async def on_destroy(self):
"""Called during shutdown"""
await self.client.close()
Hook into FastAPI lifespan:
from app.core.injector import async_shutdown_all, get_service, process_async_inits
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: Eagerly init services (queues async on_init)
_ = get_service(CacheService)
# โ ๏ธ v2.0: Must await async on_init() hooks
await process_async_inits()
yield
# Shutdown: Call all on_destroy hooks
await async_shutdown_all()
Programmatic Access
Use get_service() outside of FastAPI request context:
from app.core.injector import get_service
# Background task
async def process_queue():
service = get_service(UserService)
await service.notify(user_id)
# CLI script
if __name__ == "__main__":
service = get_service(MyService)
service.run()
Circular Dependencies
โ ๏ธ Always refactor your code to avoid circular dependencies!
forwardRefandInjectshould only be used as a last resort.
Pattern 1: One-way with forwardRef
def _get_a():
from .service_a import ServiceA
return ServiceA
@Injectable
class ServiceB:
def __init__(self, a: forwardRef(_get_a)):
self.a = a # Instance of ServiceA
Pattern 2: TRUE Circular with Inject(forwardRef(...))
# service_a.py
def _get_b():
from .service_b import ServiceB
return ServiceB
@Injectable
class ServiceA:
def __init__(self, get_b: Inject(forwardRef(_get_b))):
self._get_b = get_b # โ GETTER FUNCTION, not instance!
def use_b(self):
return self._get_b().do_something() # โ Call when needed
Testing Utilities
override() - Mock a Provider
from app.core.injector import override
from unittest.mock import Mock
def test_user_endpoint():
mock_service = Mock()
mock_service.get_user.return_value = {"id": 1}
with override(UserService, mock_service):
response = client.get("/users/1")
assert response.json()["id"] == 1
# Original provider restored automatically
test_container() - Complete Isolation
from app.core.injector import test_container, Injectable
def test_isolated():
with test_container():
@Injectable
class TestService:
pass
# Fresh registry, only TestService exists
# Original registry restored
clear_registry() - Reset All
@pytest.fixture(autouse=True)
def reset_di():
yield
clear_registry()
Debugging
Set DI_VERBOSE=1 to see detailed logs:
DI_VERBOSE=1 make dev
Limitations
โ ๏ธ Services with FastAPI dependencies (e.g., db)
Services that depend on FastAPI-specific dependencies like db: AsyncSession = Depends(get_db) cannot be accessed via get_service() until they've been "warmed up" by at least one HTTP request.
@Injectable
class UserService:
def __init__(self, db: Annotated[AsyncSession, Depends(get_db)]):
self.db = db
# โ This will FAIL if no request has been made yet
service = get_service(UserService)
# โ
After first HTTP request, singleton is cached and get_service() works
Workaround for background workers:
@Injectable
class UserService:
def __init__(self, db: Annotated[AsyncSession, Depends(get_db)]):
self.db = db
async def process(self, db: AsyncSession | None = None):
"""
Methods that workers will use should accept optional db param.
- db=None (from app) โ use self.db
- db not None (from worker) โ use passed db
"""
session = db or self.db
await session.execute(...)
# In worker:
async def background_task():
async with async_session_maker() as session:
service = get_service(UserService)
await service.process(db=session) # Pass worker's session
Alternative approaches:
- For services that need programmatic access, ensure they only depend on other
@Injectableclasses, not FastAPIDepends(). - Or make a dummy HTTP request during startup to warm up the cache.
Parameter Order Limitation
This is a Python language constraint, not a framework limitation.
When placing service parameters before required params (path, query), you must give the service a default value. This applies to BOTH @AutoInject and InjectableRouter.
# โ WRONG - Python syntax error (for BOTH patterns)
@app.get("/users/{id}")
@AutoInject
def get_user(service: UserService, id: int): # Error!
pass
# Also fails with InjectableRouter!
router = InjectableRouter()
@router.get("/users/{id}")
def get_user(service: UserService, id: int): # Same error!
pass
# โ
CORRECT - Service has default value
@app.get("/users/{id}")
@AutoInject
def get_user(service: UserService = None, id: int = Path()):
pass
# โ
BEST - Put service AFTER required params (cleaner!)
@app.get("/users/{id}")
@AutoInject
def get_user(id: int, service: UserService):
pass
Why? When the framework transforms the signature, it adds = Depends(...) to service params. Python doesn't allow parameters without defaults to come after parameters with defaults.
# What happens internally:
def get_user(service: UserService, id: int):
pass
# Transforms to:
def get_user(service: UserService = Depends(...), id: int): # โ Python error!
pass
Solution: Always put injectable services AFTER required parameters!
Best Practices
โ Controllers (Routes/Endpoints)
Always use explicit FastAPI parameter annotations for clarity and better Swagger documentation:
from fastapi import Path, Query, Body, Header
from app.core.eagle_di import InjectableRouter, Injectable
@Injectable
class UserService:
def get_user(self, user_id: int) -> dict:
return {"id": user_id, "name": f"User{user_id}"}
def search_users(self, query: str, limit: int) -> list:
return [{"name": query}][:limit]
router = InjectableRouter(prefix="/api")
# โ
GOOD - Explicit parameter annotations
@router.get("/users/{user_id}")
async def get_user(
user_id: int = Path(..., description="User ID"),
include_metadata: bool = Query(False, description="Include metadata"),
service: UserService = None # Auto-injected, can omit annotation
):
return service.get_user(user_id)
@router.post("/users")
async def create_user(
data: dict = Body(...),
x_request_id: str = Header(None),
service: UserService = None
):
return {"created": True, "request_id": x_request_id}
# โ BAD - Implicit parameters (unclear in Swagger)
@router.get("/search")
async def search_users(q: str, limit: int, service: UserService):
# Works, but Swagger won't show parameter descriptions
return service.search_users(q, limit)
Why?
- โ Better Swagger/OpenAPI documentation
- โ Clear validation rules and descriptions
- โ Easier for frontend developers to understand API
- โ Type hints + FastAPI annotations = bulletproof API
โ Services (Business Logic)
Services should NOT use FastAPI dependencies - keep them pure Python:
from app.core.eagle_di import Injectable
# โ
GOOD - Pure Python service
@Injectable
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo # DI-injected service
def get_user(self, user_id: int) -> dict:
"""Pure business logic - no FastAPI deps"""
return self.repo.find_by_id(user_id)
def search_users(self, query: str, limit: int = 10) -> list:
"""Simple Python parameters"""
return self.repo.search(query, limit)
# โ BAD - Service with FastAPI dependencies
@Injectable
class BadUserService:
def __init__(self, db: Annotated[Session, Depends(get_db)]):
# โ Don't do this! Services should be framework-agnostic
self.db = db
def get_user(self, user_id: int = Path(...)):
# โ Services shouldn't use Path/Query/Body!
pass
Why?
- โ Services stay framework-agnostic (can be reused in CLI, workers, tests)
- โ Easier to test (no FastAPI dependencies to mock)
- โ Clear separation of concerns (controller = HTTP, service = business logic)
- โ
Services accessible via
get_service()anywhere in codebase
โ Layered Architecture Pattern
# ========================================
# LAYER 1: Controllers (routes.py)
# ========================================
from fastapi import Path, Query, Body
from app.core.eagle_di import InjectableRouter
router = InjectableRouter(prefix="/api/users")
@router.get("/{user_id}")
async def get_user_endpoint(
user_id: int = Path(..., ge=1), # FastAPI validation
include_posts: bool = Query(False),
service: UserService = None # DI-injected
):
"""HTTP layer - handles request/response"""
return await service.get_user_with_posts(user_id, include_posts)
# ========================================
# LAYER 2: Services (services/user_service.py)
# ========================================
from app.core.eagle_di import Injectable
@Injectable
class UserService:
def __init__(self, repo: UserRepository, post_svc: PostService):
self.repo = repo
self.post_svc = post_svc
async def get_user_with_posts(self, user_id: int, include_posts: bool) -> dict:
"""Business logic - no FastAPI deps"""
user = await self.repo.get_by_id(user_id)
if include_posts:
user['posts'] = await self.post_svc.get_user_posts(user_id)
return user
# ========================================
# LAYER 3: Repositories (repositories/user_repo.py)
# ========================================
@Injectable
class UserRepository:
async def get_by_id(self, user_id: int) -> dict:
"""Data access - pure queries"""
# Database logic here
return {"id": user_id, "name": "Alice"}
โ DO
- โ
Use
InjectableRouterfor production apps (5+ routes) - โ Put services AFTER required params in route signatures
- โ
Use explicit
Path(),Query(),Body()in controllers - โ Keep services framework-agnostic (pure Python)
- โ
Implement
async_init()for async setup (DB connections, etc.) - โ
Use
get_service()for programmatic access (workers, CLI) - โ Layer architecture: Controller โ Service โ Repository
โ DON'T
- โ Put service param before required params without
= None - โ Use FastAPI
Depends()in service constructors - โ Use
Path(),Query(),Body()in service methods - โ Create circular dependencies (refactor instead!)
- โ Call
get_service()in__init__methods - โ Mix business logic in controllers (keep them thin)
Test Suite
Run all DI tests to verify the framework works correctly:
# Run all DI tests
pytest tests/ -v -s
# Run specific test files
pytest tests/test_injection.py -v -s
๐ค When to Use Which Pattern?
| Pattern | Best For | Pros | Cons |
|---|---|---|---|
| InjectableRouter | Production apps, multiple routes | โ
No decorator on routes โ Cleaner code โ Better organization |
โ Slight overhead at router creation |
| @AutoInject + @app | Quick prototypes, 1-5 routes | โ
Simple setup โ Direct routing |
โ Decorator on every route โ Less organized |
| @Controller | Fans of NestJS/Spring | โ
Familiar syntax โ Class-based โ DI in constructor |
โ Requires registration |
Rule of Thumb:
- ๐ฆ Use
InjectableRouterif you have 5+ routes or are building a production app - โก Use
@AutoInjectfor quick scripts, demos, or very simple APIs - ๐ฏ Use
@Controllerif you love NestJS/Spring and want class-based controllers
๐ฏ Method 3๏ธโฃ: @Controller (For NestJS & Spring Boot Fans)
If you're coming from NestJS or Spring Boot, you'll feel right at home with this pattern!
from fastapi import FastAPI, Path, Query, Body
from app.core.eagle_di import (
Injectable,
Controller,
Get, Post, Put, Delete, Patch,
register_controller,
process_async_inits
)
# 1. Define your services (same as before)
@Injectable
class UserRepository:
def get_user(self, user_id: int) -> dict:
return {"id": user_id, "name": f"User{user_id}"}
@Injectable
class UserService:
def __init__(self, repo: UserRepository): # DI in constructor!
self.repo = repo
def get_user(self, user_id: int) -> dict:
return self.repo.get_user(user_id)
def create_user(self, name: str) -> dict:
return {"created": True, "name": name}
# 2. Define Controller with NestJS-style decorators
@Controller(prefix="/api/users", tags=["Users"])
class UserController:
def __init__(self, service: UserService): # Service auto-injected!
self.service = service
@Get()
def list_users(self):
"""GET /api/users"""
return [{"id": 1}, {"id": 2}]
@Get("/{user_id}")
def get_user(self, user_id: int = Path(..., ge=1)):
"""GET /api/users/{user_id}"""
return self.service.get_user(user_id)
@Post()
def create_user(self, data: dict = Body(...)):
"""POST /api/users"""
return self.service.create_user(data["name"])
@Put("/{user_id}")
def update_user(self, user_id: int, data: dict = Body(...)):
"""PUT /api/users/{user_id}"""
return {"updated": True, "id": user_id}
@Delete("/{user_id}")
def delete_user(self, user_id: int):
"""DELETE /api/users/{user_id}"""
return {"deleted": True, "id": user_id}
# 3. Register controller with FastAPI app
app = FastAPI()
register_controller(UserController, app)
@app.on_event("startup")
async def startup():
await process_async_inits()
Multiple Controllers:
@Controller(prefix="/products", tags=["Products"])
class ProductController:
def __init__(self, product_service: ProductService):
self.product_service = product_service
@Get()
def list_products(self):
return self.product_service.list_all()
app = FastAPI()
register_controller(UserController, app)
register_controller(ProductController, app) # Multiple controllers!
Why Controllers?
- โ Familiar for NestJS/Spring developers - Same @Controller, @Get, @Post pattern
- โ Class-based organization - Services injected in constructor
- โ Clean separation - Controller handles HTTP, service handles business logic
- โ All HTTP methods - @Get, @Post, @Put, @Delete, @Patch
- โ Works with all FastAPI features - Path, Query, Body, Header parameters
๐ Test Suite
Tests are organized into logical groups for easy navigation:
tests/
โโโ core/ # Core DI functionality
โโโ integration/ # FastAPI integration
โโโ controller/ # Controller pattern
โโโ transaction/ # Transaction management
โโโ benchmarks/ # Performance tests
tests/core/ - Core DI (83 tests)
| Test File | Tests | Description |
|---|---|---|
test_injection.py |
19 | Singleton, override, circular deps |
test_advanced_features.py |
23 | Advanced DI patterns & features |
test_edge_cases.py |
20 | Error handling & edge scenarios |
test_limitations.py |
12 | Known limitations & constraints |
test_async_lifecycle.py |
9 | Async on_init/on_destroy |
tests/integration/ - FastAPI Integration (46 tests)
| Test File | Tests | Description |
|---|---|---|
test_fastapi_integration.py |
15 | Path, query, body, header params |
test_injectable_router.py |
14 | Router-level DI injection |
test_backward_compatibility.py |
12 | Legacy API compatibility |
test_swagger_compatible.py |
5 | OpenAPI/Swagger integration |
tests/controller/ - Controller Pattern (28 tests)
| Test File | Tests | Description |
|---|---|---|
test_controller.py |
21 | Controller pattern implementation |
test_controller_edge_cases.py |
7 | Controller error scenarios |
tests/transaction/ - Transaction Management (55 tests)
| Test File | Tests | Description |
|---|---|---|
test_transaction.py |
37 | Transaction management & rollback |
test_transaction_advanced.py |
18 | Nested transactions & savepoints |
tests/benchmarks/ - Performance (16 tests)
| Test File | Tests | Description |
|---|---|---|
test_performance.py |
11 | Benchmarks & scalability |
test_benchmark_compare.py |
5 | Performance vs dependency-injector |
| Total | 228 | โ All passing |
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 eagle_di-5.0.1.tar.gz.
File metadata
- Download URL: eagle_di-5.0.1.tar.gz
- Upload date:
- Size: 53.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00e65f6195665069c6510d9921ffd7658dc633e6ae6d20b943d7c7a91c540b88
|
|
| MD5 |
919654c8d14c81337a01097a4d8d43ed
|
|
| BLAKE2b-256 |
09db7be3af0387e3661712f737c5667e12cf2f69dff6f705baf0948bfbbb1b9e
|
Provenance
The following attestation bundles were made for eagle_di-5.0.1.tar.gz:
Publisher:
ci.yml on DavidNguyen2212/eagle-di
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
eagle_di-5.0.1.tar.gz -
Subject digest:
00e65f6195665069c6510d9921ffd7658dc633e6ae6d20b943d7c7a91c540b88 - Sigstore transparency entry: 836216111
- Sigstore integration time:
-
Permalink:
DavidNguyen2212/eagle-di@03fbdff7b036885d03c2cdcc7c313e88856ae8a5 -
Branch / Tag:
refs/tags/v5.0.1 - Owner: https://github.com/DavidNguyen2212
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@03fbdff7b036885d03c2cdcc7c313e88856ae8a5 -
Trigger Event:
release
-
Statement type:
File details
Details for the file eagle_di-5.0.1-py3-none-any.whl.
File metadata
- Download URL: eagle_di-5.0.1-py3-none-any.whl
- Upload date:
- Size: 36.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84a509d73eb5ddafafbeaa93370bf32bafc4c400c0d51af9249d6fb5e7773d28
|
|
| MD5 |
5d4df6e18a8c4ead16929a6f4a673588
|
|
| BLAKE2b-256 |
02ca748d78f54a657b4a0f7e8652c463008e68b62c9bbe59486f3ee68f761b48
|
Provenance
The following attestation bundles were made for eagle_di-5.0.1-py3-none-any.whl:
Publisher:
ci.yml on DavidNguyen2212/eagle-di
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
eagle_di-5.0.1-py3-none-any.whl -
Subject digest:
84a509d73eb5ddafafbeaa93370bf32bafc4c400c0d51af9249d6fb5e7773d28 - Sigstore transparency entry: 836216116
- Sigstore integration time:
-
Permalink:
DavidNguyen2212/eagle-di@03fbdff7b036885d03c2cdcc7c313e88856ae8a5 -
Branch / Tag:
refs/tags/v5.0.1 - Owner: https://github.com/DavidNguyen2212
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@03fbdff7b036885d03c2cdcc7c313e88856ae8a5 -
Trigger Event:
release
-
Statement type: