Skip to content

Commit 1ca6a7b

Browse files
stackjayjaygaha
authored andcommitted
day #48 fastapi #6 dependencies & dependency injection
1 parent a67fa11 commit 1ca6a7b

12 files changed

Lines changed: 1011 additions & 0 deletions

File tree

workspace/7_framework/fastapi/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ uvicorn main:app --reload
8585
Learn how to use and return appropriate HTTP status codes, create custom exception classes, handle validation and business logic errors, and implement global exception handlers for consistent error responses.
8686
_Includes: custom error classes, exception handlers, validation and business logic error handling, logging, and test coverage._
8787

88+
- [Day 06: Dependencies and Dependency Injection](day06/README.md)
89+
Learn how to use FastAPI's powerful dependency injection system to manage dependencies, handle security, and create reusable components.
90+
_Includes: function and class-based dependencies, authentication, authorization, sub-dependencies, and dependency caching._
91+
8892
---
8993

9094
## Recommended Project Structure
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# FastAPI Day 06: Dependencies and Dependency Injection
2+
3+
Welcome to **Day 06** of the FastAPI tutorial! Today you'll learn how to leverage FastAPI's powerful dependency injection system to write cleaner, more modular, and maintainable code.
4+
5+
---
6+
7+
## What You'll Learn
8+
9+
- Understand the core principles of dependency injection in FastAPI.
10+
- Create simple, reusable function-based and class-based dependencies.
11+
- Implement robust authentication and authorization using dependencies.
12+
- Manage complex dependency chains with sub-dependencies.
13+
- Understand and utilize dependency caching for better performance.
14+
- Write effective tests for endpoints that rely on dependencies.
15+
16+
---
17+
18+
## Key Concepts
19+
20+
### 1. Dependency Injection System
21+
22+
**Dependency Injection (DI)** is a design pattern where a component's dependencies (i.e., services or objects it needs to function) are "injected" from an external source rather than created internally. In FastAPI, this is managed by the `Depends` function.
23+
24+
This system allows you to:
25+
- **Share Logic**: Reuse the same code across multiple endpoints.
26+
- **Share Database Connections**: Manage the lifecycle of database connections.
27+
- **Enforce Security**: Implement authentication, authorization, and role-based access control.
28+
- **Improve Testability**: Easily mock or override dependencies during testing.
29+
30+
### 2. Creating Dependencies
31+
32+
Any function or class that returns a value can be a dependency. You simply define it and then pass it to `Depends` in your path operation function.
33+
34+
**Function-based Dependency:** A simple Python function can serve as a dependency. This is useful for self-contained logic like managing a database session.
35+
36+
Example dependency:
37+
```python
38+
# from dependencies.py
39+
40+
class DatabaseConnection:
41+
def __init__(self):
42+
self.connection_id = f"conn_{datetime.now().timestamp()}"
43+
44+
def get_database():
45+
db = DatabaseConnection()
46+
try:
47+
yield db # Provide the connection
48+
finally:
49+
# Code here runs after the request is finished
50+
print(f"Closing DB connection {db.connection_id}")
51+
```
52+
53+
**Class-based Dependency:** A class can be used to group related request parameters. FastAPI will treat the class itself as a dependency and instantiate it with parameters from the request.
54+
55+
Example dependency for query parameters:
56+
```python
57+
# from dependencies.py
58+
59+
class CommonQueryParams:
60+
def __init__(
61+
self,
62+
q: Optional[str] = Query(None),
63+
include_inactive: bool = Query(False)
64+
):
65+
self.q = q
66+
self.include_inactive = include_inactive
67+
```
68+
69+
### 3. Authentication with Dependencies
70+
71+
A common and powerful use case for dependencies is handling security. You can create a dependency that verifies a token and returns the current user.
72+
73+
Example `get_current_user` dependency:
74+
```python
75+
# from dependencies.py
76+
77+
def get_current_user(username: str = Depends(verify_token)) -> User:
78+
user_data = users_db.get(username)
79+
if user_data is None or not user_data["is_active"]:
80+
raise HTTPException(status_code=401, detail="Could not validate credentials")
81+
return User(**user_data)
82+
```
83+
84+
You can then protect an endpoint by adding this dependency:
85+
```python
86+
# from main.py
87+
88+
@app.get("/users/me")
89+
def read_users_me(current_user: User = Depends(get_current_user)):
90+
return current_user
91+
```
92+
If the token is invalid or the user doesn't exist, the request will be stopped with a `401 Unauthorized` error before your endpoint code is even executed.
93+
94+
### 4. Sub-dependencies and Authorization
95+
96+
Dependencies can depend on other dependencies. FastAPI automatically handles resolving this chain. This is useful for building layers of functionality, like adding authorization on top of authentication.
97+
98+
For example, `get_admin_user` first ensures the user is authenticated by depending on `get_current_user`, and then it performs an additional check to ensure the user has the "admin" role.
99+
100+
Example admin check:
101+
```python
102+
# from dependencies.py
103+
104+
def get_admin_user(current_user: User = Depends(get_current_user)) -> User:
105+
if "admin" not in current_user.roles:
106+
raise HTTPException(
107+
status_code=status.HTTP_403_FORBIDDEN,
108+
detail="Not enough permissions"
109+
)
110+
return current_user
111+
```
112+
113+
### 5. Dependency Caching
114+
115+
Within a single request, FastAPI caches the return value of a dependency. If multiple parts of your code (e.g., your endpoint and another dependency) depend on the same dependency with the same parameters, FastAPI will only call it once and reuse the result.
116+
117+
This is crucial for performance, especially for expensive operations or database connections.
118+
119+
Example of a cached dependency:
120+
```python
121+
# from main.py
122+
123+
# This function will only run once per request, even if multiple
124+
# endpoints or other dependencies require it.
125+
def expensive_operation():
126+
time.sleep(0.1) # Simulate delay
127+
return {"computed_value": "expensive_result"}
128+
129+
@app.get("/expensive")
130+
def get_expensive_data(result=Depends(expensive_operation)):
131+
return {"expensive_data": result}
132+
```
133+
134+
---
135+
136+
## Next Steps
137+
138+
- Explore `dependencies.py` to see how various dependencies are defined and structured.
139+
- Examine `main.py` and trace how dependencies like `get_current_user` and `get_admin_user` are used to protect different endpoints.
140+
- Run the application and use the interactive API docs at `/api/docs` to test the authenticated endpoints. Try getting a token and using it as a bearer token.
141+
- Review `tests/test_main.py` to understand how to test endpoints that have dependencies.
142+
143+
---
144+
145+
**Tip:** FastAPI’s automatic docs are available at `/api/docs` when you run your app. They will automatically include fields for authentication when you use security dependencies!
146+
147+
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
asyncio_default_fixture_loop_scope = function
3+
pythonpath = . src
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
fastapi>=0.115.0
2+
pydantic>=1.10.0
3+
pyjwt>=2.4.0
4+
uvicorn>=0.30.0
5+
python-multipart>=0.0.20
6+
pytest>=7.0.0
7+
pytest-asyncio>=0.20.0
8+
pytest-cov>=4.0.0
9+
httpx>=0.24.0
10+
flake8>=5.0.0
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""
2+
Dependencies for FastAPI application
3+
4+
How it works:
5+
*Declare dependencies*
6+
- Define regular functions, these functions can take parameters, including other dependencies
7+
8+
*Inject dependencies*
9+
- Inject dependencies into routes using the `Depends` function (decorated with `@` like @app.get, @app.post)
10+
11+
Benefits:
12+
- Improved Code Organization: Separates concerns by allowing you to extract shared logic into reusable dependency functions.
13+
- Enhanced Testability: Dependencies can be easily mocked or replaced during testing.
14+
- Reusability: Dependencies can be used across multiple routes and endpoints.
15+
- Reduced duplication of code: Dependencies can be used across multiple routes and endpoints.
16+
- Better maintainability: Centralizes dependency creation and management, simplifying updates and changes.
17+
- Automatic Handling: Dependencies can be automatically handled by FastAPI, such as dependency injection and error handling.
18+
19+
"""
20+
from fastapi import Depends, HTTPException, status, Header, Query
21+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
22+
from typing import Optional, List
23+
import jwt
24+
from datetime import datetime, timedelta
25+
import hashlib
26+
27+
# Security
28+
security = HTTPBearer()
29+
30+
# Mock user database
31+
users_db = {
32+
"admin": {
33+
"id": 1,
34+
"username": "admin",
35+
"email": "admin@example.com",
36+
"hashed_password": hashlib.sha256("admin123".encode()).hexdigest(),
37+
"roles": ["admin", "user"],
38+
"is_active": True
39+
},
40+
"user": {
41+
"id": 2,
42+
"username": "jay",
43+
"email": "jay@example.com",
44+
"hashed_password": hashlib.sha256("user123".encode()).hexdigest(),
45+
"roles": ["user"],
46+
"is_active": True
47+
}
48+
}
49+
50+
SECRET_KEY = "python-secret-key" # In real application, this should be a secret key stored securely
51+
ALGORITHM = "HS256"
52+
53+
class User:
54+
def __init__(self, id: int, username: str, email: str, roles: List[str], is_active: bool):
55+
self.id = id
56+
self.username = username
57+
self.email = email
58+
self.roles = roles
59+
self.is_active = is_active
60+
61+
# Database dependency
62+
class DatabaseConnection:
63+
def __init__(self):
64+
self.connected = True # Mocked for demonstration purposes, this should be replaced with actual database connection logic
65+
self.connection_id = f"conn_{datetime.now().timestamp()}"
66+
67+
def close(self):
68+
self.connected = False
69+
70+
def get_database():
71+
db = DatabaseConnection()
72+
try:
73+
yield db
74+
finally:
75+
db.close()
76+
77+
# Authentication dependencies
78+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
79+
to_encode = data.copy()
80+
if expires_delta:
81+
expire = datetime.now() + expires_delta
82+
else:
83+
expire = datetime.now() + timedelta(minutes=15)
84+
to_encode.update({"exp": expire})
85+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
86+
return encoded_jwt
87+
88+
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
89+
try:
90+
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
91+
username: str = payload.get("sub")
92+
print(f"Token decoded: username={username}, payload={payload}") # Debug output
93+
if username is None:
94+
raise HTTPException(
95+
status_code=status.HTTP_401_UNAUTHORIZED,
96+
detail="Could not validate credentials",
97+
headers={"WWW-Authenticate": "Bearer"},
98+
)
99+
return username
100+
except jwt.PyJWTError as e:
101+
print(f"Token validation failed: {str(e)}") # Debug output
102+
raise HTTPException(
103+
status_code=status.HTTP_401_UNAUTHORIZED,
104+
detail="Could not validate credentials",
105+
headers={"WWW-Authenticate": "Bearer"},
106+
)
107+
108+
"""
109+
In this get_current_user, verify_token is the dependency that verifies the token and returns the username.
110+
"""
111+
def get_current_user(username: str = Depends(verify_token)) -> User:
112+
user_data = users_db.get(username)
113+
if user_data is None:
114+
raise HTTPException(
115+
status_code=status.HTTP_401_UNAUTHORIZED,
116+
detail="User not found"
117+
)
118+
if not user_data["is_active"]:
119+
raise HTTPException(
120+
status_code=status.HTTP_400_BAD_REQUEST,
121+
detail="Inactive user"
122+
)
123+
user_data_without_password = {
124+
"id": user_data["id"],
125+
"username": user_data["username"],
126+
"email": user_data["email"],
127+
"roles": user_data["roles"],
128+
"is_active": user_data["is_active"]
129+
}
130+
return User(**user_data_without_password)
131+
132+
def get_admin_user(current_user: User = Depends(get_current_user)) -> User:
133+
if "admin" not in current_user.roles:
134+
raise HTTPException(
135+
status_code=status.HTTP_403_FORBIDDEN,
136+
detail="Not enough permissions"
137+
)
138+
return current_user
139+
140+
# Pagination dependency
141+
class PaginationParams:
142+
def __init__(
143+
self,
144+
skip: int = Query(0, ge=0, description="Number of records to skip"),
145+
limit: int = Query(10, ge=1, le=100, description="Number of records to return")
146+
):
147+
self.skip = skip
148+
self.limit = limit
149+
150+
# Sorting dependency
151+
class SortingParams:
152+
def __init__(
153+
self,
154+
sort_by: str = Query("id", description="Field to sort by"),
155+
sort_order: str = Query("asc", patterns="^(asc|desc)$", description="Sort order")
156+
):
157+
self.sort_by = sort_by
158+
self.sort_order = sort_order
159+
160+
# Rate limiting dependency
161+
class RateLimiter:
162+
def __init__(self):
163+
self.requests = {}
164+
165+
def is_allowed(self, client_ip: str, limit: int = 100, window: int = 3600) -> bool:
166+
now = datetime.now()
167+
if client_ip not in self.requests:
168+
self.requests[client_ip] = []
169+
170+
# Clean old requests
171+
self.requests[client_ip] = [
172+
req_time for req_time in self.requests[client_ip]
173+
if (now - req_time).seconds < window
174+
]
175+
176+
if len(self.requests[client_ip]) >= limit:
177+
return False
178+
179+
self.requests[client_ip].append(now)
180+
return True
181+
182+
rate_limiter = RateLimiter()
183+
184+
def check_rate_limit(
185+
x_forwarded_for: Optional[str] = Header(None),
186+
x_real_ip: Optional[str] = Header(None)
187+
):
188+
client_ip = x_forwarded_for or x_real_ip or "127.0.0.1"
189+
if not rate_limiter.is_allowed(client_ip):
190+
raise HTTPException(
191+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
192+
detail="Rate limit exceeded"
193+
)
194+
return client_ip
195+
196+
# Validation dependencies
197+
def validate_positive_int(value: int) -> int:
198+
if value <= 0:
199+
raise HTTPException(
200+
status_code=status.HTTP_400_BAD_REQUEST,
201+
detail="Value must be positive"
202+
)
203+
return value
204+
205+
def get_item_id(item_id: int) -> int:
206+
return validate_positive_int(item_id)
207+
208+
# Common query parameters
209+
class CommonQueryParams:
210+
def __init__(
211+
self,
212+
q: Optional[str] = Query(None, min_length=1, max_length=50, description="Search query"),
213+
include_inactive: bool = Query(False, description="Include inactive items")
214+
):
215+
self.q = q
216+
self.include_inactive = include_inactive

workspace/7_framework/fastapi/day06/src/enums/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)