@malamute/ai-rules 1.0.0 → 1.3.0
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.
- package/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI Background Tasks
|
|
7
|
+
|
|
8
|
+
## Simple Background Tasks
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from fastapi import BackgroundTasks
|
|
12
|
+
|
|
13
|
+
def write_log(message: str):
|
|
14
|
+
with open("log.txt", "a") as f:
|
|
15
|
+
f.write(f"{datetime.now()}: {message}\n")
|
|
16
|
+
|
|
17
|
+
@router.post("/items")
|
|
18
|
+
async def create_item(
|
|
19
|
+
item: ItemCreate,
|
|
20
|
+
background_tasks: BackgroundTasks,
|
|
21
|
+
) -> ItemResponse:
|
|
22
|
+
# Process item
|
|
23
|
+
created_item = await item_service.create(item)
|
|
24
|
+
|
|
25
|
+
# Queue background task
|
|
26
|
+
background_tasks.add_task(write_log, f"Item created: {created_item.id}")
|
|
27
|
+
|
|
28
|
+
# Return immediately
|
|
29
|
+
return created_item
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Async Background Tasks
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
async def send_notification(user_id: int, message: str):
|
|
36
|
+
async with httpx.AsyncClient() as client:
|
|
37
|
+
await client.post(
|
|
38
|
+
"https://notification-service/send",
|
|
39
|
+
json={"user_id": user_id, "message": message},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@router.post("/orders")
|
|
43
|
+
async def create_order(
|
|
44
|
+
order: OrderCreate,
|
|
45
|
+
background_tasks: BackgroundTasks,
|
|
46
|
+
db: DbSession,
|
|
47
|
+
) -> OrderResponse:
|
|
48
|
+
created_order = await order_service.create(db, order)
|
|
49
|
+
|
|
50
|
+
# Async task
|
|
51
|
+
background_tasks.add_task(
|
|
52
|
+
send_notification,
|
|
53
|
+
order.user_id,
|
|
54
|
+
f"Order {created_order.id} confirmed",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return created_order
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Multiple Background Tasks
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
@router.post("/signup")
|
|
64
|
+
async def signup(
|
|
65
|
+
user: UserCreate,
|
|
66
|
+
background_tasks: BackgroundTasks,
|
|
67
|
+
) -> UserResponse:
|
|
68
|
+
created_user = await user_service.create(user)
|
|
69
|
+
|
|
70
|
+
# Queue multiple tasks
|
|
71
|
+
background_tasks.add_task(send_welcome_email, created_user.email)
|
|
72
|
+
background_tasks.add_task(notify_admin, created_user.id)
|
|
73
|
+
background_tasks.add_task(update_analytics, "new_signup")
|
|
74
|
+
|
|
75
|
+
return created_user
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Background Tasks in Dependencies
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
async def log_request(
|
|
82
|
+
request: Request,
|
|
83
|
+
background_tasks: BackgroundTasks,
|
|
84
|
+
):
|
|
85
|
+
background_tasks.add_task(
|
|
86
|
+
log_to_database,
|
|
87
|
+
path=request.url.path,
|
|
88
|
+
method=request.method,
|
|
89
|
+
timestamp=datetime.now(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@router.get("/data", dependencies=[Depends(log_request)])
|
|
93
|
+
async def get_data() -> dict:
|
|
94
|
+
return {"data": "value"}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Long-Running Tasks with Celery
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# tasks.py
|
|
101
|
+
from celery import Celery
|
|
102
|
+
|
|
103
|
+
celery_app = Celery(
|
|
104
|
+
"tasks",
|
|
105
|
+
broker="redis://localhost:6379/0",
|
|
106
|
+
backend="redis://localhost:6379/0",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@celery_app.task
|
|
110
|
+
def process_large_file(file_path: str) -> dict:
|
|
111
|
+
# Long-running process
|
|
112
|
+
result = heavy_computation(file_path)
|
|
113
|
+
return {"status": "completed", "result": result}
|
|
114
|
+
|
|
115
|
+
# router.py
|
|
116
|
+
from tasks import process_large_file
|
|
117
|
+
|
|
118
|
+
@router.post("/process")
|
|
119
|
+
async def process_file(file: UploadFile) -> dict:
|
|
120
|
+
# Save file
|
|
121
|
+
file_path = f"/tmp/{file.filename}"
|
|
122
|
+
async with aiofiles.open(file_path, "wb") as f:
|
|
123
|
+
await f.write(await file.read())
|
|
124
|
+
|
|
125
|
+
# Queue Celery task
|
|
126
|
+
task = process_large_file.delay(file_path)
|
|
127
|
+
|
|
128
|
+
return {"task_id": task.id, "status": "processing"}
|
|
129
|
+
|
|
130
|
+
@router.get("/process/{task_id}")
|
|
131
|
+
async def get_task_status(task_id: str) -> dict:
|
|
132
|
+
task = process_large_file.AsyncResult(task_id)
|
|
133
|
+
|
|
134
|
+
if task.ready():
|
|
135
|
+
return {"status": "completed", "result": task.result}
|
|
136
|
+
|
|
137
|
+
return {"status": task.status}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## ARQ (Async Redis Queue)
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
# tasks.py
|
|
144
|
+
from arq import create_pool
|
|
145
|
+
from arq.connections import RedisSettings
|
|
146
|
+
|
|
147
|
+
async def send_email_task(ctx, email: str, subject: str, body: str):
|
|
148
|
+
await email_service.send(email, subject, body)
|
|
149
|
+
|
|
150
|
+
class WorkerSettings:
|
|
151
|
+
functions = [send_email_task]
|
|
152
|
+
redis_settings = RedisSettings()
|
|
153
|
+
|
|
154
|
+
# router.py
|
|
155
|
+
from arq import create_pool
|
|
156
|
+
|
|
157
|
+
@router.post("/send-email")
|
|
158
|
+
async def send_email(email: EmailSchema) -> dict:
|
|
159
|
+
redis = await create_pool(RedisSettings())
|
|
160
|
+
|
|
161
|
+
job = await redis.enqueue_job(
|
|
162
|
+
"send_email_task",
|
|
163
|
+
email.to,
|
|
164
|
+
email.subject,
|
|
165
|
+
email.body,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return {"job_id": job.job_id, "status": "queued"}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Periodic Tasks
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
# Using APScheduler
|
|
175
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
176
|
+
|
|
177
|
+
scheduler = AsyncIOScheduler()
|
|
178
|
+
|
|
179
|
+
async def cleanup_expired_sessions():
|
|
180
|
+
async with async_session() as db:
|
|
181
|
+
await db.execute(
|
|
182
|
+
delete(Session).where(Session.expires_at < datetime.utcnow())
|
|
183
|
+
)
|
|
184
|
+
await db.commit()
|
|
185
|
+
|
|
186
|
+
@asynccontextmanager
|
|
187
|
+
async def lifespan(app: FastAPI):
|
|
188
|
+
# Start scheduler
|
|
189
|
+
scheduler.add_job(
|
|
190
|
+
cleanup_expired_sessions,
|
|
191
|
+
"interval",
|
|
192
|
+
hours=1,
|
|
193
|
+
)
|
|
194
|
+
scheduler.start()
|
|
195
|
+
yield
|
|
196
|
+
# Shutdown
|
|
197
|
+
scheduler.shutdown()
|
|
198
|
+
|
|
199
|
+
app = FastAPI(lifespan=lifespan)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Task Status Tracking
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from enum import Enum
|
|
206
|
+
import uuid
|
|
207
|
+
|
|
208
|
+
class TaskStatus(str, Enum):
|
|
209
|
+
PENDING = "pending"
|
|
210
|
+
RUNNING = "running"
|
|
211
|
+
COMPLETED = "completed"
|
|
212
|
+
FAILED = "failed"
|
|
213
|
+
|
|
214
|
+
# In-memory store (use Redis in production)
|
|
215
|
+
tasks_store: dict[str, dict] = {}
|
|
216
|
+
|
|
217
|
+
async def process_with_tracking(task_id: str, data: dict):
|
|
218
|
+
tasks_store[task_id]["status"] = TaskStatus.RUNNING
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
result = await heavy_process(data)
|
|
222
|
+
tasks_store[task_id].update({
|
|
223
|
+
"status": TaskStatus.COMPLETED,
|
|
224
|
+
"result": result,
|
|
225
|
+
})
|
|
226
|
+
except Exception as e:
|
|
227
|
+
tasks_store[task_id].update({
|
|
228
|
+
"status": TaskStatus.FAILED,
|
|
229
|
+
"error": str(e),
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
@router.post("/process")
|
|
233
|
+
async def start_processing(
|
|
234
|
+
data: ProcessData,
|
|
235
|
+
background_tasks: BackgroundTasks,
|
|
236
|
+
) -> dict:
|
|
237
|
+
task_id = str(uuid.uuid4())
|
|
238
|
+
|
|
239
|
+
tasks_store[task_id] = {
|
|
240
|
+
"status": TaskStatus.PENDING,
|
|
241
|
+
"created_at": datetime.utcnow(),
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
background_tasks.add_task(process_with_tracking, task_id, data.dict())
|
|
245
|
+
|
|
246
|
+
return {"task_id": task_id}
|
|
247
|
+
|
|
248
|
+
@router.get("/process/{task_id}")
|
|
249
|
+
async def get_status(task_id: str) -> dict:
|
|
250
|
+
if task_id not in tasks_store:
|
|
251
|
+
raise HTTPException(404, "Task not found")
|
|
252
|
+
|
|
253
|
+
return tasks_store[task_id]
|
|
254
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# FastAPI Dependency Injection
|
|
7
|
+
|
|
8
|
+
## Basic Dependencies
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# GOOD - reusable dependency
|
|
12
|
+
from typing import Annotated
|
|
13
|
+
from fastapi import Depends
|
|
14
|
+
|
|
15
|
+
async def get_db():
|
|
16
|
+
async with async_session() as session:
|
|
17
|
+
yield session
|
|
18
|
+
|
|
19
|
+
DbSession = Annotated[AsyncSession, Depends(get_db)]
|
|
20
|
+
|
|
21
|
+
@router.get("/users/{user_id}")
|
|
22
|
+
async def get_user(user_id: int, db: DbSession) -> User:
|
|
23
|
+
return await db.get(User, user_id)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Dependency with Parameters
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
# GOOD - parameterized dependency
|
|
30
|
+
def get_pagination(
|
|
31
|
+
page: int = Query(1, ge=1),
|
|
32
|
+
size: int = Query(20, ge=1, le=100),
|
|
33
|
+
) -> Pagination:
|
|
34
|
+
return Pagination(page=page, size=size)
|
|
35
|
+
|
|
36
|
+
Paginated = Annotated[Pagination, Depends(get_pagination)]
|
|
37
|
+
|
|
38
|
+
@router.get("/users")
|
|
39
|
+
async def list_users(pagination: Paginated) -> Page[User]:
|
|
40
|
+
...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Class-Based Dependencies
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
# GOOD - stateful dependency
|
|
47
|
+
class RateLimiter:
|
|
48
|
+
def __init__(self, requests_per_minute: int):
|
|
49
|
+
self.rpm = requests_per_minute
|
|
50
|
+
self.requests: dict[str, list[float]] = {}
|
|
51
|
+
|
|
52
|
+
async def __call__(self, request: Request) -> None:
|
|
53
|
+
client_ip = request.client.host
|
|
54
|
+
now = time.time()
|
|
55
|
+
|
|
56
|
+
# Clean old requests
|
|
57
|
+
self.requests[client_ip] = [
|
|
58
|
+
t for t in self.requests.get(client_ip, [])
|
|
59
|
+
if now - t < 60
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
if len(self.requests[client_ip]) >= self.rpm:
|
|
63
|
+
raise HTTPException(429, "Rate limit exceeded")
|
|
64
|
+
|
|
65
|
+
self.requests[client_ip].append(now)
|
|
66
|
+
|
|
67
|
+
rate_limiter = RateLimiter(requests_per_minute=60)
|
|
68
|
+
|
|
69
|
+
@router.get("/api/data", dependencies=[Depends(rate_limiter)])
|
|
70
|
+
async def get_data():
|
|
71
|
+
...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Authentication Dependencies
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
# GOOD - reusable auth dependency
|
|
78
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
|
|
79
|
+
|
|
80
|
+
async def get_current_user(
|
|
81
|
+
token: Annotated[str, Depends(oauth2_scheme)],
|
|
82
|
+
db: DbSession,
|
|
83
|
+
) -> User:
|
|
84
|
+
payload = verify_token(token)
|
|
85
|
+
if not payload:
|
|
86
|
+
raise HTTPException(401, "Invalid token")
|
|
87
|
+
|
|
88
|
+
user = await db.get(User, payload["sub"])
|
|
89
|
+
if not user:
|
|
90
|
+
raise HTTPException(401, "User not found")
|
|
91
|
+
|
|
92
|
+
return user
|
|
93
|
+
|
|
94
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
95
|
+
|
|
96
|
+
# Role-based dependency
|
|
97
|
+
def require_role(role: str):
|
|
98
|
+
async def check_role(user: CurrentUser) -> User:
|
|
99
|
+
if user.role != role:
|
|
100
|
+
raise HTTPException(403, "Insufficient permissions")
|
|
101
|
+
return user
|
|
102
|
+
return check_role
|
|
103
|
+
|
|
104
|
+
AdminUser = Annotated[User, Depends(require_role("admin"))]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Dependency Chaining
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Dependencies can depend on other dependencies
|
|
111
|
+
async def get_user_service(db: DbSession) -> UserService:
|
|
112
|
+
return UserService(db)
|
|
113
|
+
|
|
114
|
+
async def get_current_user_with_service(
|
|
115
|
+
token: Annotated[str, Depends(oauth2_scheme)],
|
|
116
|
+
service: Annotated[UserService, Depends(get_user_service)],
|
|
117
|
+
) -> User:
|
|
118
|
+
return await service.get_by_token(token)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Global Dependencies
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Apply to all routes in router
|
|
125
|
+
router = APIRouter(
|
|
126
|
+
prefix="/admin",
|
|
127
|
+
dependencies=[Depends(require_role("admin"))],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Apply to entire app
|
|
131
|
+
app = FastAPI(dependencies=[Depends(verify_api_key)])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Yield Dependencies (Context Managers)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# GOOD - cleanup after request
|
|
138
|
+
async def get_db_transaction():
|
|
139
|
+
async with async_session() as session:
|
|
140
|
+
async with session.begin():
|
|
141
|
+
yield session
|
|
142
|
+
# Commit happens automatically if no exception
|
|
143
|
+
# Rollback happens automatically on exception
|
|
144
|
+
|
|
145
|
+
# GOOD - resource cleanup
|
|
146
|
+
async def get_temp_file():
|
|
147
|
+
path = Path(tempfile.mktemp())
|
|
148
|
+
try:
|
|
149
|
+
yield path
|
|
150
|
+
finally:
|
|
151
|
+
path.unlink(missing_ok=True)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Testing Dependencies
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# Override dependencies in tests
|
|
158
|
+
from fastapi.testclient import TestClient
|
|
159
|
+
|
|
160
|
+
def get_test_db():
|
|
161
|
+
return TestDatabase()
|
|
162
|
+
|
|
163
|
+
app.dependency_overrides[get_db] = get_test_db
|
|
164
|
+
|
|
165
|
+
with TestClient(app) as client:
|
|
166
|
+
response = client.get("/users")
|
|
167
|
+
|
|
168
|
+
# Clean up
|
|
169
|
+
app.dependency_overrides.clear()
|
|
170
|
+
```
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
paths:
|
|
3
|
-
- "
|
|
3
|
+
- "**/routers/**/*.py"
|
|
4
|
+
- "**/routes/**/*.py"
|
|
5
|
+
- "**/api/**/*.py"
|
|
6
|
+
- "**/endpoints/**/*.py"
|
|
7
|
+
- "**/main.py"
|
|
8
|
+
- "**/app.py"
|
|
4
9
|
---
|
|
5
10
|
|
|
6
11
|
# FastAPI Rules
|
|
@@ -270,3 +275,58 @@ app = FastAPI(
|
|
|
270
275
|
if settings.environment == "production":
|
|
271
276
|
app = FastAPI(docs_url=None, redoc_url=None)
|
|
272
277
|
```
|
|
278
|
+
|
|
279
|
+
## Type Hints
|
|
280
|
+
|
|
281
|
+
Always use modern syntax (Python 3.10+):
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
# Modern type hints
|
|
285
|
+
async def get_user(user_id: int) -> User | None:
|
|
286
|
+
...
|
|
287
|
+
|
|
288
|
+
def process_items(items: list[str]) -> dict[str, int]:
|
|
289
|
+
...
|
|
290
|
+
|
|
291
|
+
# Annotated for dependency injection
|
|
292
|
+
DbSession = Annotated[AsyncSession, Depends(get_db)]
|
|
293
|
+
CurrentUser = Annotated[User, Depends(get_current_user)]
|
|
294
|
+
|
|
295
|
+
@router.get("/{user_id}")
|
|
296
|
+
async def get_user(
|
|
297
|
+
user_id: int,
|
|
298
|
+
db: DbSession,
|
|
299
|
+
current_user: CurrentUser,
|
|
300
|
+
) -> UserResponse:
|
|
301
|
+
...
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Custom Exception Handling
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
# Custom exceptions
|
|
308
|
+
class NotFoundError(Exception):
|
|
309
|
+
def __init__(self, resource: str, id: int):
|
|
310
|
+
self.resource = resource
|
|
311
|
+
self.id = id
|
|
312
|
+
|
|
313
|
+
class BusinessError(Exception):
|
|
314
|
+
def __init__(self, message: str, code: str):
|
|
315
|
+
self.message = message
|
|
316
|
+
self.code = code
|
|
317
|
+
|
|
318
|
+
# FastAPI exception handlers
|
|
319
|
+
@app.exception_handler(NotFoundError)
|
|
320
|
+
async def not_found_handler(request: Request, exc: NotFoundError):
|
|
321
|
+
return JSONResponse(
|
|
322
|
+
status_code=404,
|
|
323
|
+
content={"detail": f"{exc.resource} {exc.id} not found"}
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
@app.exception_handler(BusinessError)
|
|
327
|
+
async def business_error_handler(request: Request, exc: BusinessError):
|
|
328
|
+
return JSONResponse(
|
|
329
|
+
status_code=422,
|
|
330
|
+
content={"detail": exc.message, "code": exc.code}
|
|
331
|
+
)
|
|
332
|
+
```
|