omgkit 2.2.0 → 2.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/databases/mongodb/SKILL.md +60 -776
  3. package/plugin/skills/databases/prisma/SKILL.md +53 -744
  4. package/plugin/skills/databases/redis/SKILL.md +53 -860
  5. package/plugin/skills/devops/aws/SKILL.md +68 -672
  6. package/plugin/skills/devops/github-actions/SKILL.md +54 -657
  7. package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
  8. package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
  9. package/plugin/skills/frameworks/django/SKILL.md +87 -853
  10. package/plugin/skills/frameworks/express/SKILL.md +95 -1301
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
  12. package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
  14. package/plugin/skills/frameworks/react/SKILL.md +94 -962
  15. package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
  16. package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
  17. package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
  18. package/plugin/skills/frontend/responsive/SKILL.md +76 -799
  19. package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
  20. package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
  21. package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
  22. package/plugin/skills/languages/javascript/SKILL.md +106 -849
  23. package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
  24. package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
  25. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
  26. package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
  27. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
  28. package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
  29. package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
  30. package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
  31. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
  32. package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
  33. package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
  34. package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
  35. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
  36. package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
  37. package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
  38. package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
  39. package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
  40. package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
  41. package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
  42. package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
  43. package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
  44. package/plugin/skills/security/better-auth/SKILL.md +46 -1034
  45. package/plugin/skills/security/oauth/SKILL.md +80 -934
  46. package/plugin/skills/security/owasp/SKILL.md +78 -862
  47. package/plugin/skills/testing/playwright/SKILL.md +77 -700
  48. package/plugin/skills/testing/pytest/SKILL.md +73 -811
  49. package/plugin/skills/testing/vitest/SKILL.md +60 -920
  50. package/plugin/skills/tools/document-processing/SKILL.md +111 -838
  51. package/plugin/skills/tools/image-processing/SKILL.md +126 -659
  52. package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
  53. package/plugin/skills/tools/media-processing/SKILL.md +118 -735
  54. package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
  55. package/plugin/skills/SKILL_STANDARDS.md +0 -743
@@ -1,1285 +1,177 @@
1
1
  ---
2
- name: fastapi
3
- description: Enterprise FastAPI development with async patterns, Pydantic v2, dependency injection, and production APIs
4
- category: frameworks
5
- triggers:
6
- - fastapi
7
- - fast api
8
- - python api
9
- - pydantic
10
- - starlette
11
- - async python
12
- - python rest api
13
- - uvicorn
2
+ name: building-fastapi-apis
3
+ description: Builds high-performance FastAPI applications with async/await, Pydantic v2, dependency injection, and SQLAlchemy. Use when creating Python REST APIs, async backends, or microservices.
14
4
  ---
15
5
 
16
6
  # FastAPI
17
7
 
18
- Enterprise-grade **FastAPI development** following industry best practices. This skill covers async programming, Pydantic v2 validation, dependency injection, authentication, background tasks, testing patterns, and production deployment configurations used by top engineering teams.
19
-
20
- ## Purpose
21
-
22
- Build high-performance Python APIs with confidence:
23
-
24
- - Design async API architectures for maximum throughput
25
- - Implement comprehensive request validation with Pydantic
26
- - Use dependency injection for clean, testable code
27
- - Handle authentication and authorization securely
28
- - Write comprehensive tests with pytest
29
- - Deploy production-ready applications
30
- - Leverage automatic OpenAPI documentation
31
-
32
- ## Features
33
-
34
- ### 1. Application Setup and Configuration
8
+ ## Quick Start
35
9
 
36
10
  ```python
37
- # app/main.py
38
- from contextlib import asynccontextmanager
39
11
  from fastapi import FastAPI
40
- from fastapi.middleware.cors import CORSMiddleware
41
- from fastapi.middleware.gzip import GZipMiddleware
42
- import uvicorn
43
-
44
- from app.api.v1 import router as api_v1_router
45
- from app.core.config import settings
46
- from app.core.logging import setup_logging
47
- from app.db.session import engine, async_session_maker
48
- from app.db.base import Base
49
-
50
-
51
- @asynccontextmanager
52
- async def lifespan(app: FastAPI):
53
- """Application lifespan events."""
54
- # Startup
55
- setup_logging()
56
- async with engine.begin() as conn:
57
- await conn.run_sync(Base.metadata.create_all)
58
- yield
59
- # Shutdown
60
- await engine.dispose()
61
-
62
-
63
- def create_app() -> FastAPI:
64
- app = FastAPI(
65
- title=settings.PROJECT_NAME,
66
- description="Enterprise API",
67
- version="1.0.0",
68
- openapi_url=f"{settings.API_V1_PREFIX}/openapi.json",
69
- docs_url=f"{settings.API_V1_PREFIX}/docs",
70
- redoc_url=f"{settings.API_V1_PREFIX}/redoc",
71
- lifespan=lifespan,
72
- )
73
-
74
- # Middleware
75
- app.add_middleware(GZipMiddleware, minimum_size=1000)
76
- app.add_middleware(
77
- CORSMiddleware,
78
- allow_origins=settings.CORS_ORIGINS,
79
- allow_credentials=True,
80
- allow_methods=["*"],
81
- allow_headers=["*"],
82
- )
83
-
84
- # Routes
85
- app.include_router(api_v1_router, prefix=settings.API_V1_PREFIX)
86
-
87
- @app.get("/health")
88
- async def health_check():
89
- return {"status": "healthy"}
90
-
91
- return app
92
-
93
-
94
- app = create_app()
95
-
96
- if __name__ == "__main__":
97
- uvicorn.run(
98
- "app.main:app",
99
- host="0.0.0.0",
100
- port=8000,
101
- reload=settings.DEBUG,
102
- workers=settings.WORKERS,
103
- )
104
-
105
-
106
- # app/core/config.py
107
- from functools import lru_cache
108
- from typing import List, Optional
109
- from pydantic import Field, PostgresDsn, field_validator
110
- from pydantic_settings import BaseSettings, SettingsConfigDict
111
-
112
12
 
113
- class Settings(BaseSettings):
114
- model_config = SettingsConfigDict(
115
- env_file=".env",
116
- env_file_encoding="utf-8",
117
- case_sensitive=True,
118
- )
13
+ app = FastAPI()
119
14
 
120
- # Application
121
- PROJECT_NAME: str = "FastAPI App"
122
- DEBUG: bool = False
123
- WORKERS: int = 4
124
- API_V1_PREFIX: str = "/api/v1"
125
-
126
- # Database
127
- DATABASE_URL: PostgresDsn
128
- DATABASE_POOL_SIZE: int = 5
129
- DATABASE_MAX_OVERFLOW: int = 10
130
-
131
- # Redis
132
- REDIS_URL: str = "redis://localhost:6379/0"
133
-
134
- # Security
135
- SECRET_KEY: str
136
- ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
137
- REFRESH_TOKEN_EXPIRE_DAYS: int = 7
138
- ALGORITHM: str = "HS256"
139
-
140
- # CORS
141
- CORS_ORIGINS: List[str] = ["http://localhost:3000"]
142
-
143
- @field_validator("CORS_ORIGINS", mode="before")
144
- @classmethod
145
- def parse_cors_origins(cls, v: str | List[str]) -> List[str]:
146
- if isinstance(v, str):
147
- return [origin.strip() for origin in v.split(",")]
148
- return v
15
+ @app.get("/health")
16
+ async def health_check():
17
+ return {"status": "ok"}
149
18
 
19
+ @app.get("/users/{user_id}")
20
+ async def get_user(user_id: int):
21
+ return {"user_id": user_id}
22
+ ```
150
23
 
