- Auto-fixed 4,496 lint issues (import sorting, modern syntax, etc.) - Added ignore rules for patterns intentional in this codebase: E402 (late imports), E712 (SQLAlchemy filters), B904 (raise from), SIM108/SIM105/SIM117 (readability preferences) - Added per-file ignores for tests and scripts - Excluded broken scripts/rename_terminology.py (has curly quotes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
267 lines
9.9 KiB
YAML
267 lines
9.9 KiB
YAML
# Architecture Rules - API Endpoint Rules
|
|
# Rules for app/api/v1/**/*.py files
|
|
|
|
api_endpoint_rules:
|
|
|
|
- id: "API-001"
|
|
name: "Endpoint must use Pydantic models for request/response"
|
|
severity: "error"
|
|
description: |
|
|
All API endpoints must use Pydantic models (BaseModel) for request bodies
|
|
and response models. Never use raw dicts or SQLAlchemy models directly.
|
|
|
|
WHY THIS MATTERS:
|
|
- Type safety: Pydantic validates response structure at runtime
|
|
- Documentation: OpenAPI/Swagger auto-generates accurate docs
|
|
- Contract stability: Schema changes are explicit and reviewable
|
|
- IDE support: Consumers get autocomplete and type hints
|
|
- Prevents bugs: Field name mismatches caught immediately
|
|
|
|
COMMON VIOLATION: Service returns dict, frontend expects different field names.
|
|
Example: Service returns {"total_imports": 5} but frontend expects {"total": 5}.
|
|
With response_model, this mismatch is caught immediately.
|
|
|
|
SCHEMA LOCATION: All response schemas must be defined in models/schema/*.py,
|
|
never inline in endpoint files. This ensures schemas are reusable and discoverable.
|
|
pattern:
|
|
file_pattern:
|
|
- "app/api/v1/**/*.py"
|
|
- "app/modules/*/routes/api/**/*.py"
|
|
anti_patterns:
|
|
- "return dict"
|
|
- "-> dict"
|
|
- "return db_object"
|
|
- "return {"
|
|
example_good: |
|
|
# In models/schema/stats.py
|
|
class ImportStatsResponse(BaseModel):
|
|
total: int
|
|
pending: int
|
|
completed: int
|
|
failed: int
|
|
|
|
# In app/api/v1/admin/marketplace.py
|
|
@router.get("/stats", response_model=ImportStatsResponse)
|
|
def get_import_statistics(db: Session = Depends(get_db)):
|
|
return stats_service.get_import_statistics(db)
|
|
example_bad: |
|
|
# WRONG: No response_model, returns raw dict
|
|
@router.get("/stats")
|
|
def get_import_statistics(db: Session = Depends(get_db)):
|
|
return stats_service.get_import_statistics(db) # Returns dict
|
|
|
|
# WRONG: Schema defined inline in endpoint file
|
|
class MyResponse(BaseModel): # Should be in models/schema/
|
|
...
|
|
|
|
@router.get("/data", response_model=MyResponse)
|
|
def get_data(): ...
|
|
|
|
- id: "API-002"
|
|
name: "No Pydantic imports in API endpoint files"
|
|
severity: "error"
|
|
description: |
|
|
API endpoint files must NOT import Pydantic directly (BaseModel, Field, etc.).
|
|
All Pydantic schemas must be defined in models/schema/*.py and imported from there.
|
|
|
|
WHY THIS MATTERS:
|
|
- Separation of concerns: API handles HTTP, schemas handle data structure
|
|
- Discoverability: All schemas in one place for easy review
|
|
- Reusability: Schemas can be shared across endpoints
|
|
- Prevents inline schema definitions that violate API-001
|
|
|
|
WRONG:
|
|
from pydantic import BaseModel, Field
|
|
class MyRequest(BaseModel): # Inline schema
|
|
name: str
|
|
|
|
RIGHT:
|
|
# In models/schema/my_feature.py
|
|
from pydantic import BaseModel
|
|
class MyRequest(BaseModel):
|
|
name: str
|
|
|
|
# In app/api/v1/admin/my_feature.py
|
|
from models.schema.my_feature import MyRequest
|
|
pattern:
|
|
file_pattern:
|
|
- "app/api/v1/**/*.py"
|
|
- "app/modules/*/routes/api/**/*.py"
|
|
anti_patterns:
|
|
- "from pydantic import"
|
|
- "from pydantic.main import"
|
|
example_good: |
|
|
# In app/api/v1/admin/vendors.py
|
|
from models.schema.vendor import (
|
|
LetzshopExportRequest,
|
|
LetzshopExportResponse,
|
|
)
|
|
|
|
@router.post("/export", response_model=LetzshopExportResponse)
|
|
def export_products(request: LetzshopExportRequest):
|
|
...
|
|
example_bad: |
|
|
# In app/api/v1/admin/vendors.py
|
|
from pydantic import BaseModel # WRONG: Don't import pydantic here
|
|
|
|
class LetzshopExportRequest(BaseModel): # WRONG: Define in models/schema/
|
|
include_inactive: bool = False
|
|
|
|
- id: "API-003"
|
|
name: "Endpoint must NOT contain business logic"
|
|
severity: "error"
|
|
description: |
|
|
API endpoints should only handle HTTP concerns (validation, auth, response formatting).
|
|
All business logic must be delegated to service layer.
|
|
|
|
Transaction control (db.commit) IS allowed at endpoint level - this is the recommended
|
|
pattern for request-scoped transactions. One request = one transaction.
|
|
|
|
What's NOT allowed:
|
|
- db.add() - creating entities is business logic
|
|
- db.query() - complex queries are business logic
|
|
- db.delete() - deleting entities is business logic
|
|
pattern:
|
|
file_pattern:
|
|
- "app/api/v1/**/*.py"
|
|
- "app/modules/*/routes/api/**/*.py"
|
|
anti_patterns:
|
|
- "db.add("
|
|
- "db.delete("
|
|
- "db.query("
|
|
|
|
- id: "API-004"
|
|
name: "Endpoint must NOT raise ANY exceptions directly"
|
|
severity: "error"
|
|
description: |
|
|
API endpoints should NOT raise exceptions directly. Endpoints are a thin
|
|
orchestration layer that:
|
|
1. Accepts request parameters (validated by Pydantic)
|
|
2. Calls dependencies for authentication/authorization (deps.py raises exceptions)
|
|
3. Calls services for business logic (services raise domain exceptions)
|
|
4. Returns response (formatted by Pydantic)
|
|
|
|
Exception raising belongs in:
|
|
- Dependencies (app/api/deps.py) - authentication/authorization validation
|
|
- Services (app/services/) - business logic validation
|
|
|
|
The global exception handler catches all WizamartException subclasses and
|
|
converts them to appropriate HTTP responses.
|
|
|
|
WRONG (endpoint raises exception):
|
|
@router.get("/orders")
|
|
def get_orders(current_user: User = Depends(get_current_vendor_api)):
|
|
if not hasattr(current_user, "token_vendor_id"): # Redundant check
|
|
raise InvalidTokenException("...") # Don't raise here
|
|
return order_service.get_orders(db, current_user.token_vendor_id)
|
|
|
|
RIGHT (dependency guarantees, endpoint trusts):
|
|
@router.get("/orders")
|
|
def get_orders(current_user: User = Depends(get_current_vendor_api)):
|
|
# Dependency guarantees token_vendor_id is present
|
|
return order_service.get_orders(db, current_user.token_vendor_id)
|
|
pattern:
|
|
file_pattern:
|
|
- "app/api/v1/**/*.py"
|
|
- "app/modules/*/routes/api/**/*.py"
|
|
anti_patterns:
|
|
- "raise HTTPException"
|
|
- "raise InvalidTokenException"
|
|
- "raise InsufficientPermissionsException"
|
|
- "if not hasattr\\(current_user.*token_vendor"
|
|
exceptions:
|
|
- "app/exceptions/handler.py"
|
|
|
|
- id: "API-005"
|
|
name: "Endpoint must have proper authentication/authorization"
|
|
severity: "warning"
|
|
description: |
|
|
Protected endpoints must use Depends() for authentication.
|
|
Use get_current_user, get_current_admin, etc.
|
|
|
|
Auto-excluded files:
|
|
- */auth.py - Authentication endpoints (login, logout, register) are intentionally public
|
|
|
|
Public endpoint markers (place on line before or after decorator):
|
|
- # public - Descriptive marker for intentionally unauthenticated endpoints
|
|
- # noqa: API-005 - Standard noqa style to suppress warning
|
|
|
|
Example:
|
|
# public - Stripe webhook receives external callbacks
|
|
@router.post("/webhook/stripe")
|
|
def stripe_webhook(request: Request):
|
|
...
|
|
pattern:
|
|
file_pattern: "app/api/v1/**/*.py"
|
|
required_if_not_public:
|
|
- "Depends(get_current_"
|
|
auto_exclude_files:
|
|
- "*/auth.py"
|
|
public_markers:
|
|
- "# public"
|
|
- "# noqa: api-005"
|
|
|
|
- id: "API-006"
|
|
name: "Multi-tenant endpoints must scope queries to vendor_id"
|
|
severity: "error"
|
|
description: |
|
|
All queries in vendor/shop contexts must filter by vendor_id.
|
|
Use request.state.vendor_id from middleware.
|
|
pattern:
|
|
file_pattern: "app/api/v1/vendor/**/*.py"
|
|
file_pattern: "app/api/v1/storefront/**/*.py"
|
|
discouraged_patterns:
|
|
- "db.query(.*).all()"
|
|
|
|
- id: "API-007"
|
|
name: "API endpoints must NOT import database models directly"
|
|
severity: "error"
|
|
description: |
|
|
API endpoints must follow the layered architecture: Routes → Services → Models.
|
|
Routes should NEVER import database models directly.
|
|
|
|
WHY THIS MATTERS:
|
|
- Layered architecture: Each layer has clear responsibilities
|
|
- Testability: Routes can be tested without database
|
|
- Flexibility: Database schema changes don't affect route signatures
|
|
- Type safety: Schemas define the API contract, not database structure
|
|
|
|
For dependency injection (e.g., current user/customer), use schemas instead
|
|
of database models as return types.
|
|
|
|
WRONG:
|
|
from models.database.customer import Customer
|
|
from models.database.order import Order
|
|
from app.modules.customers.models.customer import Customer
|
|
|
|
@router.get("/orders")
|
|
def get_orders(customer: Customer = Depends(get_current_customer)):
|
|
...
|
|
|
|
RIGHT:
|
|
from app.modules.customers.schemas import CustomerContext
|
|
from app.modules.orders.services import order_service
|
|
|
|
@router.get("/orders")
|
|
def get_orders(customer: CustomerContext = Depends(get_current_customer)):
|
|
return order_service.get_orders(db, customer.id, customer.vendor_id)
|
|
|
|
ALLOWED IMPORTS IN API ROUTES:
|
|
- Schemas: from models.schema.* or from app.modules.*.schemas
|
|
- Services: from app.services.* or from app.modules.*.services
|
|
- Dependencies: from app.api.deps
|
|
- Database session: from app.core.database import get_db
|
|
|
|
NOT ALLOWED:
|
|
- from models.database.*
|
|
- from app.modules.*.models.*
|
|
pattern:
|
|
file_pattern:
|
|
- "app/api/**/*.py"
|
|
- "app/modules/*/routes/api/**/*.py"
|
|
anti_patterns:
|
|
- "from models\\.database\\."
|
|
- "from app\\.modules\\.[a-z_]+\\.models\\."
|
|
exclude_files:
|
|
- "app/api/deps.py" # Dependencies may need model access for queries
|