feat: initial Claude Code configuration scaffold
Comprehensive Claude Code guidance system with: - 5 agents: tdd-guardian, code-reviewer, security-scanner, refactor-scan, dependency-audit - 18 skills covering languages (Python, TypeScript, Rust, Go, Java, C#), infrastructure (AWS, Azure, GCP, Terraform, Ansible, Docker/K8s, Database, CI/CD), testing (TDD, UI, Browser), and patterns (Monorepo, API Design, Observability) - 3 hooks: secret detection, auto-formatting, TDD git pre-commit - Strict TDD enforcement with 80%+ coverage requirements - Multi-model strategy: Opus for planning, Sonnet for execution (opusplan) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
446
.claude/skills/languages/python/SKILL.md
Normal file
446
.claude/skills/languages/python/SKILL.md
Normal file
@@ -0,0 +1,446 @@
|
||||
---
|
||||
name: python-fastapi
|
||||
description: Python development with FastAPI, pytest, Ruff, Pydantic v2, and SQLAlchemy async patterns. Use when writing Python code, tests, or APIs.
|
||||
---
|
||||
|
||||
# Python Development Skill
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── backend/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # FastAPI app entry
|
||||
│ ├── config.py # Pydantic Settings
|
||||
│ ├── domains/
|
||||
│ │ └── users/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── router.py # API routes
|
||||
│ │ ├── service.py # Business logic
|
||||
│ │ ├── models.py # SQLAlchemy models
|
||||
│ │ └── schemas.py # Pydantic schemas
|
||||
│ └── shared/
|
||||
│ ├── database.py # DB session management
|
||||
│ └── models/ # Base models
|
||||
tests/
|
||||
├── backend/
|
||||
│ ├── conftest.py # Shared fixtures
|
||||
│ └── domains/
|
||||
│ └── users/
|
||||
│ └── test_service.py
|
||||
```
|
||||
|
||||
## Testing with pytest
|
||||
|
||||
### Configuration (pyproject.toml)
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
addopts = [
|
||||
"-v",
|
||||
"--strict-markers",
|
||||
"--cov=src",
|
||||
"--cov-report=term-missing",
|
||||
"--cov-fail-under=80"
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["src"]
|
||||
omit = ["*/__init__.py", "*/migrations/*"]
|
||||
```
|
||||
|
||||
### Async Test Fixtures
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from src.backend.shared.models.base import Base
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def db_session():
|
||||
"""Create in-memory SQLite for fast tests."""
|
||||
engine = create_async_engine(
|
||||
"sqlite+aiosqlite:///:memory:",
|
||||
echo=False,
|
||||
)
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async_session = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
await session.rollback()
|
||||
|
||||
await engine.dispose()
|
||||
```
|
||||
|
||||
### Factory Functions for Test Data
|
||||
```python
|
||||
# tests/factories.py
|
||||
from typing import Any
|
||||
|
||||
from src.backend.domains.users.schemas import UserCreate
|
||||
|
||||
|
||||
def get_mock_user_create(overrides: dict[str, Any] | None = None) -> UserCreate:
|
||||
"""Factory for UserCreate with sensible defaults."""
|
||||
defaults = {
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
"password": "securepassword123",
|
||||
}
|
||||
return UserCreate(**(defaults | (overrides or {})))
|
||||
|
||||
|
||||
def get_mock_user_dict(overrides: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Factory for raw user dict."""
|
||||
defaults = {
|
||||
"id": "user_123",
|
||||
"email": "test@example.com",
|
||||
"name": "Test User",
|
||||
"is_active": True,
|
||||
}
|
||||
return defaults | (overrides or {})
|
||||
```
|
||||
|
||||
### Test Patterns
|
||||
```python
|
||||
# tests/backend/domains/users/test_service.py
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from src.backend.domains.users.service import UserService
|
||||
from tests.factories import get_mock_user_create
|
||||
|
||||
|
||||
class TestUserService:
|
||||
"""Test user service behavior through public API."""
|
||||
|
||||
async def test_create_user_returns_user_with_id(self, db_session):
|
||||
"""Creating a user should return user with generated ID."""
|
||||
service = UserService(db_session)
|
||||
user_data = get_mock_user_create()
|
||||
|
||||
result = await service.create_user(user_data)
|
||||
|
||||
assert result.id is not None
|
||||
assert result.email == user_data.email
|
||||
|
||||
async def test_create_user_hashes_password(self, db_session):
|
||||
"""Password should be hashed, not stored in plain text."""
|
||||
service = UserService(db_session)
|
||||
user_data = get_mock_user_create({"password": "mypassword"})
|
||||
|
||||
result = await service.create_user(user_data)
|
||||
|
||||
assert result.hashed_password != "mypassword"
|
||||
assert len(result.hashed_password) > 50 # Hashed
|
||||
|
||||
async def test_create_duplicate_email_raises_error(self, db_session):
|
||||
"""Duplicate email should raise ValueError."""
|
||||
service = UserService(db_session)
|
||||
user_data = get_mock_user_create()
|
||||
await service.create_user(user_data)
|
||||
|
||||
with pytest.raises(ValueError, match="Email already exists"):
|
||||
await service.create_user(user_data)
|
||||
|
||||
async def test_get_user_not_found_returns_none(self, db_session):
|
||||
"""Non-existent user should return None."""
|
||||
service = UserService(db_session)
|
||||
|
||||
result = await service.get_user("nonexistent_id")
|
||||
|
||||
assert result is None
|
||||
```
|
||||
|
||||
## Pydantic v2 Patterns
|
||||
|
||||
### Schemas with Validation
|
||||
```python
|
||||
# src/backend/domains/users/schemas.py
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
"""Base user schema with shared fields."""
|
||||
|
||||
email: EmailStr
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
"""Schema for creating a user."""
|
||||
|
||||
password: str = Field(..., min_length=8)
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def password_must_be_strong(cls, v: str) -> str:
|
||||
if not any(c.isupper() for c in v):
|
||||
raise ValueError("Password must contain uppercase letter")
|
||||
if not any(c.isdigit() for c in v):
|
||||
raise ValueError("Password must contain a digit")
|
||||
return v
|
||||
|
||||
|
||||
class UserResponse(UserBase):
|
||||
"""Schema for user responses (no password)."""
|
||||
|
||||
id: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
```
|
||||
|
||||
### Configuration with Pydantic Settings
|
||||
```python
|
||||
# src/backend/config.py
|
||||
from functools import lru_cache
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings from environment."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
# Database
|
||||
database_url: str
|
||||
database_pool_size: int = 5
|
||||
|
||||
# Redis
|
||||
redis_url: str = "redis://localhost:6379"
|
||||
|
||||
# Security
|
||||
secret_key: str
|
||||
access_token_expire_minutes: int = 30
|
||||
|
||||
# AWS
|
||||
aws_region: str = "eu-west-2"
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
```
|
||||
|
||||
## FastAPI Patterns
|
||||
|
||||
### Router with Dependency Injection
|
||||
```python
|
||||
# src/backend/domains/users/router.py
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.backend.shared.database import get_db_session
|
||||
from .schemas import UserCreate, UserResponse
|
||||
from .service import UserService
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
def get_user_service(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> UserService:
|
||||
return UserService(session)
|
||||
|
||||
|
||||
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
user_data: UserCreate,
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> UserResponse:
|
||||
"""Create a new user."""
|
||||
try:
|
||||
return await service.create_user(user_data)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
async def get_user(
|
||||
user_id: str,
|
||||
service: UserService = Depends(get_user_service),
|
||||
) -> UserResponse:
|
||||
"""Get user by ID."""
|
||||
user = await service.get_user(user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
return user
|
||||
```
|
||||
|
||||
## SQLAlchemy 2.0 Async Patterns
|
||||
|
||||
### Model Definition
|
||||
```python
|
||||
# src/backend/domains/users/models.py
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, Boolean, DateTime
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.backend.shared.models.base import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
hashed_password: Mapped[str] = mapped_column(String(255))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow
|
||||
)
|
||||
```
|
||||
|
||||
### Async Repository Pattern
|
||||
```python
|
||||
# src/backend/domains/users/repository.py
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self._session = session
|
||||
|
||||
async def get_by_id(self, user_id: str) -> User | None:
|
||||
result = await self._session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_by_email(self, email: str) -> User | None:
|
||||
result = await self._session.execute(
|
||||
select(User).where(User.email == email)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create(self, user: User) -> User:
|
||||
self._session.add(user)
|
||||
await self._session.commit()
|
||||
await self._session.refresh(user)
|
||||
return user
|
||||
```
|
||||
|
||||
## Ruff Configuration
|
||||
|
||||
```toml
|
||||
# pyproject.toml
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"B", # flake8-bugbear
|
||||
"SIM", # flake8-simplify
|
||||
"ASYNC", # flake8-async
|
||||
]
|
||||
ignore = [
|
||||
"B008", # function-call-in-default-argument (FastAPI Depends)
|
||||
"E501", # line-too-long (handled by formatter)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["F401"] # unused imports (re-exports)
|
||||
"tests/*" = ["S101"] # assert allowed in tests
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Testing
|
||||
pytest # Run all tests
|
||||
pytest tests/backend/domains/users/ # Run specific directory
|
||||
pytest -k "test_create" # Run tests matching pattern
|
||||
pytest --cov=src --cov-report=html # Coverage with HTML report
|
||||
pytest -x # Stop on first failure
|
||||
pytest --lf # Run last failed tests
|
||||
|
||||
# Linting & Formatting
|
||||
ruff check . # Lint check
|
||||
ruff check . --fix # Auto-fix issues
|
||||
ruff format . # Format code
|
||||
ruff format . --check # Check formatting
|
||||
|
||||
# Type Checking
|
||||
mypy src # Full type check
|
||||
pyright src # Alternative type checker
|
||||
|
||||
# Development
|
||||
uvicorn src.backend.main:app --reload # Dev server
|
||||
alembic revision --autogenerate -m "msg" # Create migration
|
||||
alembic upgrade head # Apply migrations
|
||||
```
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# BAD: Mutable default argument
|
||||
def process_items(items: list = []): # Creates shared state!
|
||||
items.append("new")
|
||||
return items
|
||||
|
||||
# GOOD: Use None default
|
||||
def process_items(items: list | None = None) -> list:
|
||||
if items is None:
|
||||
items = []
|
||||
return [*items, "new"]
|
||||
|
||||
|
||||
# BAD: Bare except
|
||||
try:
|
||||
risky_operation()
|
||||
except: # Catches everything including SystemExit!
|
||||
pass
|
||||
|
||||
# GOOD: Specific exceptions
|
||||
try:
|
||||
risky_operation()
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation failed: {e}")
|
||||
raise
|
||||
|
||||
|
||||
# BAD: String formatting in SQL (injection risk!)
|
||||
query = f"SELECT * FROM users WHERE email = '{email}'"
|
||||
|
||||
# GOOD: Parameterized query
|
||||
stmt = select(User).where(User.email == email)
|
||||
|
||||
|
||||
# BAD: Sync in async context
|
||||
async def get_data():
|
||||
return requests.get(url) # Blocks event loop!
|
||||
|
||||
# GOOD: Use async client
|
||||
async def get_data():
|
||||
async with httpx.AsyncClient() as client:
|
||||
return await client.get(url)
|
||||
```
|
||||
Reference in New Issue
Block a user