151
- @lru_cache
152
- def get_settings() -> Settings:
153
- return Settings()
24
+ ## Features
154
25
 
26
+ | Feature | Description | Guide |
27
+ |---------|-------------|-------|
28
+ | Routing | Path params, query params, body | [ROUTING.md](ROUTING.md) |
29
+ | Pydantic | Schemas, validation, serialization | [SCHEMAS.md](SCHEMAS.md) |
30
+ | Dependencies | Injection, database sessions | [DEPENDENCIES.md](DEPENDENCIES.md) |
31
+ | Auth | JWT, OAuth2, security utils | [AUTH.md](AUTH.md) |
32
+ | Database | SQLAlchemy async, migrations | [DATABASE.md](DATABASE.md) |
33
+ | Testing | pytest, AsyncClient | [TESTING.md](TESTING.md) |
155
34
 
156
- settings = get_settings()
157
- ```
35
+ ## Common Patterns
158
36
 
159
- ### 2. Pydantic Schemas and Validation
37
+ ### Pydantic Schemas
160
38
 
161
39
  ```python
162
- # app/schemas/user.py
163
- from datetime import datetime
164
- from typing import Optional, List
165
- from uuid import UUID
166
- from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
40
+ from pydantic import BaseModel, EmailStr, Field, field_validator
167
41
 
168
-
169
- class UserBase(BaseModel):
42
+ class UserCreate(BaseModel):
170
43
  email: EmailStr
171
44
  name: str = Field(..., min_length=2, max_length=100)
172
- is_active: bool = True
173
-
174
-
175
- class UserCreate(UserBase):
176
- password: str = Field(..., min_length=8, max_length=128)
45
+ password: str = Field(..., min_length=8)
177
46
 
178
47
  @field_validator("password")
179
48
  @classmethod
180
49
  def validate_password(cls, v: str) -> str:
181
50
  if not any(c.isupper() for c in v):
182
- raise ValueError("Password must contain uppercase letter")
51
+ raise ValueError("Must contain uppercase")
183
52
  if not any(c.isdigit() for c in v):
184
- raise ValueError("Password must contain a digit")
185
- if not any(c in "!@#$%^&*" for c in v):
186
- raise ValueError("Password must contain a special character")
53
+ raise ValueError("Must contain digit")
187
54
  return v
188
55
 
189
-
190
- class UserUpdate(BaseModel):
191
- email: Optional[EmailStr] = None
192
- name: Optional[str] = Field(None, min_length=2, max_length=100)
193
- is_active: Optional[bool] = None
194
-
195
-
196
- class UserInDB(UserBase):
197
- model_config = ConfigDict(from_attributes=True)
198
-
199
- id: UUID
200
- role: str
201
- created_at: datetime
202
- updated_at: datetime
203
-
204
-
205
- class UserResponse(UserInDB):
206
- """Response model excluding sensitive fields."""
207
- pass
208
-
209
-
210
- class UserWithOrganizations(UserResponse):
211
- organizations: List["OrganizationSummary"] = []
212
-
213
-
214
- # app/schemas/organization.py
215
- from datetime import datetime
216
- from typing import Optional, List
217
- from uuid import UUID
218
- from pydantic import BaseModel, Field, ConfigDict
219
- from enum import Enum
220
-
221
-
222
- class MemberRole(str, Enum):
223
- OWNER = "owner"
224
- ADMIN = "admin"
225
- MEMBER = "member"
226
- VIEWER = "viewer"
227
-
228
-
229
- class OrganizationCreate(BaseModel):
230
- name: str = Field(..., min_length=2, max_length=255)
231
- slug: str = Field(..., min_length=2, max_length=100, pattern=r"^[a-z0-9-]+$")
232
-
233
-
234
- class OrganizationUpdate(BaseModel):
235
- name: Optional[str] = Field(None, min_length=2, max_length=255)
236
-
237
-
238
- class OrganizationSummary(BaseModel):
56
+ class UserResponse(BaseModel):
239
57
  model_config = ConfigDict(from_attributes=True)
240
58
 
241
59
  id: UUID
60
+ email: EmailStr
242
61
  name: str
243
- slug: str
244
-
245
-
246
- class OrganizationResponse(OrganizationSummary):
247
- owner_id: UUID
248
- member_count: int = 0
249
62
  created_at: datetime
