diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 68ee985d..3fbabfea 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ { "name": "ecc", "source": "./", - "description": "The most comprehensive Claude Code plugin — 50 agents, 188 skills, 68 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning", + "description": "The most comprehensive Claude Code plugin — 51 agents, 189 skills, 69 legacy command shims, selective install profiles, and production-ready hooks for TDD, security scanning, code review, and continuous learning", "version": "2.0.0-rc.1", "author": { "name": "Affaan Mustafa", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b5c9a8c0..e1ee3c3f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ecc", "version": "2.0.0-rc.1", - "description": "Battle-tested Claude Code plugin for engineering teams — 50 agents, 188 skills, 68 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use", + "description": "Battle-tested Claude Code plugin for engineering teams — 51 agents, 189 skills, 69 legacy command shims, production-ready hooks, and selective install workflows evolved through continuous real-world use", "author": { "name": "Affaan Mustafa", "url": "https://x.com/affaanmustafa" diff --git a/AGENTS.md b/AGENTS.md index e3166b4f..6c66ad5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — Agent Instructions -This is a **production-ready AI coding plugin** providing 50 specialized agents, 188 skills, 68 commands, and automated hook workflows for software development. +This is a **production-ready AI coding plugin** providing 51 specialized agents, 189 skills, 69 commands, and automated hook workflows for software development. **Version:** 2.0.0-rc.1 @@ -145,9 +145,9 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat ## Project Structure ``` -agents/ — 50 specialized subagents -skills/ — 188 workflow skills and domain knowledge -commands/ — 68 slash commands +agents/ — 51 specialized subagents +skills/ — 189 workflow skills and domain knowledge +commands/ — 69 slash commands hooks/ — Trigger-based automations rules/ — Always-follow guidelines (common + per-language) scripts/ — Cross-platform Node.js utilities diff --git a/README.md b/README.md index 180a60f5..fb7329a3 100644 --- a/README.md +++ b/README.md @@ -350,7 +350,7 @@ If you stacked methods, clean up in this order: /plugin list ecc@ecc ``` -**That's it!** You now have access to 50 agents, 188 skills, and 68 legacy command shims. +**That's it!** You now have access to 51 agents, 189 skills, and 69 legacy command shims. ### Dashboard GUI @@ -448,7 +448,7 @@ everything-claude-code/ | |-- plugin.json # Plugin metadata and component paths | |-- marketplace.json # Marketplace catalog for /plugin marketplace add | -|-- agents/ # 50 specialized subagents for delegation +|-- agents/ # 51 specialized subagents for delegation | |-- planner.md # Feature implementation planning | |-- architect.md # System design decisions | |-- tdd-guide.md # Test-driven development @@ -1336,9 +1336,9 @@ The configuration is automatically detected from `.opencode/opencode.json`. | Feature | Claude Code | OpenCode | Status | |---------|-------------|----------|--------| -| Agents | PASS: 50 agents | PASS: 12 agents | **Claude Code leads** | -| Commands | PASS: 68 commands | PASS: 31 commands | **Claude Code leads** | -| Skills | PASS: 188 skills | PASS: 37 skills | **Claude Code leads** | +| Agents | PASS: 51 agents | PASS: 12 agents | **Claude Code leads** | +| Commands | PASS: 69 commands | PASS: 31 commands | **Claude Code leads** | +| Skills | PASS: 189 skills | PASS: 37 skills | **Claude Code leads** | | Hooks | PASS: 8 event types | PASS: 11 events | **OpenCode has more!** | | Rules | PASS: 29 rules | PASS: 13 instructions | **Claude Code leads** | | MCP Servers | PASS: 14 servers | PASS: Full | **Full parity** | @@ -1441,9 +1441,9 @@ ECC is the **first plugin to maximize every major AI coding tool**. Here's how e | Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **Agents** | 50 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | -| **Commands** | 68 | Shared | Instruction-based | 31 | -| **Skills** | 188 | Shared | 10 (native format) | 37 | +| **Agents** | 51 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | +| **Commands** | 69 | Shared | Instruction-based | 31 | +| **Skills** | 189 | Shared | 10 (native format) | 37 | | **Hook Events** | 8 types | 15 types | None yet | 11 types | | **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | | **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | diff --git a/README.zh-CN.md b/README.zh-CN.md index 98e52c60..0a52684b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -160,7 +160,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**完成!** 你现在可以使用 50 个代理、188 个技能和 68 个命令。 +**完成!** 你现在可以使用 51 个代理、189 个技能和 69 个命令。 ### multi-* 命令需要额外配置 diff --git a/agent.yaml b/agent.yaml index b881da12..954fa5fc 100644 --- a/agent.yaml +++ b/agent.yaml @@ -152,6 +152,7 @@ commands: - cpp-review - cpp-test - evolve + - fastapi-review - feature-dev - flutter-build - flutter-review diff --git a/agents/fastapi-reviewer.md b/agents/fastapi-reviewer.md new file mode 100644 index 00000000..735ff05a --- /dev/null +++ b/agents/fastapi-reviewer.md @@ -0,0 +1,70 @@ +--- +name: fastapi-reviewer +description: Reviews FastAPI applications for async correctness, dependency injection, Pydantic schemas, security, OpenAPI quality, testing, and production readiness. +tools: ["Read", "Grep", "Glob", "Bash"] +model: sonnet +--- + +You are a senior FastAPI reviewer focused on production Python APIs. + +## Review Scope + +- FastAPI app construction, routing, middleware, and exception handling. +- Pydantic request, update, and response models. +- Async database and HTTP patterns. +- Dependency injection for database sessions, auth, pagination, and settings. +- Authentication, authorization, CORS, rate limits, logging, and secret handling. +- Test dependency overrides and client setup. +- OpenAPI metadata and generated docs. + +## Out of Scope + +- Non-FastAPI frameworks unless they directly interact with the FastAPI app. +- Broad Python style review already covered by `python-reviewer`. +- Dependency additions without a concrete problem and maintenance rationale. + +## Review Workflow + +1. Locate the app entry point, usually `main.py`, `app.py`, or `app/main.py`. +2. Identify routers, schemas, dependencies, database session setup, and tests. +3. Run available local checks when safe, such as `pytest`, `ruff`, `mypy`, or `uv run pytest`. +4. Review the changed files first, then inspect adjacent definitions needed to prove findings. +5. Report only actionable issues with file and line references when available. + +## Finding Priorities + +### Critical + +- Hardcoded secrets or tokens. +- SQL built through string interpolation. +- Passwords, token hashes, or internal auth fields exposed in response models. +- Auth dependencies that can be bypassed or do not validate expiry/signature. + +### High + +- Blocking database or HTTP clients inside async routes. +- Database sessions created inline in handlers instead of dependencies. +- Test overrides targeting the wrong dependency. +- `allow_origins=["*"]` combined with credentialed CORS. +- Missing request validation for write endpoints. + +### Medium + +- Missing pagination on list endpoints. +- OpenAPI docs missing response models or error response descriptions. +- Duplicated route logic that should move into a service/dependency. +- Missing timeout settings for external HTTP clients. + +## Output Format + +```text +[SEVERITY] Short issue title +File: path/to/file.py:42 +Issue: What is wrong and why it matters. +Fix: Concrete change to make. +``` + +End with: + +- `Tests checked:` commands run or why they were skipped. +- `Residual risk:` anything important that could not be verified. diff --git a/commands/fastapi-review.md b/commands/fastapi-review.md new file mode 100644 index 00000000..9d730c6e --- /dev/null +++ b/commands/fastapi-review.md @@ -0,0 +1,39 @@ +--- +description: Review a FastAPI application for architecture, async correctness, dependency injection, Pydantic schemas, security, performance, and testability. +--- + +# FastAPI Review + +Invoke the `fastapi-reviewer` agent for a focused FastAPI review. + +## Usage + +```text +/fastapi-review [file-or-directory] +``` + +## Review Areas + +- App factory, router boundaries, middleware, and exception handlers. +- Pydantic request and response schema separation. +- Dependency injection for database sessions, auth, pagination, and settings. +- Async database and external HTTP patterns. +- CORS, auth, rate limits, logging, and secret handling. +- OpenAPI metadata and documented response models. +- Test client setup and dependency overrides. + +## Expected Output + +```text +[SEVERITY] Short issue title +File: path/to/file.py:42 +Issue: What is wrong and why it matters. +Fix: Concrete change to make. +``` + +## Related + +- Agent: `fastapi-reviewer` +- Skill: `fastapi-patterns` +- Command: `/python-review` +- Skill: `security-scan` diff --git a/docs/zh-CN/AGENTS.md b/docs/zh-CN/AGENTS.md index f48b0dda..e0fd304a 100644 --- a/docs/zh-CN/AGENTS.md +++ b/docs/zh-CN/AGENTS.md @@ -1,6 +1,6 @@ # Everything Claude Code (ECC) — 智能体指令 -这是一个**生产就绪的 AI 编码插件**,提供 50 个专业代理、188 项技能、68 条命令以及自动化钩子工作流,用于软件开发。 +这是一个**生产就绪的 AI 编码插件**,提供 51 个专业代理、189 项技能、69 条命令以及自动化钩子工作流,用于软件开发。 **版本:** 2.0.0-rc.1 @@ -146,9 +146,9 @@ ## 项目结构 ``` -agents/ — 50 个专业子代理 -skills/ — 188 个工作流技能和领域知识 -commands/ — 68 个斜杠命令 +agents/ — 51 个专业子代理 +skills/ — 189 个工作流技能和领域知识 +commands/ — 69 个斜杠命令 hooks/ — 基于触发的自动化 rules/ — 始终遵循的指导方针(通用 + 每种语言) scripts/ — 跨平台 Node.js 实用工具 diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 5f189265..da6f4574 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -224,7 +224,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/" /plugin list ecc@ecc ``` -**搞定!** 你现在可以使用 50 个智能体、188 项技能和 68 个命令了。 +**搞定!** 你现在可以使用 51 个智能体、189 项技能和 69 个命令了。 *** @@ -1132,9 +1132,9 @@ opencode | 功能特性 | Claude Code | OpenCode | 状态 | |---------|-------------|----------|--------| -| 智能体 | PASS: 50 个 | PASS: 12 个 | **Claude Code 领先** | -| 命令 | PASS: 68 个 | PASS: 31 个 | **Claude Code 领先** | -| 技能 | PASS: 188 项 | PASS: 37 项 | **Claude Code 领先** | +| 智能体 | PASS: 51 个 | PASS: 12 个 | **Claude Code 领先** | +| 命令 | PASS: 69 个 | PASS: 31 个 | **Claude Code 领先** | +| 技能 | PASS: 189 项 | PASS: 37 项 | **Claude Code 领先** | | 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** | | 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** | | MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** | @@ -1240,9 +1240,9 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以 | 功能特性 | Claude Code | Cursor IDE | Codex CLI | OpenCode | |---------|------------|------------|-----------|----------| -| **智能体** | 50 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | -| **命令** | 68 | 共享 | 基于指令 | 31 | -| **技能** | 188 | 共享 | 10 (原生格式) | 37 | +| **智能体** | 51 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 | +| **命令** | 69 | 共享 | 基于指令 | 31 | +| **技能** | 189 | 共享 | 10 (原生格式) | 37 | | **钩子事件** | 8 种类型 | 15 种类型 | 暂无 | 11 种类型 | | **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | N/A | 插件钩子 | | **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 | diff --git a/manifests/install-modules.json b/manifests/install-modules.json index 5941ba2b..bf43984d 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -125,6 +125,7 @@ "skills/django-tdd", "skills/django-verification", "skills/dotnet-patterns", + "skills/fastapi-patterns", "skills/frontend-patterns", "skills/frontend-slides", "skills/golang-patterns", diff --git a/package.json b/package.json index ca6a7292..8746d7db 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "skills/evm-token-decimals/", "skills/exa-search/", "skills/fal-ai-media/", + "skills/fastapi-patterns/", "skills/finance-billing-ops/", "skills/foundation-models-on-device/", "skills/frontend-patterns/", diff --git a/rules/python/fastapi.md b/rules/python/fastapi.md new file mode 100644 index 00000000..6417b3a8 --- /dev/null +++ b/rules/python/fastapi.md @@ -0,0 +1,58 @@ +--- +paths: + - "**/app/**/*.py" + - "**/fastapi/**/*.py" + - "**/*_api.py" +--- +# FastAPI Rules + +Use these rules for FastAPI projects alongside the general Python rules. + +## Structure + +- Put app construction in `create_app()`. +- Keep routers thin; move persistence and business behavior into services or CRUD helpers. +- Keep request schemas, update schemas, and response schemas separate. +- Keep database sessions and auth in dependencies. + +## Async + +- Use `async def` for endpoints that perform I/O. +- Use async database and HTTP clients from async endpoints. +- Do not call `requests`, sync SQLAlchemy sessions, or blocking file/network operations from async routes. + +## Dependency Injection + +```python +@router.get("/users/{user_id}") +async def get_user( + user_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + ... +``` + +Do not create `SessionLocal()` or long-lived clients inside route handlers. + +## Schemas + +- Never include passwords, password hashes, access tokens, refresh tokens, or internal auth state in response models. +- Use `response_model` on endpoints that return application data. +- Use field constraints instead of hand-written validation when Pydantic can express the rule. + +## Security + +- Keep CORS origins environment-specific. +- Do not combine wildcard origins with credentialed CORS. +- Validate JWT expiry, issuer, audience, and algorithm. +- Rate-limit auth and write-heavy endpoints. +- Redact credentials, cookies, authorization headers, and tokens from logs. + +## Testing + +- Override the exact dependency used by `Depends`. +- Clear `app.dependency_overrides` after tests. +- Prefer async test clients for async applications. + +See skill: `fastapi-patterns`. diff --git a/skills/fastapi-patterns/SKILL.md b/skills/fastapi-patterns/SKILL.md new file mode 100644 index 00000000..541a06b7 --- /dev/null +++ b/skills/fastapi-patterns/SKILL.md @@ -0,0 +1,327 @@ +--- +name: fastapi-patterns +description: FastAPI patterns for async APIs, dependency injection, Pydantic request and response models, OpenAPI docs, tests, security, and production readiness. +origin: community +--- + +# FastAPI Patterns + +Production-oriented patterns for FastAPI services. + +## When to Use + +- Building or reviewing a FastAPI app. +- Splitting routers, schemas, dependencies, and database access. +- Writing async endpoints that call a database or external service. +- Adding authentication, authorization, OpenAPI docs, tests, or deployment settings. +- Checking a FastAPI PR for copy-pasteable examples and production risks. + +## How It Works + +Treat the FastAPI app as a thin HTTP layer over explicit dependencies and service code: + +- `main.py` owns app construction, middleware, exception handlers, and router registration. +- `schemas/` owns Pydantic request and response models. +- `dependencies.py` owns database, auth, pagination, and request-scoped dependencies. +- `services/` or `crud/` owns business and persistence operations. +- `tests/` overrides dependencies instead of opening production resources. + +Prefer small routers and explicit `response_model` declarations. Keep raw ORM objects, secrets, and framework globals out of response schemas. + +## Project Layout + +```text +app/ +|-- main.py +|-- config.py +|-- dependencies.py +|-- exceptions.py +|-- api/ +| `-- routes/ +| |-- users.py +| `-- health.py +|-- core/ +| |-- security.py +| `-- middleware.py +|-- db/ +| |-- session.py +| `-- crud.py +|-- models/ +|-- schemas/ +`-- tests/ +``` + +## Application Factory + +Use a factory so tests and workers can build the app with controlled settings. + +```python +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.routes import health, users +from app.config import settings +from app.db.session import close_db, init_db +from app.exceptions import register_exception_handlers + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_db() + yield + await close_db() + + +def create_app() -> FastAPI: + app = FastAPI( + title=settings.api_title, + version=settings.api_version, + lifespan=lifespan, + ) + + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=bool(settings.cors_origins), + allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], + allow_headers=["Authorization", "Content-Type"], + ) + + register_exception_handlers(app) + app.include_router(health.router, prefix="/health", tags=["health"]) + app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) + return app + + +app = create_app() +``` + +Do not use `allow_origins=["*"]` with `allow_credentials=True`; browsers reject that combination and Starlette disallows it for credentialed requests. + +## Pydantic Schemas + +Keep request, update, and response models separate. + +```python +from datetime import datetime +from typing import Annotated +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, EmailStr, Field + + +class UserBase(BaseModel): + email: EmailStr + full_name: Annotated[str, Field(min_length=1, max_length=100)] + + +class UserCreate(UserBase): + password: Annotated[str, Field(min_length=12, max_length=128)] + + +class UserUpdate(BaseModel): + email: EmailStr | None = None + full_name: Annotated[str | None, Field(min_length=1, max_length=100)] = None + + +class UserResponse(UserBase): + model_config = ConfigDict(from_attributes=True) + + id: UUID + created_at: datetime + updated_at: datetime +``` + +Response models must never include password hashes, access tokens, refresh tokens, or internal authorization state. + +## Dependencies + +Use dependency injection for request-scoped resources. + +```python +from collections.abc import AsyncIterator +from uuid import UUID + +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.security import decode_token +from app.db.session import session_factory +from app.models.user import User + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + + +async def get_db() -> AsyncIterator[AsyncSession]: + async with session_factory() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db), +) -> User: + payload = decode_token(token) + user_id = UUID(payload["sub"]) + user = await db.get(User, user_id) + if user is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") + return user +``` + +Avoid creating sessions, clients, or credentials inline inside route handlers. + +## Async Endpoints + +Keep route handlers async when they perform I/O, and use async libraries inside them. + +```python +from fastapi import APIRouter, Depends, Query +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_current_user, get_db +from app.models.user import User +from app.schemas.user import UserResponse + + +router = APIRouter() + + +@router.get("/", response_model=list[UserResponse]) +async def list_users( + limit: int = Query(default=50, ge=1, le=100), + offset: int = Query(default=0, ge=0), + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + result = await db.execute( + select(User).order_by(User.created_at.desc()).limit(limit).offset(offset) + ) + return result.scalars().all() +``` + +Use `httpx.AsyncClient` for external HTTP calls from async handlers. Do not call `requests` in an async route. + +## Error Handling + +Centralize domain exceptions and keep response shapes stable. + +```python +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + + +class ApiError(Exception): + def __init__(self, status_code: int, code: str, message: str): + self.status_code = status_code + self.code = code + self.message = message + + +def register_exception_handlers(app: FastAPI) -> None: + @app.exception_handler(ApiError) + async def api_error_handler(request: Request, exc: ApiError): + return JSONResponse( + status_code=exc.status_code, + content={"error": {"code": exc.code, "message": exc.message}}, + ) +``` + +## OpenAPI Customization + +Assign the custom OpenAPI callable to `app.openapi`; do not just call the function once. + +```python +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi + + +def install_openapi(app: FastAPI) -> None: + def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + app.openapi_schema = get_openapi( + title="Service API", + version="1.0.0", + routes=app.routes, + ) + return app.openapi_schema + + app.openapi = custom_openapi +``` + +## Testing + +Override the dependency used by `Depends`, not an internal helper that route handlers never reference. + +```python +import pytest +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.dependencies import get_db +from app.main import create_app + + +@pytest.fixture +async def client(test_session: AsyncSession): + app = create_app() + + async def override_get_db(): + yield test_session + + app.dependency_overrides[get_db] = override_get_db + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://test", + ) as test_client: + yield test_client + app.dependency_overrides.clear() +``` + +## Security Checklist + +- Hash passwords with `argon2-cffi`, `bcrypt`, or a current passlib-compatible hasher. +- Validate JWT issuer, audience, expiry, and signing algorithm. +- Keep CORS origins environment-specific. +- Put rate limits on auth and write-heavy endpoints. +- Use Pydantic models for all request bodies. +- Use ORM parameter binding or SQLAlchemy Core expressions; never build SQL with f-strings. +- Redact tokens, authorization headers, cookies, and passwords from logs. +- Run dependency audit tooling in CI. + +## Performance Checklist + +- Configure database connection pooling explicitly. +- Add pagination to list endpoints. +- Watch for N+1 queries and use eager loading intentionally. +- Use async HTTP/database clients in async paths. +- Add compression only after checking payload size and CPU tradeoffs. +- Cache stable expensive reads behind explicit invalidation. + +## Examples + +Use these examples as patterns, not as project-wide templates: + +- Application factory: configure middleware and routers once in `create_app`. +- Schema split: `UserCreate`, `UserUpdate`, and `UserResponse` have different responsibilities. +- Dependency override: tests override `get_db` directly. +- OpenAPI customization: assign `app.openapi = custom_openapi`. + +## See Also + +- Agent: `fastapi-reviewer` +- Command: `/fastapi-review` +- Skill: `python-patterns` +- Skill: `python-testing` +- Skill: `api-design`