red64-cli 0.1.0 → 0.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 +1 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +5 -0
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/components/init/CompleteStep.d.ts.map +1 -1
- package/dist/components/init/CompleteStep.js +2 -2
- package/dist/components/init/CompleteStep.js.map +1 -1
- package/dist/components/init/TestCheckStep.d.ts +16 -0
- package/dist/components/init/TestCheckStep.d.ts.map +1 -0
- package/dist/components/init/TestCheckStep.js +120 -0
- package/dist/components/init/TestCheckStep.js.map +1 -0
- package/dist/components/init/index.d.ts +1 -0
- package/dist/components/init/index.d.ts.map +1 -1
- package/dist/components/init/index.js +1 -0
- package/dist/components/init/index.js.map +1 -1
- package/dist/components/init/types.d.ts +9 -0
- package/dist/components/init/types.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.d.ts.map +1 -1
- package/dist/components/screens/InitScreen.js +69 -6
- package/dist/components/screens/InitScreen.js.map +1 -1
- package/dist/components/screens/ListScreen.d.ts.map +1 -1
- package/dist/components/screens/ListScreen.js +28 -3
- package/dist/components/screens/ListScreen.js.map +1 -1
- package/dist/components/screens/StartScreen.d.ts.map +1 -1
- package/dist/components/screens/StartScreen.js +212 -13
- package/dist/components/screens/StartScreen.js.map +1 -1
- package/dist/components/ui/ArtifactsSidebar.d.ts +19 -0
- package/dist/components/ui/ArtifactsSidebar.d.ts.map +1 -0
- package/dist/components/ui/ArtifactsSidebar.js +51 -0
- package/dist/components/ui/ArtifactsSidebar.js.map +1 -0
- package/dist/components/ui/FeatureSidebar.d.ts.map +1 -1
- package/dist/components/ui/FeatureSidebar.js +1 -1
- package/dist/components/ui/FeatureSidebar.js.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/index.js.map +1 -1
- package/dist/services/ClaudeErrorDetector.js +3 -3
- package/dist/services/ClaudeErrorDetector.js.map +1 -1
- package/dist/services/ConfigService.d.ts +1 -0
- package/dist/services/ConfigService.d.ts.map +1 -1
- package/dist/services/ConfigService.js.map +1 -1
- package/dist/services/ProjectDetector.d.ts +28 -0
- package/dist/services/ProjectDetector.d.ts.map +1 -0
- package/dist/services/ProjectDetector.js +236 -0
- package/dist/services/ProjectDetector.js.map +1 -0
- package/dist/services/TestRunner.d.ts +46 -0
- package/dist/services/TestRunner.d.ts.map +1 -0
- package/dist/services/TestRunner.js +85 -0
- package/dist/services/TestRunner.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/types/index.d.ts +13 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/framework/.red64/settings/templates/specs/gap-analysis.md +163 -0
- package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
- package/framework/agents/claude/.claude/agents/red64/validate-gap.md +13 -7
- package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
- package/framework/agents/claude/.claude/commands/red64/validate-gap.md +4 -0
- package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
- package/framework/agents/codex/.codex/agents/red64/validate-gap.md +13 -7
- package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
- package/framework/agents/codex/.codex/commands/red64/validate-gap.md +4 -0
- package/framework/stacks/generic/feedback.md +80 -0
- package/framework/stacks/nextjs/accessibility.md +437 -0
- package/framework/stacks/nextjs/api.md +431 -0
- package/framework/stacks/nextjs/coding-style.md +282 -0
- package/framework/stacks/nextjs/commenting.md +226 -0
- package/framework/stacks/nextjs/components.md +411 -0
- package/framework/stacks/nextjs/conventions.md +333 -0
- package/framework/stacks/nextjs/css.md +310 -0
- package/framework/stacks/nextjs/error-handling.md +442 -0
- package/framework/stacks/nextjs/feedback.md +124 -0
- package/framework/stacks/nextjs/migrations.md +332 -0
- package/framework/stacks/nextjs/models.md +362 -0
- package/framework/stacks/nextjs/queries.md +410 -0
- package/framework/stacks/nextjs/responsive.md +338 -0
- package/framework/stacks/nextjs/tech-stack.md +177 -0
- package/framework/stacks/nextjs/test-writing.md +475 -0
- package/framework/stacks/nextjs/validation.md +467 -0
- package/framework/stacks/python/api.md +468 -0
- package/framework/stacks/python/authentication.md +342 -0
- package/framework/stacks/python/code-quality.md +283 -0
- package/framework/stacks/python/code-refactoring.md +315 -0
- package/framework/stacks/python/coding-style.md +462 -0
- package/framework/stacks/python/conventions.md +399 -0
- package/framework/stacks/python/error-handling.md +512 -0
- package/framework/stacks/python/feedback.md +92 -0
- package/framework/stacks/python/implement-ai-llm.md +468 -0
- package/framework/stacks/python/migrations.md +388 -0
- package/framework/stacks/python/models.md +399 -0
- package/framework/stacks/python/python.md +232 -0
- package/framework/stacks/python/queries.md +451 -0
- package/framework/stacks/python/structure.md +245 -58
- package/framework/stacks/python/tech.md +92 -35
- package/framework/stacks/python/testing.md +380 -0
- package/framework/stacks/python/validation.md +471 -0
- package/framework/stacks/rails/authentication.md +176 -0
- package/framework/stacks/rails/code-quality.md +287 -0
- package/framework/stacks/rails/code-refactoring.md +299 -0
- package/framework/stacks/rails/feedback.md +130 -0
- package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
- package/framework/stacks/rails/rails.md +301 -0
- package/framework/stacks/rails/rails8-best-practices.md +498 -0
- package/framework/stacks/rails/rails8-css.md +573 -0
- package/framework/stacks/rails/structure.md +140 -0
- package/framework/stacks/rails/tech.md +108 -0
- package/framework/stacks/react/code-quality.md +521 -0
- package/framework/stacks/react/components.md +625 -0
- package/framework/stacks/react/data-fetching.md +586 -0
- package/framework/stacks/react/feedback.md +110 -0
- package/framework/stacks/react/forms.md +694 -0
- package/framework/stacks/react/performance.md +640 -0
- package/framework/stacks/react/product.md +22 -9
- package/framework/stacks/react/state-management.md +472 -0
- package/framework/stacks/react/structure.md +351 -44
- package/framework/stacks/react/tech.md +219 -30
- package/framework/stacks/react/testing.md +690 -0
- package/package.json +1 -1
- package/framework/stacks/node/product.md +0 -27
- package/framework/stacks/node/structure.md +0 -82
- package/framework/stacks/node/tech.md +0 -63
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Authentication Patterns
|
|
2
|
+
|
|
3
|
+
Modern Python authentication using JWT, OAuth2, and session-based approaches.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
- **Stateless API auth**: JWT tokens for API consumers
|
|
10
|
+
- **OAuth2 flows**: Standards-based third-party auth
|
|
11
|
+
- **Secure defaults**: bcrypt hashing, httpOnly cookies, short-lived tokens
|
|
12
|
+
- **Separation of concerns**: Auth middleware, not scattered checks
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## JWT Authentication (API)
|
|
17
|
+
|
|
18
|
+
### Token Generation
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
# app/utils/auth.py
|
|
22
|
+
from datetime import datetime, timedelta, timezone
|
|
23
|
+
from jose import JWTError, jwt
|
|
24
|
+
from app.config import settings
|
|
25
|
+
|
|
26
|
+
ALGORITHM = "HS256"
|
|
27
|
+
|
|
28
|
+
def create_access_token(
|
|
29
|
+
data: dict,
|
|
30
|
+
expires_delta: timedelta | None = None,
|
|
31
|
+
) -> str:
|
|
32
|
+
to_encode = data.copy()
|
|
33
|
+
expire = datetime.now(timezone.utc) + (
|
|
34
|
+
expires_delta or timedelta(minutes=settings.access_token_expire_minutes)
|
|
35
|
+
)
|
|
36
|
+
to_encode["exp"] = expire
|
|
37
|
+
return jwt.encode(to_encode, settings.secret_key, algorithm=ALGORITHM)
|
|
38
|
+
|
|
39
|
+
def decode_access_token(token: str) -> dict:
|
|
40
|
+
"""Decode and validate JWT token.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
JWTError: If token is invalid or expired.
|
|
44
|
+
"""
|
|
45
|
+
return jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Refresh Token Pattern
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
def create_token_pair(user_id: int) -> dict:
|
|
52
|
+
access = create_access_token(
|
|
53
|
+
{"sub": str(user_id), "type": "access"},
|
|
54
|
+
expires_delta=timedelta(minutes=15),
|
|
55
|
+
)
|
|
56
|
+
refresh = create_access_token(
|
|
57
|
+
{"sub": str(user_id), "type": "refresh"},
|
|
58
|
+
expires_delta=timedelta(days=7),
|
|
59
|
+
)
|
|
60
|
+
return {"access_token": access, "refresh_token": refresh, "token_type": "bearer"}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### FastAPI Dependency
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fastapi import Depends, HTTPException, status
|
|
67
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
68
|
+
|
|
69
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
|
70
|
+
|
|
71
|
+
async def get_current_user(
|
|
72
|
+
token: str = Depends(oauth2_scheme),
|
|
73
|
+
db: AsyncSession = Depends(get_db),
|
|
74
|
+
) -> User:
|
|
75
|
+
try:
|
|
76
|
+
payload = decode_access_token(token)
|
|
77
|
+
user_id = int(payload["sub"])
|
|
78
|
+
except (JWTError, KeyError, ValueError):
|
|
79
|
+
raise HTTPException(
|
|
80
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
81
|
+
detail="Invalid or expired token",
|
|
82
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
user = await db.get(User, user_id)
|
|
86
|
+
if user is None or not user.is_active:
|
|
87
|
+
raise HTTPException(status_code=401, detail="User not found or inactive")
|
|
88
|
+
return user
|
|
89
|
+
|
|
90
|
+
# Usage in route
|
|
91
|
+
@router.get("/me")
|
|
92
|
+
async def get_profile(current_user: User = Depends(get_current_user)):
|
|
93
|
+
return UserResponse.model_validate(current_user)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Password Management
|
|
99
|
+
|
|
100
|
+
### Hashing with passlib + bcrypt
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# app/utils/hashing.py
|
|
104
|
+
from passlib.context import CryptContext
|
|
105
|
+
|
|
106
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
107
|
+
|
|
108
|
+
def hash_password(password: str) -> str:
|
|
109
|
+
return pwd_context.hash(password)
|
|
110
|
+
|
|
111
|
+
def verify_password(plain: str, hashed: str) -> bool:
|
|
112
|
+
return pwd_context.verify(plain, hashed)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Login Endpoint
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@router.post("/auth/login")
|
|
119
|
+
async def login(
|
|
120
|
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
121
|
+
db: AsyncSession = Depends(get_db),
|
|
122
|
+
):
|
|
123
|
+
user = await user_repo.get_by_email(db, form_data.username)
|
|
124
|
+
if not user or not verify_password(form_data.password, user.hashed_password):
|
|
125
|
+
raise HTTPException(
|
|
126
|
+
status_code=401,
|
|
127
|
+
detail="Incorrect email or password",
|
|
128
|
+
)
|
|
129
|
+
return create_token_pair(user.id)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## OAuth2 with authlib
|
|
135
|
+
|
|
136
|
+
### Provider Setup
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# app/auth/oauth.py
|
|
140
|
+
from authlib.integrations.starlette_client import OAuth
|
|
141
|
+
|
|
142
|
+
oauth = OAuth()
|
|
143
|
+
|
|
144
|
+
oauth.register(
|
|
145
|
+
name="google",
|
|
146
|
+
client_id=settings.google_client_id,
|
|
147
|
+
client_secret=settings.google_client_secret,
|
|
148
|
+
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
|
149
|
+
client_kwargs={"scope": "openid email profile"},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
oauth.register(
|
|
153
|
+
name="github",
|
|
154
|
+
client_id=settings.github_client_id,
|
|
155
|
+
client_secret=settings.github_client_secret,
|
|
156
|
+
authorize_url="https://github.com/login/oauth/authorize",
|
|
157
|
+
access_token_url="https://github.com/login/oauth/access_token",
|
|
158
|
+
api_base_url="https://api.github.com/",
|
|
159
|
+
client_kwargs={"scope": "user:email"},
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### OAuth Flow Endpoints
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from starlette.requests import Request
|
|
167
|
+
|
|
168
|
+
@router.get("/auth/{provider}/login")
|
|
169
|
+
async def oauth_login(provider: str, request: Request):
|
|
170
|
+
client = getattr(oauth, provider, None)
|
|
171
|
+
if not client:
|
|
172
|
+
raise HTTPException(404, f"Unknown provider: {provider}")
|
|
173
|
+
redirect_uri = request.url_for("oauth_callback", provider=provider)
|
|
174
|
+
return await client.authorize_redirect(request, redirect_uri)
|
|
175
|
+
|
|
176
|
+
@router.get("/auth/{provider}/callback")
|
|
177
|
+
async def oauth_callback(
|
|
178
|
+
provider: str,
|
|
179
|
+
request: Request,
|
|
180
|
+
db: AsyncSession = Depends(get_db),
|
|
181
|
+
):
|
|
182
|
+
client = getattr(oauth, provider)
|
|
183
|
+
token = await client.authorize_access_token(request)
|
|
184
|
+
user_info = token.get("userinfo") or await client.userinfo(token=token)
|
|
185
|
+
|
|
186
|
+
# Find or create user
|
|
187
|
+
user = await find_or_create_oauth_user(
|
|
188
|
+
db=db,
|
|
189
|
+
provider=provider,
|
|
190
|
+
provider_id=str(user_info["sub"]),
|
|
191
|
+
email=user_info["email"],
|
|
192
|
+
name=user_info.get("name", ""),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return create_token_pair(user.id)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Session-Based Authentication
|
|
201
|
+
|
|
202
|
+
### Cookie Sessions (for web apps)
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
206
|
+
|
|
207
|
+
app.add_middleware(
|
|
208
|
+
SessionMiddleware,
|
|
209
|
+
secret_key=settings.secret_key,
|
|
210
|
+
max_age=86400, # 24 hours
|
|
211
|
+
https_only=True, # Production only
|
|
212
|
+
same_site="lax",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Login: store user in session
|
|
216
|
+
@router.post("/login")
|
|
217
|
+
async def login(request: Request, form: LoginForm = Depends()):
|
|
218
|
+
user = await authenticate_user(form.email, form.password)
|
|
219
|
+
if not user:
|
|
220
|
+
raise HTTPException(401)
|
|
221
|
+
request.session["user_id"] = user.id
|
|
222
|
+
return RedirectResponse("/dashboard", status_code=303)
|
|
223
|
+
|
|
224
|
+
# Middleware: load user from session
|
|
225
|
+
async def get_session_user(
|
|
226
|
+
request: Request,
|
|
227
|
+
db: AsyncSession = Depends(get_db),
|
|
228
|
+
) -> User | None:
|
|
229
|
+
user_id = request.session.get("user_id")
|
|
230
|
+
if not user_id:
|
|
231
|
+
return None
|
|
232
|
+
return await db.get(User, user_id)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Authorization
|
|
238
|
+
|
|
239
|
+
### Role-Based Access Control
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from enum import StrEnum
|
|
243
|
+
from functools import wraps
|
|
244
|
+
|
|
245
|
+
class Role(StrEnum):
|
|
246
|
+
ADMIN = "admin"
|
|
247
|
+
MEMBER = "member"
|
|
248
|
+
VIEWER = "viewer"
|
|
249
|
+
|
|
250
|
+
def require_role(*roles: Role):
|
|
251
|
+
"""Dependency that checks user role."""
|
|
252
|
+
async def checker(current_user: User = Depends(get_current_user)) -> User:
|
|
253
|
+
if current_user.role not in roles:
|
|
254
|
+
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
|
255
|
+
return current_user
|
|
256
|
+
return checker
|
|
257
|
+
|
|
258
|
+
# Usage
|
|
259
|
+
@router.delete("/users/{user_id}")
|
|
260
|
+
async def delete_user(
|
|
261
|
+
user_id: int,
|
|
262
|
+
admin: User = Depends(require_role(Role.ADMIN)),
|
|
263
|
+
):
|
|
264
|
+
...
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Resource Ownership
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
# Pattern: Always scope queries to current user
|
|
271
|
+
async def get_user_content(
|
|
272
|
+
content_id: int,
|
|
273
|
+
current_user: User = Depends(get_current_user),
|
|
274
|
+
db: AsyncSession = Depends(get_db),
|
|
275
|
+
) -> Content:
|
|
276
|
+
content = await db.get(Content, content_id)
|
|
277
|
+
if not content or content.user_id != current_user.id:
|
|
278
|
+
raise HTTPException(status_code=404)
|
|
279
|
+
return content
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Rate Limiting
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
# Using slowapi
|
|
288
|
+
from slowapi import Limiter
|
|
289
|
+
from slowapi.util import get_remote_address
|
|
290
|
+
|
|
291
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
292
|
+
|
|
293
|
+
@router.post("/auth/login")
|
|
294
|
+
@limiter.limit("5/minute")
|
|
295
|
+
async def login(request: Request, ...):
|
|
296
|
+
...
|
|
297
|
+
|
|
298
|
+
@router.post("/auth/reset-password")
|
|
299
|
+
@limiter.limit("3/minute")
|
|
300
|
+
async def reset_password(request: Request, ...):
|
|
301
|
+
...
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Security Checklist
|
|
307
|
+
|
|
308
|
+
- [x] Passwords hashed with bcrypt (passlib)
|
|
309
|
+
- [x] JWT tokens with short expiry (15 min access, 7 day refresh)
|
|
310
|
+
- [x] httpOnly cookies for session auth
|
|
311
|
+
- [x] Rate limiting on auth endpoints
|
|
312
|
+
- [x] CORS configured for allowed origins
|
|
313
|
+
- [ ] Account lockout after failed attempts (implement as needed)
|
|
314
|
+
- [ ] Email verification flow (implement as needed)
|
|
315
|
+
- [ ] MFA / TOTP support (implement as needed)
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Testing Auth
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
# Fixture: authenticated client
|
|
323
|
+
@pytest.fixture
|
|
324
|
+
async def auth_client(client: httpx.AsyncClient, sample_user: User):
|
|
325
|
+
token = create_access_token({"sub": str(sample_user.id)})
|
|
326
|
+
client.headers["Authorization"] = f"Bearer {token}"
|
|
327
|
+
return client
|
|
328
|
+
|
|
329
|
+
# Test protected endpoint
|
|
330
|
+
async def test_profile_requires_auth(client: httpx.AsyncClient):
|
|
331
|
+
response = await client.get("/api/v1/me")
|
|
332
|
+
assert response.status_code == 401
|
|
333
|
+
|
|
334
|
+
async def test_profile_with_auth(auth_client: httpx.AsyncClient):
|
|
335
|
+
response = await auth_client.get("/api/v1/me")
|
|
336
|
+
assert response.status_code == 200
|
|
337
|
+
assert "email" in response.json()
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
_Document patterns and extension points, not implementation details._
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Code Quality Standards
|
|
2
|
+
|
|
3
|
+
Project memory for code quality conventions, linting, type checking, and testing standards in Python.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Linting and Formatting
|
|
8
|
+
|
|
9
|
+
### Ruff (Single Tool for Everything)
|
|
10
|
+
|
|
11
|
+
Ruff replaces black, isort, flake8, pyupgrade, and more. Configuration lives in `pyproject.toml`:
|
|
12
|
+
|
|
13
|
+
```toml
|
|
14
|
+
[tool.ruff]
|
|
15
|
+
target-version = "py312"
|
|
16
|
+
src = ["src"]
|
|
17
|
+
line-length = 99
|
|
18
|
+
|
|
19
|
+
[tool.ruff.lint]
|
|
20
|
+
select = [
|
|
21
|
+
"E", # pycodestyle errors
|
|
22
|
+
"F", # pyflakes
|
|
23
|
+
"I", # isort
|
|
24
|
+
"N", # pep8-naming
|
|
25
|
+
"UP", # pyupgrade (modern syntax)
|
|
26
|
+
"B", # flake8-bugbear
|
|
27
|
+
"A", # flake8-builtins
|
|
28
|
+
"SIM", # flake8-simplify
|
|
29
|
+
"TCH", # flake8-type-checking (move imports behind TYPE_CHECKING)
|
|
30
|
+
"RUF", # ruff-specific rules
|
|
31
|
+
"ASYNC",# flake8-async
|
|
32
|
+
]
|
|
33
|
+
ignore = ["E501"] # line length handled by formatter
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint.isort]
|
|
36
|
+
known-first-party = ["app"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Run checks
|
|
41
|
+
uv run ruff check . # Lint
|
|
42
|
+
uv run ruff check . --fix # Auto-fix
|
|
43
|
+
uv run ruff format . # Format
|
|
44
|
+
uv run ruff format . --check # Verify formatting
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Type Checking
|
|
50
|
+
|
|
51
|
+
### mypy (Strict Mode)
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
python_version = "3.12"
|
|
56
|
+
strict = true
|
|
57
|
+
warn_return_any = true
|
|
58
|
+
warn_unused_configs = true
|
|
59
|
+
plugins = ["pydantic.mypy"]
|
|
60
|
+
|
|
61
|
+
[[tool.mypy.overrides]]
|
|
62
|
+
module = ["tests.*"]
|
|
63
|
+
disallow_untyped_defs = false
|
|
64
|
+
|
|
65
|
+
[[tool.mypy.overrides]]
|
|
66
|
+
module = ["celery.*", "arq.*"]
|
|
67
|
+
ignore_missing_imports = true
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Type Annotation Patterns
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# Pattern: Annotate all public function signatures
|
|
74
|
+
async def get_user(user_id: int, db: AsyncSession) -> User | None:
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
# Pattern: Use TYPE_CHECKING for import-only types
|
|
78
|
+
from __future__ import annotations
|
|
79
|
+
from typing import TYPE_CHECKING
|
|
80
|
+
|
|
81
|
+
if TYPE_CHECKING:
|
|
82
|
+
from app.models.user import User
|
|
83
|
+
|
|
84
|
+
# Pattern: TypeVar for generic functions
|
|
85
|
+
from typing import TypeVar
|
|
86
|
+
T = TypeVar("T")
|
|
87
|
+
|
|
88
|
+
async def get_or_404(model: type[T], id: int, db: AsyncSession) -> T:
|
|
89
|
+
result = await db.get(model, id)
|
|
90
|
+
if result is None:
|
|
91
|
+
raise NotFoundError(model.__name__, str(id))
|
|
92
|
+
return result
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Pre-commit Hooks
|
|
98
|
+
|
|
99
|
+
### Configuration (`.pre-commit-config.yaml`)
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
repos:
|
|
103
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
104
|
+
rev: v0.8.0
|
|
105
|
+
hooks:
|
|
106
|
+
- id: ruff
|
|
107
|
+
args: [--fix]
|
|
108
|
+
- id: ruff-format
|
|
109
|
+
|
|
110
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
111
|
+
rev: v1.13.0
|
|
112
|
+
hooks:
|
|
113
|
+
- id: mypy
|
|
114
|
+
additional_dependencies:
|
|
115
|
+
- pydantic>=2.0
|
|
116
|
+
- types-redis
|
|
117
|
+
|
|
118
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
119
|
+
rev: v5.0.0
|
|
120
|
+
hooks:
|
|
121
|
+
- id: check-yaml
|
|
122
|
+
- id: check-toml
|
|
123
|
+
- id: check-added-large-files
|
|
124
|
+
- id: no-commit-to-branch
|
|
125
|
+
args: [--branch, main]
|
|
126
|
+
- id: detect-private-key
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Setup
|
|
131
|
+
uv run pre-commit install
|
|
132
|
+
uv run pre-commit run --all-files # Verify
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Testing Framework
|
|
138
|
+
|
|
139
|
+
### pytest Configuration
|
|
140
|
+
|
|
141
|
+
```toml
|
|
142
|
+
[tool.pytest.ini_options]
|
|
143
|
+
testpaths = ["tests"]
|
|
144
|
+
asyncio_mode = "auto"
|
|
145
|
+
addopts = [
|
|
146
|
+
"-v",
|
|
147
|
+
"--strict-markers",
|
|
148
|
+
"--tb=short",
|
|
149
|
+
]
|
|
150
|
+
markers = [
|
|
151
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
152
|
+
"integration: marks integration tests",
|
|
153
|
+
]
|
|
154
|
+
filterwarnings = [
|
|
155
|
+
"error",
|
|
156
|
+
"ignore::DeprecationWarning:sqlalchemy.*",
|
|
157
|
+
]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Coverage Configuration
|
|
161
|
+
|
|
162
|
+
```toml
|
|
163
|
+
[tool.coverage.run]
|
|
164
|
+
source = ["src/app"]
|
|
165
|
+
omit = ["*/migrations/*", "*/tests/*"]
|
|
166
|
+
|
|
167
|
+
[tool.coverage.report]
|
|
168
|
+
fail_under = 80
|
|
169
|
+
show_missing = true
|
|
170
|
+
exclude_lines = [
|
|
171
|
+
"pragma: no cover",
|
|
172
|
+
"if TYPE_CHECKING:",
|
|
173
|
+
"raise NotImplementedError",
|
|
174
|
+
"@overload",
|
|
175
|
+
]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
uv run pytest --cov=src/app --cov-report=term-missing
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Test Data Generation
|
|
185
|
+
|
|
186
|
+
### Factory Boy + Faker
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# tests/factories/user_factory.py
|
|
190
|
+
import factory
|
|
191
|
+
from faker import Faker
|
|
192
|
+
from app.models.user import User
|
|
193
|
+
|
|
194
|
+
fake = Faker()
|
|
195
|
+
|
|
196
|
+
class UserFactory(factory.Factory):
|
|
197
|
+
class Meta:
|
|
198
|
+
model = User
|
|
199
|
+
|
|
200
|
+
id = factory.Sequence(lambda n: n + 1)
|
|
201
|
+
email = factory.LazyFunction(fake.email)
|
|
202
|
+
name = factory.LazyFunction(fake.name)
|
|
203
|
+
hashed_password = "hashed_test_password"
|
|
204
|
+
is_active = True
|
|
205
|
+
|
|
206
|
+
# Usage
|
|
207
|
+
user = UserFactory()
|
|
208
|
+
admin = UserFactory(name="Admin", is_active=True)
|
|
209
|
+
users = UserFactory.build_batch(5)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Security Scanning
|
|
215
|
+
|
|
216
|
+
### bandit (Static Analysis)
|
|
217
|
+
|
|
218
|
+
```toml
|
|
219
|
+
# pyproject.toml
|
|
220
|
+
[tool.bandit]
|
|
221
|
+
exclude_dirs = ["tests"]
|
|
222
|
+
skips = ["B101"] # Allow assert in non-test code if needed
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
uv run bandit -r src/ -c pyproject.toml
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Dependency Auditing
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
uv run pip-audit # Check for known vulnerabilities
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Documentation Standards
|
|
238
|
+
|
|
239
|
+
### Docstrings (Google Style)
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
class UserService:
|
|
243
|
+
"""Service for user-related business logic.
|
|
244
|
+
|
|
245
|
+
Handles user creation, authentication, and profile management.
|
|
246
|
+
Depends on UserRepo for data access.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
async def create_user(self, data: CreateUserRequest) -> User:
|
|
250
|
+
"""Create a new user account.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
data: Validated user creation request.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
The created User instance.
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
ValidationError: If email already exists.
|
|
260
|
+
"""
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Quality Commands Summary
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Full quality check (CI pipeline)
|
|
269
|
+
uv run ruff check . # Lint
|
|
270
|
+
uv run ruff format . --check # Format check
|
|
271
|
+
uv run mypy src/ # Type check
|
|
272
|
+
uv run bandit -r src/ # Security
|
|
273
|
+
uv run pytest --cov=src/app # Tests + coverage
|
|
274
|
+
|
|
275
|
+
# Development workflow
|
|
276
|
+
uv run ruff check . --fix && uv run ruff format . # Quick fix
|
|
277
|
+
uv run pytest tests/unit/ -x # Fast feedback
|
|
278
|
+
uv run mypy src/app/services/ # Focused type check
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
_Focus on patterns over exhaustive rules. Code should be typed, formatted, and tested._
|