250
-
251
-
252
- class MembershipResponse(BaseModel):
253
- model_config = ConfigDict(from_attributes=True)
254
-
255
- user_id: UUID
256
- organization_id: UUID
257
- role: MemberRole
258
- joined_at: datetime
259
-
260
-
261
- # app/schemas/common.py
262
- from typing import Generic, TypeVar, List, Optional
263
- from pydantic import BaseModel, Field
264
-
265
- T = TypeVar("T")
266
-
267
-
268
- class PaginationParams(BaseModel):
269
- page: int = Field(1, ge=1)
270
- limit: int = Field(20, ge=1, le=100)
271
-
272
- @property
273
- def offset(self) -> int:
274
- return (self.page - 1) * self.limit
275
-
276
-
277
- class PaginatedResponse(BaseModel, Generic[T]):
278
- data: List[T]
279
- total: int
280
- page: int
281
- limit: int
282
- total_pages: int
283
- has_more: bool
284
-
285
- @classmethod
286
- def create(
287
- cls,
288
- data: List[T],
289
- total: int,
290
- page: int,
291
- limit: int,
292
- ) -> "PaginatedResponse[T]":
293
- total_pages = (total + limit - 1) // limit
294
- return cls(
295
- data=data,
296
- total=total,
297
- page=page,
298
- limit=limit,
299
- total_pages=total_pages,
300
- has_more=page < total_pages,
301
- )
302
63
  ```
303
64
 
304
- ### 3. Dependency Injection
65
+ ### Dependency Injection
305
66
 
306
67
  ```python
307
- # app/api/deps.py
308
- from typing import Annotated, AsyncGenerator, Optional
309
- from fastapi import Depends, HTTPException, status, Query
310
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
68
+ from fastapi import Depends
311
69
  from sqlalchemy.ext.asyncio import AsyncSession
312
- from jose import JWTError, jwt
313
- from uuid import UUID
314
-
315
- from app.core.config import settings
316
- from app.db.session import async_session_maker
317
- from app.models.user import User
318
- from app.schemas.common import PaginationParams
319
- from app.services.user import UserService
320
- from app.services.organization import OrganizationService
321
- from app.core.redis import redis_client
322
-
323
-
324
- security = HTTPBearer()
325
-
326
70
 
327
71
  async def get_db() -> AsyncGenerator[AsyncSession, None]:
328
- """Database session dependency."""
329
72
  async with async_session_maker() as session:
330
- try:
331
- yield session
332
- finally:
333
- await session.close()
334
-
335
-
336
- async def get_redis():
337
- """Redis client dependency."""
338
- return redis_client
339
-
340
-
341
- def get_pagination(
342
- page: int = Query(1, ge=1, description="Page number"),
343
- limit: int = Query(20, ge=1, le=100, description="Items per page"),
344
- ) -> PaginationParams:
345
- """Pagination parameters dependency."""
346
- return PaginationParams(page=page, limit=limit)
347
-
73
+ yield session
348
74
 
349
75
  async def get_current_user(
350
- credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
351
- db: Annotated[AsyncSession, Depends(get_db)],
76
+ token: str = Depends(oauth2_scheme),
77
+ db: AsyncSession = Depends(get_db),
352
78
  ) -> User:
353
- """Get current authenticated user."""
354
- credentials_exception = HTTPException(
355
- status_code=status.HTTP_401_UNAUTHORIZED,
356
- detail="Could not validate credentials",
357
- headers={"WWW-Authenticate": "Bearer"},
358
- )
359
-
360
- try:
361
- payload = jwt.decode(
362
- credentials.credentials,
363
- settings.SECRET_KEY,
364
- algorithms=[settings.ALGORITHM],
365
- )
366
- user_id: str = payload.get("sub")
367
- if user_id is None:
368
- raise credentials_exception
369
- except JWTError:
370
- raise credentials_exception
371
-
372
- user_service = UserService(db)
373
- user = await user_service.get_by_id(UUID(user_id))
374
-
375
- if user is None:
376
- raise credentials_exception
377
- if not user.is_active:
378
- raise HTTPException(
379
- status_code=status.HTTP_403_FORBIDDEN,
380
- detail="Inactive user",
381
- )
382
-
79
+ payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
80
+ user = await db.get(User, payload["sub"])
81
+ if not user:
82
+ raise HTTPException(status_code=401)
383
83
  return user
384
84
 
385
-
386
- async def get_current_active_user(
387
- current_user: Annotated[User, Depends(get_current_user)],
388
- ) -> User:
389
- """Get current active user."""
390
- if not current_user.is_active:
391
- raise HTTPException(status_code=400, detail="Inactive user")
392
- return current_user
393
-
394
-
395
- def require_role(*roles: str):
396
- """Role-based access control dependency factory."""
397
- async def role_checker(
398
- current_user: Annotated[User, Depends(get_current_user)],
399
- ) -> User:
400
- if current_user.role not in roles:
401
- raise HTTPException(
402
- status_code=status.HTTP_403_FORBIDDEN,
403
- detail="Insufficient permissions",
404
- )
405
- return current_user
406
- return role_checker
407
-
408
-
409
- async def get_organization_member(
410
- org_slug: str,
411
- current_user: Annotated[User, Depends(get_current_user)],
412
- db: Annotated[AsyncSession, Depends(get_db)],
413
- ):
414
- """Verify user is a member of the organization."""
415
- org_service = OrganizationService(db)
416
- organization = await org_service.get_by_slug(org_slug)
417
-
418
- if not organization:
419
- raise HTTPException(
420
- status_code=status.HTTP_404_NOT_FOUND,
421
- detail="Organization not found",
422
- )
423
-
424
- membership = await org_service.get_membership(organization.id, current_user.id)
425
- if not membership:
426
- raise HTTPException(
427
- status_code=status.HTTP_403_FORBIDDEN,
428
- detail="Not a member of this organization",
429
- )
430
-
431
- return {"organization": organization, "membership": membership}
432
-
433
-
434
85
  # Type aliases for cleaner signatures
435
86
  DB = Annotated[AsyncSession, Depends(get_db)]
436
87
  CurrentUser = Annotated[User, Depends(get_current_user)]
437
- Pagination = Annotated[PaginationParams, Depends(get_pagination)]
438
- AdminUser = Annotated[User, Depends(require_role("admin"))]
439
88
  ```
440
89
 
441
- ### 4. Routes and Endpoints
90
+ ### Route with Service Layer
442
91
 
443
92
  ```python
444
- # app/api/v1/__init__.py
445
- from fastapi import APIRouter
446
- from app.api.v1 import auth, users, organizations, projects
447
-
448
- router = APIRouter()
449
-
450
- router.include_router(auth.router, prefix="/auth", tags=["auth"])
451
- router.include_router(users.router, prefix="/users", tags=["users"])
452
- router.include_router(organizations.router, prefix="/organizations", tags=["organizations"])
453
- router.include_router(projects.router, prefix="/projects", tags=["projects"])
454
-
455
-
456
- # app/api/v1/users.py
457
- from typing import Optional
458
- from uuid import UUID
459
- from fastapi import APIRouter, HTTPException, status, Query
460
-
461
- from app.api.deps import DB, CurrentUser, Pagination, AdminUser
462
- from app.schemas.user import UserCreate, UserUpdate, UserResponse, UserWithOrganizations
463
- from app.schemas.common import PaginatedResponse
464
- from app.services.user import UserService
465
-
466
- router = APIRouter()
467
-
468
-
469
93
  @router.get("/", response_model=PaginatedResponse[UserResponse])
470
94
  async def list_users(
471
- db: DB,
472
- current_user: AdminUser,
473
- pagination: Pagination,
474
- search: Optional[str] = Query(None, min_length=2),
475
- role: Optional[str] = None,
476
- is_active: Optional[bool] = None,
477
- ):
478
- """List all users (admin only)."""
479
- service = UserService(db)
480
- users, total = await service.list(
481
- offset=pagination.offset,
482
- limit=pagination.limit,
483
- search=search,
484
- role=role,
485
- is_active=is_active,
486
- )
487
- return PaginatedResponse.create(
488
- data=users,
489
- total=total,
490
- page=pagination.page,
491
- limit=pagination.limit,
492
- )
493
-
494
-
495
- @router.get("/me", response_model=UserWithOrganizations)
496
- async def get_current_user(db: DB, current_user: CurrentUser):
497
- """Get current user profile."""
498
- service = UserService(db)
499
- return await service.get_with_organizations(current_user.id)
500
-
501
-
502
- @router.patch("/me", response_model=UserResponse)
503
- async def update_current_user(
504
95
  db: DB,
505
96
  current_user: CurrentUser,
506
- user_in: UserUpdate,
97
+ page: int = Query(1, ge=1),
98
+ limit: int = Query(20, ge=1, le=100),
507
99
  ):
508
- """Update current user profile."""
509
- service = UserService(db)
510
- return await service.update(current_user.id, user_in)
511
-
512
-
513
- @router.get("/{user_id}", response_model=UserResponse)
514
- async def get_user(db: DB, current_user: AdminUser, user_id: UUID):
515
- """Get user by ID (admin only)."""
516
100
  service = UserService(db)
517
- user = await service.get_by_id(user_id)
518
- if not user:
519
- raise HTTPException(
520
- status_code=status.HTTP_404_NOT_FOUND,
521
- detail="User not found",
522
- )
523
- return user
101
+ users, total = await service.list(offset=(page - 1) * limit, limit=limit)
102
+ return PaginatedResponse.create(data=users, total=total, page=page, limit=limit)
524
103
 
525
-
526
- @router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
527
- async def create_user(db: DB, current_user: AdminUser, user_in: UserCreate):
528
- """Create new user (admin only)."""
104
+ @router.post("/", response_model=UserResponse, status_code=201)
105
+ async def create_user(db: DB, user_in: UserCreate):
529
106
  service = UserService(db)
530
- existing = await service.get_by_email(user_in.email)
531
- if existing:
532
- raise HTTPException(
533
- status_code=status.HTTP_409_CONFLICT,
534
- detail="Email already registered",
535
- )
107
+ if await service.get_by_email(user_in.email):
108
+ raise HTTPException(status_code=409, detail="Email exists")
536
109
  return await service.create(user_in)
537
-
538
-
539
- @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
540
- async def delete_user(db: DB, current_user: AdminUser, user_id: UUID):
541
- """Delete user (admin only)."""
542
- service = UserService(db)
543
- user = await service.get_by_id(user_id)
544
- if not user:
545
- raise HTTPException(
546
- status_code=status.HTTP_404_NOT_FOUND,
547
- detail="User not found",
548
- )
549
- await service.delete(user_id)
550
-
551
-
552
- # app/api/v1/auth.py
553
- from datetime import timedelta
554
- from fastapi import APIRouter, HTTPException, status, BackgroundTasks
555
- from pydantic import BaseModel, EmailStr
556
-
557
- from app.api.deps import DB
558
- from app.core.config import settings
559
- from app.core.security import create_access_token, create_refresh_token, verify_password
560
- from app.services.user import UserService
561
- from app.schemas.user import UserCreate, UserResponse
562
-
563
- router = APIRouter()
564
-
565
-
566
- class LoginRequest(BaseModel):
567
- email: EmailStr
568
- password: str
569
-
570
-
571
- class TokenResponse(BaseModel):
572
- access_token: str
573
- refresh_token: str
574
- token_type: str = "bearer"
575
- user: UserResponse
576
-
577
-
578
- class RefreshRequest(BaseModel):
579
- refresh_token: str
580
-
581
-
582
- @router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
583
- async def register(
584
- db: DB,
585
- user_in: UserCreate,
586
- background_tasks: BackgroundTasks,
587
- ):
588
- """Register a new user."""
589
- service = UserService(db)
590
- existing = await service.get_by_email(user_in.email)
591
- if existing:
592
- raise HTTPException(
593
- status_code=status.HTTP_409_CONFLICT,
594
- detail="Email already registered",
595
- )
596
-
597
- user = await service.create(user_in)
598
-
599
- # Send welcome email in background
600
- background_tasks.add_task(send_welcome_email, user.email, user.name)
601
-
602
- return user
603
-
604
-
605
- @router.post("/login", response_model=TokenResponse)
606
- async def login(db: DB, login_data: LoginRequest):
607
- """Authenticate user and return tokens."""
608
- service = UserService(db)
609
- user = await service.get_by_email(login_data.email)
610
-
611
- if not user or not verify_password(login_data.password, user.hashed_password):
612
- raise HTTPException(
613
- status_code=status.HTTP_401_UNAUTHORIZED,
614
- detail="Incorrect email or password",
615
- )
616
-
617
- if not user.is_active:
618
- raise HTTPException(
619
- status_code=status.HTTP_403_FORBIDDEN,
620
- detail="Inactive account",
621
- )
622
-
623
- access_token = create_access_token(
624
- subject=str(user.id),
625
- expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),
626
- )
627
- refresh_token = create_refresh_token(
628
- subject=str(user.id),
629
- expires_delta=timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),
630
- )
631
-
632
- return TokenResponse(
633
- access_token=access_token,
634
- refresh_token=refresh_token,
635
- user=user,
636
- )
637
-
638
-
639
- @router.post("/refresh", response_model=TokenResponse)
640
- async def refresh_token(db: DB, refresh_data: RefreshRequest):
641
- """Refresh access token."""
642
- # Verify and decode refresh token
643
- # Generate new access token
644
- pass
645
-
646
-
647
- async def send_welcome_email(email: str, name: str):
648
- """Background task to send welcome email."""
649
- # Implement email sending logic
650
- pass
651
110
  ```
652
111
 
653
- ### 5. Service Layer
112
+ ## Workflows
654
113
 
655
- ```python
656
- # app/services/user.py
657
- from typing import Optional, List, Tuple
658
- from uuid import UUID
659
- from sqlalchemy import select, func, or_
660
- from sqlalchemy.ext.asyncio import AsyncSession
661
- from sqlalchemy.orm import selectinload
114
+ ### API Development
662
115
 
663
- from app.models.user import User
664
- from app.models.membership import Membership
665
- from app.schemas.user import UserCreate, UserUpdate
666
- from app.core.security import get_password_hash
116
+ 1. Define Pydantic schemas for request/response
117
+ 2. Create service layer for business logic
118
+ 3. Add route with dependency injection
119
+ 4. Write tests with pytest-asyncio
120
+ 5. Document with OpenAPI (automatic)
667
121
 
122
+ ### Service Pattern
668
123
 
124
+ ```python
669
125
  class UserService:
670
126
  def __init__(self, db: AsyncSession):
671
127
  self.db = db
672
128
 
673
- async def get_by_id(self, user_id: UUID) -> Optional[User]:
129
+ async def get_by_id(self, user_id: UUID) -> User | None:
674
130
  result = await self.db.execute(
675
- select(User).where(User.id == user_id, User.deleted_at.is_(None))
131
+ select(User).where(User.id == user_id)
676
132
  )
677
133
  return result.scalar_one_or_none()
678
134
 
679
- async def get_by_email(self, email: str) -> Optional[User]:
680
- result = await self.db.execute(
681
- select(User).where(User.email == email, User.deleted_at.is_(None))
682
- )
683
- return result.scalar_one_or_none()
684
-
685
- async def get_with_organizations(self, user_id: UUID) -> Optional[User]:
686
- result = await self.db.execute(
687
- select(User)
688
- .options(selectinload(User.memberships).selectinload(Membership.organization))
689
- .where(User.id == user_id, User.deleted_at.is_(None))
690
- )
691
- return result.scalar_one_or_none()
692
-
693
- async def list(
694
- self,
695
- offset: int = 0,
696
- limit: int = 20,
697
- search: Optional[str] = None,
698
- role: Optional[str] = None,
699
- is_active: Optional[bool] = None,
700
- ) -> Tuple[List[User], int]:
701
- query = select(User).where(User.deleted_at.is_(None))
702
-
703
- if search:
704
- query = query.where(
705
- or_(
706
- User.name.ilike(f"%{search}%"),
707
- User.email.ilike(f"%{search}%"),
708
- )
709
- )
710
-
711
- if role:
712
- query = query.where(User.role == role)
713
-
714
- if is_active is not None:
715
- query = query.where(User.is_active == is_active)
716
-
717
- # Get total count
718
- count_query = select(func.count()).select_from(query.subquery())
719
- total_result = await self.db.execute(count_query)
720
- total = total_result.scalar()
721
-
722
- # Get paginated results
723
- query = query.order_by(User.created_at.desc()).offset(offset).limit(limit)
724
- result = await self.db.execute(query)
725
- users = list(result.scalars().all())
726
-
727
- return users, total
728
-
729
- async def create(self, user_in: UserCreate) -> User:
730
- user = User(
731
- email=user_in.email,
732
- name=user_in.name,
733
- hashed_password=get_password_hash(user_in.password),
734
- role="user",
735
- is_active=True,
736
- )
135
+ async def create(self, data: UserCreate) -> User:
136
+ user = User(**data.model_dump(), hashed_password=hash_password(data.password))
737
137
  self.db.add(user)
738
138
  await self.db.commit()
739
- await self.db.refresh(user)
740
- return user
741
-
742
- async def update(self, user_id: UUID, user_in: UserUpdate) -> Optional[User]:
743
- user = await self.get_by_id(user_id)
744
- if not user:
745
- return None
746
-
747
- update_data = user_in.model_dump(exclude_unset=True)
748
- for field, value in update_data.items():
749
- setattr(user, field, value)
750
-
751
- await self.db.commit()
752
- await self.db.refresh(user)
753
139
  return user
754
-
755
- async def delete(self, user_id: UUID) -> bool:
756
- user = await self.get_by_id(user_id)
757
- if not user:
758
- return False
759
-
760
- # Soft delete
761
- from datetime import datetime
762
- user.deleted_at = datetime.utcnow()
763
- await self.db.commit()
764
- return True
765
-
766
-
767
- # app/services/organization.py
768
- from typing import Optional, List, Tuple
769
- from uuid import UUID
770
- from sqlalchemy import select, func
771
- from sqlalchemy.ext.asyncio import AsyncSession
772
- from sqlalchemy.orm import selectinload
773
-
774
- from app.models.organization import Organization
775
- from app.models.membership import Membership
776
- from app.schemas.organization import OrganizationCreate, OrganizationUpdate, MemberRole
777
-
778
-
779
- class OrganizationService:
780
- def __init__(self, db: AsyncSession):
781
- self.db = db
782
-
783
- async def get_by_id(self, org_id: UUID) -> Optional[Organization]:
784
- result = await self.db.execute(
785
- select(Organization).where(Organization.id == org_id)
786
- )
787
- return result.scalar_one_or_none()
788
-
789
- async def get_by_slug(self, slug: str) -> Optional[Organization]:
790
- result = await self.db.execute(
791
- select(Organization).where(Organization.slug == slug)
792
- )
793
- return result.scalar_one_or_none()
794
-
795
- async def get_membership(
796
- self, org_id: UUID, user_id: UUID
797
- ) -> Optional[Membership]:
798
- result = await self.db.execute(
799
- select(Membership).where(
800
- Membership.organization_id == org_id,
801
- Membership.user_id == user_id,
802
- )
803
- )
804
- return result.scalar_one_or_none()
805
-
806
- async def list_for_user(
807
- self, user_id: UUID, offset: int = 0, limit: int = 20
808
- ) -> Tuple[List[Organization], int]:
809
- query = (
810
- select(Organization)
811
- .join(Membership)
812
- .where(Membership.user_id == user_id)
813
- )
814
-
815
- count_result = await self.db.execute(
816
- select(func.count()).select_from(query.subquery())
817
- )
818
- total = count_result.scalar()
819
-
820
- result = await self.db.execute(
821
- query.order_by(Organization.created_at.desc())
822
- .offset(offset)
823
- .limit(limit)
824
- )
825
- organizations = list(result.scalars().all())
826
-
827
- return organizations, total
828
-
829
- async def create(
830
- self, org_in: OrganizationCreate, owner_id: UUID
831
- ) -> Organization:
832
- org = Organization(
833
- name=org_in.name,
834
- slug=org_in.slug,
835
- owner_id=owner_id,
836
- )
837
- self.db.add(org)
838
- await self.db.flush()
839
-
840
- # Create owner membership
841
- membership = Membership(
842
- user_id=owner_id,
843
- organization_id=org.id,
844
- role=MemberRole.OWNER,
845
- )
846
- self.db.add(membership)
847
-
848
- await self.db.commit()
849
- await self.db.refresh(org)
850
- return org
851
-
852
- async def add_member(
853
- self, org_id: UUID, user_id: UUID, role: MemberRole = MemberRole.MEMBER
854
- ) -> Membership:
855
- membership = Membership(
856
- user_id=user_id,
857
- organization_id=org_id,
858
- role=role,
859
- )
860
- self.db.add(membership)
861
- await self.db.commit()
862
- await self.db.refresh(membership)
863
- return membership
864
- ```
865
-
866
- ### 6. Database Models
867
-
868
- ```python
869
- # app/models/user.py
870
- from datetime import datetime
871
- from typing import List, TYPE_CHECKING
872
- from uuid import uuid4
873
- from sqlalchemy import String, Boolean, DateTime
874
- from sqlalchemy.dialects.postgresql import UUID
875
- from sqlalchemy.orm import Mapped, mapped_column, relationship
876
-
877
- from app.db.base import Base
878
-
879
- if TYPE_CHECKING:
880
- from app.models.membership import Membership
881
- from app.models.organization import Organization
882
-
883
-
884
- class User(Base):
885
- __tablename__ = "users"
886
-
887
- id: Mapped[UUID] = mapped_column(
888
- UUID(as_uuid=True), primary_key=True, default=uuid4
889
- )
890
- email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
891
- name: Mapped[str] = mapped_column(String(100))
892
- hashed_password: Mapped[str] = mapped_column(String(255))
893
- role: Mapped[str] = mapped_column(String(50), default="user")
894
- is_active: Mapped[bool] = mapped_column(Boolean, default=True)
895
- created_at: Mapped[datetime] = mapped_column(
896
- DateTime, default=datetime.utcnow
897
- )
898
- updated_at: Mapped[datetime] = mapped_column(
899
- DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
900
- )
901
- deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
902
-
903
- # Relationships
904
- memberships: Mapped[List["Membership"]] = relationship(
905
- "Membership", back_populates="user", lazy="selectin"
906
- )
907
- owned_organizations: Mapped[List["Organization"]] = relationship(
908
- "Organization", back_populates="owner", foreign_keys="Organization.owner_id"
909
- )
910
-
911
-
912
- # app/models/organization.py
913
- from datetime import datetime
914
- from typing import List, TYPE_CHECKING
915
- from uuid import uuid4
916
- from sqlalchemy import String, DateTime, ForeignKey
917
- from sqlalchemy.dialects.postgresql import UUID
918
- from sqlalchemy.orm import Mapped, mapped_column, relationship
919
-
920
- from app.db.base import Base
921
-
922
- if TYPE_CHECKING:
923
- from app.models.user import User
924
- from app.models.membership import Membership
925
-
926
-
927
- class Organization(Base):
928
- __tablename__ = "organizations"
929
-
930
- id: Mapped[UUID] = mapped_column(
931
- UUID(as_uuid=True), primary_key=True, default=uuid4
932
- )
933
- name: Mapped[str] = mapped_column(String(255))
934
- slug: Mapped[str] = mapped_column(String(100), unique=True, index=True)
935
- owner_id: Mapped[UUID] = mapped_column(
936
- UUID(as_uuid=True), ForeignKey("users.id")
937
- )
938
- created_at: Mapped[datetime] = mapped_column(
939
- DateTime, default=datetime.utcnow
940
- )
941
-
942
- # Relationships
943
- owner: Mapped["User"] = relationship(
944
- "User", back_populates="owned_organizations", foreign_keys=[owner_id]
945
- )
946
- memberships: Mapped[List["Membership"]] = relationship(
947
- "Membership", back_populates="organization"
948
- )
949
-
950
-
951
- # app/models/membership.py
952
- from datetime import datetime
953
- from uuid import uuid4
954
- from sqlalchemy import String, DateTime, ForeignKey, UniqueConstraint
955
- from sqlalchemy.dialects.postgresql import UUID
956
- from sqlalchemy.orm import Mapped, mapped_column, relationship
957
-
958
- from app.db.base import Base
959
- from app.models.user import User
960
- from app.models.organization import Organization
961
-
962
-
963
- class Membership(Base):
964
- __tablename__ = "memberships"
965
- __table_args__ = (
966
- UniqueConstraint("user_id", "organization_id", name="uq_user_org"),
967
- )
968
-
969
- id: Mapped[UUID] = mapped_column(
970
- UUID(as_uuid=True), primary_key=True, default=uuid4
971
- )
972
- user_id: Mapped[UUID] = mapped_column(
973
- UUID(as_uuid=True), ForeignKey("users.id")
974
- )
975
- organization_id: Mapped[UUID] = mapped_column(
976
- UUID(as_uuid=True), ForeignKey("organizations.id")
977
- )
978
- role: Mapped[str] = mapped_column(String(50), default="member")
979
- joined_at: Mapped[datetime] = mapped_column(
980
- DateTime, default=datetime.utcnow
981
- )
982
-
983
- # Relationships
984
- user: Mapped["User"] = relationship("User", back_populates="memberships")
985
- organization: Mapped["Organization"] = relationship(
986
- "Organization", back_populates="memberships"
987
- )
988
- ```
989
-
990
- ### 7. Testing Patterns
991
-
992
- ```python
993
- # tests/conftest.py
994
- import asyncio
995
- from typing import AsyncGenerator
996
- import pytest
997
- from httpx import AsyncClient, ASGITransport
998
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
999
- from sqlalchemy.orm import sessionmaker
1000
-
1001
- from app.main import app
1002
- from app.db.base import Base
1003
- from app.api.deps import get_db
1004
- from app.core.security import create_access_token
1005
-
1006
- TEST_DATABASE_URL = "sqlite+aiosqlite:///./test.db"
1007
-
1008
- engine = create_async_engine(TEST_DATABASE_URL, echo=True)
1009
- TestingSessionLocal = sessionmaker(
1010
- engine, class_=AsyncSession, expire_on_commit=False
1011
- )
1012
-
1013
-
1014
- @pytest.fixture(scope="session")
1015
- def event_loop():
1016
- loop = asyncio.get_event_loop_policy().new_event_loop()
1017
- yield loop
1018
- loop.close()
1019
-
1020
-
1021
- @pytest.fixture(scope="function")
1022
- async def db_session() -> AsyncGenerator[AsyncSession, None]:
1023
- async with engine.begin() as conn:
1024
- await conn.run_sync(Base.metadata.create_all)
1025
-
1026
- async with TestingSessionLocal() as session:
1027
- yield session
1028
-
1029
- async with engine.begin() as conn:
1030
- await conn.run_sync(Base.metadata.drop_all)
1031
-
1032
-
1033
- @pytest.fixture(scope="function")
1034
- async def client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
1035
- async def override_get_db():
1036
- yield db_session
1037
-
1038
- app.dependency_overrides[get_db] = override_get_db
1039
-
1040
- async with AsyncClient(
1041
- transport=ASGITransport(app=app),
1042
- base_url="http://test",
1043
- ) as ac:
1044
- yield ac
1045
-
1046
- app.dependency_overrides.clear()
1047
-
1048
-
1049
- @pytest.fixture
1050
- async def test_user(db_session: AsyncSession):
1051
- from app.models.user import User
1052
- from app.core.security import get_password_hash
1053
-
1054
- user = User(
1055
- email="test@example.com",
1056
- name="Test User",
1057
- hashed_password=get_password_hash("TestPass123!"),
1058
- role="user",
1059
- is_active=True,
1060
- )
1061
- db_session.add(user)
1062
- await db_session.commit()
1063
- await db_session.refresh(user)
1064
- return user
1065
-
1066
-
1067
- @pytest.fixture
1068
- def auth_headers(test_user):
1069
- token = create_access_token(subject=str(test_user.id))
1070
- return {"Authorization": f"Bearer {token}"}
1071
-
1072
-
1073
- # tests/test_users.py
1074
- import pytest
1075
- from httpx import AsyncClient
1076
-
1077
-
1078
- @pytest.mark.asyncio
1079
- async def test_get_current_user(client: AsyncClient, auth_headers: dict):
1080
- response = await client.get("/api/v1/users/me", headers=auth_headers)
1081
- assert response.status_code == 200
1082
- data = response.json()
1083
- assert data["email"] == "test@example.com"
1084
- assert "hashed_password" not in data
1085
-
1086
-
1087
- @pytest.mark.asyncio
1088
- async def test_update_current_user(client: AsyncClient, auth_headers: dict):
1089
- response = await client.patch(
1090
- "/api/v1/users/me",
1091
- headers=auth_headers,
1092
- json={"name": "Updated Name"},
1093
- )
1094
- assert response.status_code == 200
1095
- assert response.json()["name"] == "Updated Name"
1096
-
1097
-
1098
- @pytest.mark.asyncio
1099
- async def test_unauthorized_access(client: AsyncClient):
1100
- response = await client.get("/api/v1/users/me")
1101
- assert response.status_code == 403
1102
-
1103
-
1104
- @pytest.mark.asyncio
1105
- async def test_register_user(client: AsyncClient):
1106
- response = await client.post(
1107
- "/api/v1/auth/register",
1108
- json={
1109
- "email": "new@example.com",
1110
- "name": "New User",
1111
- "password": "SecurePass123!",
1112
- },
1113
- )
1114
- assert response.status_code == 201
1115
- data = response.json()
1116
- assert data["email"] == "new@example.com"
1117
- assert "password" not in data
1118
-
1119
-
1120
- @pytest.mark.asyncio
1121
- async def test_register_duplicate_email(client: AsyncClient, test_user):
1122
- response = await client.post(
1123
- "/api/v1/auth/register",
1124
- json={
1125
- "email": "test@example.com",
1126
- "name": "Duplicate User",
1127
- "password": "SecurePass123!",
1128
- },
1129
- )
1130
- assert response.status_code == 409
1131
-
1132
-
1133
- @pytest.mark.asyncio
1134
- async def test_login(client: AsyncClient, test_user):
1135
- response = await client.post(
1136
- "/api/v1/auth/login",
1137
- json={
1138
- "email": "test@example.com",
1139
- "password": "TestPass123!",
1140
- },
1141
- )
1142
- assert response.status_code == 200
1143
- data = response.json()
1144
- assert "access_token" in data
1145
- assert "refresh_token" in data
1146
-
1147
-
1148
- @pytest.mark.asyncio
1149
- async def test_login_wrong_password(client: AsyncClient, test_user):
1150
- response = await client.post(
1151
- "/api/v1/auth/login",
1152
- json={
1153
- "email": "test@example.com",
1154
- "password": "WrongPassword123!",
1155
- },
1156
- )
1157
- assert response.status_code == 401
1158
140
  ```
1159
141
 
1160
- ## Use Cases
1161
-
1162
- ### Background Task Processing
1163
-
1164
- ```python
1165
- # app/tasks/email.py
1166
- from celery import Celery
1167
- from app.core.config import settings
1168
-
1169
- celery = Celery("tasks", broker=settings.REDIS_URL)
1170
-
1171
-
1172
- @celery.task(bind=True, max_retries=3)
1173
- def send_email_task(self, to: str, subject: str, body: str):
1174
- try:
1175
- # Send email logic
1176
- pass
1177
- except Exception as exc:
1178
- raise self.retry(exc=exc, countdown=60)
142
+ ## Best Practices
1179
143
 
144
+ | Do | Avoid |
145
+ |----|-------|
146
+ | Use async/await everywhere | Sync operations in async code |
147
+ | Validate with Pydantic v2 | Manual validation |
148
+ | Use dependency injection | Direct imports |
149
+ | Handle errors with HTTPException | Generic exceptions |
150
+ | Use type hints | `Any` types |
1180
151
 
1181
- # Using with FastAPI background tasks
1182
- from fastapi import BackgroundTasks
152
+ ## Project Structure
1183
153
 
1184
- @router.post("/invite")
1185
- async def invite_user(
1186
- email: str,
1187
- background_tasks: BackgroundTasks,
1188
- current_user: CurrentUser,
1189
- ):
1190
- # Add task to background
1191
- background_tasks.add_task(
1192
- send_invitation_email,
1193
- email=email,
1194
- inviter_name=current_user.name,
1195
- )
1196
- return {"message": "Invitation sent"}
1197
154
  ```
1198
-
1199
- ### WebSocket for Real-time Updates
1200
-
1201
- ```python
1202
- # app/api/v1/websocket.py
1203
- from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends
1204
- from typing import Dict, Set
1205
- from uuid import UUID
1206
- import json
1207
-
1208
- router = APIRouter()
1209
-
1210
-
1211
- class ConnectionManager:
1212
- def __init__(self):
1213
- self.active_connections: Dict[str, Set[WebSocket]] = {}
1214
-
1215
- async def connect(self, websocket: WebSocket, room: str):
1216
- await websocket.accept()
1217
- if room not in self.active_connections:
1218
- self.active_connections[room] = set()
1219
- self.active_connections[room].add(websocket)
1220
-
1221
- def disconnect(self, websocket: WebSocket, room: str):
1222
- if room in self.active_connections:
1223
- self.active_connections[room].discard(websocket)
1224
-
1225
- async def broadcast(self, room: str, message: dict):
1226
- if room in self.active_connections:
1227
- for connection in self.active_connections[room]:
1228
- await connection.send_json(message)
1229
-
1230
-
1231
- manager = ConnectionManager()
1232
-
1233
-
1234
- @router.websocket("/ws/projects/{project_id}")
1235
- async def project_websocket(
1236
- websocket: WebSocket,
1237
- project_id: UUID,
1238
- ):
1239
- room = f"project:{project_id}"
1240
- await manager.connect(websocket, room)
1241
-
1242
- try:
1243
- while True:
1244
- data = await websocket.receive_text()
1245
- message = json.loads(data)
1246
- await manager.broadcast(room, message)
1247
- except WebSocketDisconnect:
1248
- manager.disconnect(websocket, room)
155
+ app/
156
+ ├── main.py
157
+ ├── core/
158
+ │ ├── config.py
159
+ │ ├── security.py
160
+ │ └── deps.py
161
+ ├── api/
162
+ │ └── v1/
163
+ │ ├── __init__.py
164
+ │ ├── users.py
165
+ │ └── auth.py
166
+ ├── models/
167
+ ├── schemas/
168
+ ├── services/
169
+ └── db/
170
+ ├── base.py
171
+ └── session.py
172
+ tests/
173
+ ├── conftest.py
174
+ └── test_users.py
1249
175
  ```
1250
176
 
1251
- ## Best Practices
1252
-
1253
- ### Do's
1254
-
1255
- - Use Pydantic v2 for all validation
1256
- - Use async/await consistently throughout
1257
- - Use dependency injection for testability
1258
- - Use proper type hints everywhere
1259
- - Use background tasks for long operations
1260
- - Use connection pooling for database
1261
- - Use Redis for caching and rate limiting
1262
- - Write comprehensive tests with pytest
1263
- - Use proper error handling with HTTPException
1264
- - Use environment variables for configuration
1265
-
1266
- ### Don'ts
1267
-
1268
- - Don't use sync operations in async functions
1269
- - Don't skip input validation
1270
- - Don't hardcode configuration values
1271
- - Don't ignore database session management
1272
- - Don't expose sensitive data in responses
1273
- - Don't use `*` imports
1274
- - Don't skip error handling
1275
- - Don't forget to close database sessions
1276
- - Don't use blocking I/O operations
1277
- - Don't ignore type checker warnings
1278
-
1279
- ## References
1280
-
1281
- - [FastAPI Documentation](https://fastapi.tiangolo.com/)
1282
- - [Pydantic v2 Documentation](https://docs.pydantic.dev/latest/)
1283
- - [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/en/20/)
1284
- - [pytest-asyncio Documentation](https://pytest-asyncio.readthedocs.io/)
1285
- - [Starlette Documentation](https://www.starlette.io/)
177
+ For detailed examples and patterns, see reference files above.