This comprehensive update transforms Job Forge from a generic MVP concept to a production-ready Python/FastAPI web application prototype with complete documentation, testing infrastructure, and deployment procedures. ## 🏗️ Architecture Changes - Updated all documentation to reflect Python/FastAPI + Dash + PostgreSQL stack - Transformed from MVP concept to deployable web application prototype - Added comprehensive multi-tenant architecture with Row Level Security (RLS) - Integrated Claude API and OpenAI API for AI-powered document generation ## 📚 Documentation Overhaul - **CLAUDE.md**: Complete rewrite as project orchestrator for 4 specialized agents - **README.md**: New centralized documentation hub with organized navigation - **API Specification**: Updated with comprehensive FastAPI endpoint documentation - **Database Design**: Enhanced schema with RLS policies and performance optimization - **Architecture Guide**: Transformed to web application focus with deployment strategy ## 🏗️ New Documentation Structure - **docs/development/**: Python/FastAPI coding standards and development guidelines - **docs/infrastructure/**: Docker setup and server deployment procedures - **docs/testing/**: Comprehensive QA procedures with pytest integration - **docs/ai/**: AI prompt templates and examples (preserved from original) ## 🎯 Team Structure Updates - **.claude/agents/**: 4 new Python/FastAPI specialized agents - simplified_technical_lead.md: Architecture and technical guidance - fullstack_developer.md: FastAPI backend + Dash frontend implementation - simplified_qa.md: pytest testing and quality assurance - simplified_devops.md: Docker deployment and server infrastructure ## 🧪 Testing Infrastructure - **pytest.ini**: Complete pytest configuration with coverage requirements - **tests/conftest.py**: Comprehensive test fixtures and database setup - **tests/unit/**: Example unit tests for auth and application services - **tests/integration/**: API integration test examples - Support for async testing, AI service mocking, and database testing ## 🧹 Cleanup - Removed 9 duplicate/outdated documentation files - Eliminated conflicting technology references (Node.js/TypeScript) - Consolidated overlapping content into comprehensive guides - Cleaned up project structure for professional development workflow ## 🚀 Production Ready Features - Docker containerization for development and production - Server deployment procedures for prototype hosting - Security best practices with JWT authentication and RLS - Performance optimization with database indexing and caching - Comprehensive testing strategy with quality gates This update establishes Job Forge as a professional Python/FastAPI web application prototype ready for development and deployment. 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
894 lines
26 KiB
Markdown
894 lines
26 KiB
Markdown
# Coding Standards - Job Forge
|
|
|
|
## Overview
|
|
|
|
This document outlines the coding standards and best practices for the Job Forge Python/FastAPI web application. Following these standards ensures code consistency, maintainability, and quality across the project.
|
|
|
|
## Python Code Style
|
|
|
|
### 1. PEP 8 Compliance
|
|
Job Forge follows [PEP 8](https://pep8.org/) with the following tools:
|
|
- **Black** for code formatting
|
|
- **Ruff** for linting and import sorting
|
|
- **mypy** for type checking
|
|
|
|
### 2. Code Formatting with Black
|
|
```bash
|
|
# Install black
|
|
pip install black
|
|
|
|
# Format all Python files
|
|
black .
|
|
|
|
# Check formatting without making changes
|
|
black --check .
|
|
|
|
# Format specific file
|
|
black app/main.py
|
|
```
|
|
|
|
#### Black Configuration (.pyproject.toml)
|
|
```toml
|
|
[tool.black]
|
|
line-length = 88
|
|
target-version = ['py312']
|
|
include = '\.pyi?$'
|
|
extend-exclude = '''
|
|
/(
|
|
# directories
|
|
\.eggs
|
|
| \.git
|
|
| \.hg
|
|
| \.mypy_cache
|
|
| \.tox
|
|
| \.venv
|
|
| build
|
|
| dist
|
|
)/
|
|
'''
|
|
```
|
|
|
|
### 3. Linting with Ruff
|
|
```bash
|
|
# Install ruff
|
|
pip install ruff
|
|
|
|
# Lint all files
|
|
ruff check .
|
|
|
|
# Fix auto-fixable issues
|
|
ruff check --fix .
|
|
|
|
# Check specific file
|
|
ruff check app/main.py
|
|
```
|
|
|
|
#### Ruff Configuration (.pyproject.toml)
|
|
```toml
|
|
[tool.ruff]
|
|
target-version = "py312"
|
|
line-length = 88
|
|
select = [
|
|
"E", # pycodestyle errors
|
|
"W", # pycodestyle warnings
|
|
"F", # pyflakes
|
|
"I", # isort
|
|
"B", # flake8-bugbear
|
|
"C4", # flake8-comprehensions
|
|
"UP", # pyupgrade
|
|
]
|
|
ignore = [
|
|
"E501", # line too long, handled by black
|
|
"B008", # do not perform function calls in argument defaults
|
|
"C901", # too complex
|
|
]
|
|
exclude = [
|
|
".bzr",
|
|
".direnv",
|
|
".eggs",
|
|
".git",
|
|
".hg",
|
|
".mypy_cache",
|
|
".nox",
|
|
".pants.d",
|
|
".pytype",
|
|
".ruff_cache",
|
|
".svn",
|
|
".tox",
|
|
".venv",
|
|
"__pypackages__",
|
|
"_build",
|
|
"buck-out",
|
|
"build",
|
|
"dist",
|
|
"node_modules",
|
|
"venv",
|
|
]
|
|
|
|
[tool.ruff.mccabe]
|
|
max-complexity = 10
|
|
|
|
[tool.ruff.isort]
|
|
known-first-party = ["app"]
|
|
```
|
|
|
|
### 4. Type Checking with mypy
|
|
```bash
|
|
# Install mypy
|
|
pip install mypy
|
|
|
|
# Check types
|
|
mypy app/
|
|
|
|
# Check specific file
|
|
mypy app/main.py
|
|
```
|
|
|
|
#### mypy Configuration (mypy.ini)
|
|
```ini
|
|
[mypy]
|
|
python_version = 3.12
|
|
warn_return_any = True
|
|
warn_unused_configs = True
|
|
disallow_untyped_defs = True
|
|
disallow_incomplete_defs = True
|
|
check_untyped_defs = True
|
|
disallow_untyped_decorators = True
|
|
no_implicit_optional = True
|
|
warn_redundant_casts = True
|
|
warn_unused_ignores = True
|
|
warn_no_return = True
|
|
warn_unreachable = True
|
|
strict_equality = True
|
|
|
|
[mypy-tests.*]
|
|
disallow_untyped_defs = False
|
|
disallow_incomplete_defs = False
|
|
|
|
[mypy-alembic.*]
|
|
ignore_errors = True
|
|
```
|
|
|
|
## FastAPI Coding Standards
|
|
|
|
### 1. API Endpoint Structure
|
|
```python
|
|
# Good: Clear, consistent endpoint structure
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from typing import List
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.user import User
|
|
from app.schemas.application import ApplicationCreate, ApplicationResponse
|
|
from app.crud.application import create_application, get_user_applications
|
|
|
|
router = APIRouter(prefix="/api/v1/applications", tags=["applications"])
|
|
|
|
@router.post("/", response_model=ApplicationResponse, status_code=status.HTTP_201_CREATED)
|
|
async def create_job_application(
|
|
application_data: ApplicationCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ApplicationResponse:
|
|
"""
|
|
Create a new job application with AI-generated cover letter.
|
|
|
|
Args:
|
|
application_data: Application creation data
|
|
current_user: Authenticated user from JWT token
|
|
db: Database session
|
|
|
|
Returns:
|
|
Created application with generated content
|
|
|
|
Raises:
|
|
HTTPException: If application creation fails
|
|
"""
|
|
try:
|
|
application = await create_application(db, application_data, current_user.id)
|
|
return ApplicationResponse.from_orm(application)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create application: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to create application"
|
|
)
|
|
|
|
@router.get("/", response_model=List[ApplicationResponse])
|
|
async def get_applications(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> List[ApplicationResponse]:
|
|
"""Get user's job applications with pagination."""
|
|
|
|
applications = await get_user_applications(
|
|
db, user_id=current_user.id, skip=skip, limit=limit
|
|
)
|
|
return [ApplicationResponse.from_orm(app) for app in applications]
|
|
```
|
|
|
|
### 2. Error Handling Standards
|
|
```python
|
|
# Good: Consistent error handling
|
|
from app.core.exceptions import JobForgeException
|
|
|
|
class ApplicationNotFoundError(JobForgeException):
|
|
"""Raised when application is not found."""
|
|
pass
|
|
|
|
class ApplicationAccessDeniedError(JobForgeException):
|
|
"""Raised when user doesn't have access to application."""
|
|
pass
|
|
|
|
@router.get("/{application_id}", response_model=ApplicationResponse)
|
|
async def get_application(
|
|
application_id: str,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ApplicationResponse:
|
|
"""Get specific job application by ID."""
|
|
|
|
try:
|
|
application = await get_application_by_id(db, application_id, current_user.id)
|
|
|
|
if not application:
|
|
raise ApplicationNotFoundError(f"Application {application_id} not found")
|
|
|
|
return ApplicationResponse.from_orm(application)
|
|
|
|
except ApplicationNotFoundError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Application not found"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error retrieving application {application_id}: {str(e)}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error"
|
|
)
|
|
```
|
|
|
|
### 3. Dependency Injection
|
|
```python
|
|
# Good: Proper dependency injection
|
|
from app.services.ai.claude_service import ClaudeService
|
|
from app.services.ai.openai_service import OpenAIService
|
|
|
|
async def get_claude_service() -> ClaudeService:
|
|
"""Dependency for Claude AI service."""
|
|
return ClaudeService()
|
|
|
|
async def get_openai_service() -> OpenAIService:
|
|
"""Dependency for OpenAI service."""
|
|
return OpenAIService()
|
|
|
|
@router.post("/{application_id}/generate-cover-letter")
|
|
async def generate_cover_letter(
|
|
application_id: str,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
claude_service: ClaudeService = Depends(get_claude_service),
|
|
) -> dict:
|
|
"""Generate AI cover letter for application."""
|
|
|
|
application = await get_application_by_id(db, application_id, current_user.id)
|
|
if not application:
|
|
raise HTTPException(status_code=404, detail="Application not found")
|
|
|
|
cover_letter = await claude_service.generate_cover_letter(
|
|
user_profile=current_user.profile,
|
|
job_description=application.job_description
|
|
)
|
|
|
|
application.cover_letter = cover_letter
|
|
await db.commit()
|
|
|
|
return {"cover_letter": cover_letter}
|
|
```
|
|
|
|
## Pydantic Model Standards
|
|
|
|
### 1. Schema Definitions
|
|
```python
|
|
# Good: Clear schema definitions with validation
|
|
from pydantic import BaseModel, Field, EmailStr, validator
|
|
from typing import Optional, List
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
class ApplicationStatus(str, Enum):
|
|
"""Application status enumeration."""
|
|
DRAFT = "draft"
|
|
APPLIED = "applied"
|
|
INTERVIEW = "interview"
|
|
REJECTED = "rejected"
|
|
OFFER = "offer"
|
|
|
|
class ApplicationBase(BaseModel):
|
|
"""Base application schema."""
|
|
company_name: str = Field(..., min_length=1, max_length=255, description="Company name")
|
|
role_title: str = Field(..., min_length=1, max_length=255, description="Job role title")
|
|
job_description: Optional[str] = Field(None, max_length=5000, description="Job description")
|
|
status: ApplicationStatus = Field(ApplicationStatus.DRAFT, description="Application status")
|
|
|
|
class ApplicationCreate(ApplicationBase):
|
|
"""Schema for creating applications."""
|
|
|
|
@validator('company_name')
|
|
def validate_company_name(cls, v):
|
|
if not v.strip():
|
|
raise ValueError('Company name cannot be empty')
|
|
return v.strip()
|
|
|
|
@validator('role_title')
|
|
def validate_role_title(cls, v):
|
|
if not v.strip():
|
|
raise ValueError('Role title cannot be empty')
|
|
return v.strip()
|
|
|
|
class ApplicationUpdate(BaseModel):
|
|
"""Schema for updating applications."""
|
|
company_name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
role_title: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
job_description: Optional[str] = Field(None, max_length=5000)
|
|
status: Optional[ApplicationStatus] = None
|
|
|
|
class ApplicationResponse(ApplicationBase):
|
|
"""Schema for application responses."""
|
|
id: str = Field(..., description="Application ID")
|
|
user_id: str = Field(..., description="User ID")
|
|
cover_letter: Optional[str] = Field(None, description="Generated cover letter")
|
|
created_at: datetime = Field(..., description="Creation timestamp")
|
|
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
|
|
class Config:
|
|
from_attributes = True # For SQLAlchemy model conversion
|
|
```
|
|
|
|
### 2. Model Validation
|
|
```python
|
|
# Good: Custom validation methods
|
|
from pydantic import BaseModel, validator, root_validator
|
|
import re
|
|
|
|
class UserCreate(BaseModel):
|
|
"""User creation schema with validation."""
|
|
email: EmailStr
|
|
password: str = Field(..., min_length=8, max_length=128)
|
|
first_name: str = Field(..., min_length=1, max_length=50)
|
|
last_name: str = Field(..., min_length=1, max_length=50)
|
|
|
|
@validator('password')
|
|
def validate_password_strength(cls, v):
|
|
"""Validate password strength."""
|
|
if len(v) < 8:
|
|
raise ValueError('Password must be at least 8 characters long')
|
|
if not re.search(r'[A-Z]', v):
|
|
raise ValueError('Password must contain at least one uppercase letter')
|
|
if not re.search(r'[a-z]', v):
|
|
raise ValueError('Password must contain at least one lowercase letter')
|
|
if not re.search(r'\d', v):
|
|
raise ValueError('Password must contain at least one digit')
|
|
return v
|
|
|
|
@validator('first_name', 'last_name')
|
|
def validate_names(cls, v):
|
|
"""Validate name fields."""
|
|
if not v.strip():
|
|
raise ValueError('Name cannot be empty')
|
|
if not re.match(r'^[a-zA-Z\s\'-]+$', v):
|
|
raise ValueError('Name contains invalid characters')
|
|
return v.strip().title()
|
|
```
|
|
|
|
## Database Model Standards
|
|
|
|
### 1. SQLAlchemy Models
|
|
```python
|
|
# Good: Well-structured SQLAlchemy models
|
|
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Enum
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
from sqlalchemy.orm import relationship
|
|
from sqlalchemy.sql import func
|
|
import uuid
|
|
|
|
from app.core.database import Base
|
|
from app.models.application import ApplicationStatus
|
|
|
|
class User(Base):
|
|
"""User model with proper relationships and constraints."""
|
|
|
|
__tablename__ = "users"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
password_hash = Column(String(255), nullable=False)
|
|
first_name = Column(String(100), nullable=False)
|
|
last_name = Column(String(100), nullable=False)
|
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
|
|
# Relationships
|
|
applications = relationship("Application", back_populates="user", cascade="all, delete-orphan")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<User(id={self.id}, email={self.email})>"
|
|
|
|
class Application(Base):
|
|
"""Application model with RLS and proper indexing."""
|
|
|
|
__tablename__ = "applications"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
|
|
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True)
|
|
company_name = Column(String(255), nullable=False, index=True)
|
|
role_title = Column(String(255), nullable=False)
|
|
job_description = Column(Text)
|
|
cover_letter = Column(Text)
|
|
status = Column(Enum(ApplicationStatus), default=ApplicationStatus.DRAFT, nullable=False, index=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="applications")
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Application(id={self.id}, company={self.company_name}, status={self.status})>"
|
|
```
|
|
|
|
### 2. Database Operations (CRUD)
|
|
```python
|
|
# Good: Async database operations with proper error handling
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, update, delete
|
|
from sqlalchemy.orm import selectinload
|
|
from typing import Optional, List
|
|
|
|
from app.models.application import Application
|
|
from app.schemas.application import ApplicationCreate, ApplicationUpdate
|
|
|
|
async def create_application(
|
|
db: AsyncSession,
|
|
application_data: ApplicationCreate,
|
|
user_id: str
|
|
) -> Application:
|
|
"""Create a new job application."""
|
|
|
|
application = Application(
|
|
user_id=user_id,
|
|
**application_data.dict()
|
|
)
|
|
|
|
db.add(application)
|
|
await db.commit()
|
|
await db.refresh(application)
|
|
|
|
return application
|
|
|
|
async def get_application_by_id(
|
|
db: AsyncSession,
|
|
application_id: str,
|
|
user_id: str
|
|
) -> Optional[Application]:
|
|
"""Get application by ID with user validation."""
|
|
|
|
query = select(Application).where(
|
|
Application.id == application_id,
|
|
Application.user_id == user_id
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def get_user_applications(
|
|
db: AsyncSession,
|
|
user_id: str,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
status_filter: Optional[ApplicationStatus] = None
|
|
) -> List[Application]:
|
|
"""Get user applications with filtering and pagination."""
|
|
|
|
query = select(Application).where(Application.user_id == user_id)
|
|
|
|
if status_filter:
|
|
query = query.where(Application.status == status_filter)
|
|
|
|
query = query.offset(skip).limit(limit).order_by(Application.created_at.desc())
|
|
|
|
result = await db.execute(query)
|
|
return list(result.scalars().all())
|
|
|
|
async def update_application(
|
|
db: AsyncSession,
|
|
application_id: str,
|
|
application_data: ApplicationUpdate,
|
|
user_id: str
|
|
) -> Optional[Application]:
|
|
"""Update application with user validation."""
|
|
|
|
# Update only provided fields
|
|
update_data = application_data.dict(exclude_unset=True)
|
|
if not update_data:
|
|
return None
|
|
|
|
query = (
|
|
update(Application)
|
|
.where(Application.id == application_id, Application.user_id == user_id)
|
|
.values(**update_data)
|
|
.returning(Application)
|
|
)
|
|
|
|
result = await db.execute(query)
|
|
await db.commit()
|
|
|
|
return result.scalar_one_or_none()
|
|
```
|
|
|
|
## Async Programming Standards
|
|
|
|
### 1. Async/Await Usage
|
|
```python
|
|
# Good: Proper async/await usage
|
|
import asyncio
|
|
from typing import List, Optional
|
|
|
|
async def process_multiple_applications(
|
|
applications: List[Application],
|
|
ai_service: ClaudeService
|
|
) -> List[str]:
|
|
"""Process multiple applications concurrently."""
|
|
|
|
async def process_single_application(app: Application) -> str:
|
|
"""Process a single application."""
|
|
if not app.job_description:
|
|
return ""
|
|
|
|
return await ai_service.generate_cover_letter(
|
|
user_profile=app.user.profile,
|
|
job_description=app.job_description
|
|
)
|
|
|
|
# Process applications concurrently
|
|
tasks = [process_single_application(app) for app in applications]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
# Handle exceptions
|
|
cover_letters = []
|
|
for i, result in enumerate(results):
|
|
if isinstance(result, Exception):
|
|
logger.error(f"Failed to process application {applications[i].id}: {result}")
|
|
cover_letters.append("")
|
|
else:
|
|
cover_letters.append(result)
|
|
|
|
return cover_letters
|
|
```
|
|
|
|
### 2. Context Managers
|
|
```python
|
|
# Good: Proper async context manager usage
|
|
from contextlib import asynccontextmanager
|
|
from typing import AsyncGenerator
|
|
|
|
@asynccontextmanager
|
|
async def get_ai_service_with_retry(
|
|
max_retries: int = 3
|
|
) -> AsyncGenerator[ClaudeService, None]:
|
|
"""Context manager for AI service with retry logic."""
|
|
|
|
service = ClaudeService()
|
|
retries = 0
|
|
|
|
try:
|
|
while retries < max_retries:
|
|
try:
|
|
await service.test_connection()
|
|
yield service
|
|
break
|
|
except Exception as e:
|
|
retries += 1
|
|
if retries >= max_retries:
|
|
raise e
|
|
await asyncio.sleep(2 ** retries) # Exponential backoff
|
|
finally:
|
|
await service.close()
|
|
|
|
# Usage
|
|
async def generate_with_retry(job_description: str) -> str:
|
|
async with get_ai_service_with_retry() as ai_service:
|
|
return await ai_service.generate_cover_letter(
|
|
user_profile={},
|
|
job_description=job_description
|
|
)
|
|
```
|
|
|
|
## Testing Standards
|
|
|
|
### 1. Test Structure
|
|
```python
|
|
# Good: Well-structured tests
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
class TestApplicationAPI:
|
|
"""Test suite for application API endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_application_success(
|
|
self,
|
|
async_client: AsyncClient,
|
|
test_user_token: str
|
|
):
|
|
"""Test successful application creation."""
|
|
|
|
# Arrange
|
|
application_data = {
|
|
"company_name": "Test Corp",
|
|
"role_title": "Software Developer",
|
|
"job_description": "Python developer position",
|
|
"status": "draft"
|
|
}
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
|
|
# Act
|
|
response = await async_client.post(
|
|
"/api/v1/applications/",
|
|
json=application_data,
|
|
headers=headers
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["company_name"] == "Test Corp"
|
|
assert data["role_title"] == "Software Developer"
|
|
assert data["status"] == "draft"
|
|
assert "id" in data
|
|
assert "created_at" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_application_with_ai_generation(
|
|
self,
|
|
async_client: AsyncClient,
|
|
test_user_token: str,
|
|
mock_claude_service: AsyncMock
|
|
):
|
|
"""Test application creation with AI cover letter generation."""
|
|
|
|
# Arrange
|
|
mock_claude_service.generate_cover_letter.return_value = "Mock cover letter"
|
|
|
|
application_data = {
|
|
"company_name": "AI Corp",
|
|
"role_title": "ML Engineer",
|
|
"job_description": "Machine learning position with Python",
|
|
"status": "draft"
|
|
}
|
|
headers = {"Authorization": f"Bearer {test_user_token}"}
|
|
|
|
# Act
|
|
with patch('app.services.ai.claude_service.ClaudeService', return_value=mock_claude_service):
|
|
response = await async_client.post(
|
|
"/api/v1/applications/",
|
|
json=application_data,
|
|
headers=headers
|
|
)
|
|
|
|
# Assert
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["cover_letter"] == "Mock cover letter"
|
|
mock_claude_service.generate_cover_letter.assert_called_once()
|
|
```
|
|
|
|
### 2. Test Fixtures
|
|
```python
|
|
# Good: Reusable test fixtures
|
|
import pytest
|
|
from typing import AsyncGenerator
|
|
from httpx import AsyncClient
|
|
from fastapi.testclient import TestClient
|
|
|
|
@pytest.fixture
|
|
async def test_application(
|
|
test_db: AsyncSession,
|
|
test_user: User
|
|
) -> Application:
|
|
"""Create a test application."""
|
|
|
|
from app.crud.application import create_application
|
|
from app.schemas.application import ApplicationCreate
|
|
|
|
app_data = ApplicationCreate(
|
|
company_name="Test Company",
|
|
role_title="Test Role",
|
|
job_description="Test job description",
|
|
status="draft"
|
|
)
|
|
|
|
application = await create_application(test_db, app_data, test_user.id)
|
|
await test_db.commit()
|
|
|
|
return application
|
|
|
|
@pytest.fixture
|
|
def mock_ai_services():
|
|
"""Mock all AI services."""
|
|
|
|
with patch('app.services.ai.claude_service.ClaudeService') as mock_claude, \
|
|
patch('app.services.ai.openai_service.OpenAIService') as mock_openai:
|
|
|
|
mock_claude.return_value.generate_cover_letter = AsyncMock(
|
|
return_value="Mock cover letter"
|
|
)
|
|
mock_openai.return_value.create_embedding = AsyncMock(
|
|
return_value=[0.1] * 1536
|
|
)
|
|
|
|
yield {
|
|
'claude': mock_claude.return_value,
|
|
'openai': mock_openai.return_value
|
|
}
|
|
```
|
|
|
|
## Documentation Standards
|
|
|
|
### 1. Docstring Format
|
|
```python
|
|
# Good: Comprehensive docstrings
|
|
def calculate_job_match_score(
|
|
user_skills: List[str],
|
|
job_requirements: List[str],
|
|
experience_years: int
|
|
) -> float:
|
|
"""
|
|
Calculate job match score based on skills and experience.
|
|
|
|
Args:
|
|
user_skills: List of user's skills
|
|
job_requirements: List of job requirements
|
|
experience_years: Years of relevant experience
|
|
|
|
Returns:
|
|
Match score between 0.0 and 1.0
|
|
|
|
Raises:
|
|
ValueError: If experience_years is negative
|
|
|
|
Example:
|
|
>>> calculate_job_match_score(
|
|
... ["Python", "FastAPI"],
|
|
... ["Python", "Django"],
|
|
... 3
|
|
... )
|
|
0.75
|
|
"""
|
|
if experience_years < 0:
|
|
raise ValueError("Experience years cannot be negative")
|
|
|
|
# Implementation...
|
|
return 0.75
|
|
```
|
|
|
|
### 2. API Documentation
|
|
```python
|
|
# Good: Comprehensive API documentation
|
|
@router.post(
|
|
"/",
|
|
response_model=ApplicationResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create job application",
|
|
description="Create a new job application with optional AI-generated cover letter",
|
|
responses={
|
|
201: {"description": "Application created successfully"},
|
|
400: {"description": "Invalid application data"},
|
|
401: {"description": "Authentication required"},
|
|
422: {"description": "Validation error"},
|
|
500: {"description": "Internal server error"}
|
|
}
|
|
)
|
|
async def create_job_application(
|
|
application_data: ApplicationCreate = Body(
|
|
...,
|
|
example={
|
|
"company_name": "Google",
|
|
"role_title": "Senior Python Developer",
|
|
"job_description": "We are looking for an experienced Python developer...",
|
|
"status": "draft"
|
|
}
|
|
),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> ApplicationResponse:
|
|
"""Create a new job application."""
|
|
# Implementation...
|
|
```
|
|
|
|
## Performance Standards
|
|
|
|
### 1. Database Query Optimization
|
|
```python
|
|
# Good: Optimized database queries
|
|
async def get_applications_with_stats(
|
|
db: AsyncSession,
|
|
user_id: str
|
|
) -> dict:
|
|
"""Get applications with statistics in a single query."""
|
|
|
|
from sqlalchemy import func, case
|
|
|
|
query = select(
|
|
func.count(Application.id).label('total_applications'),
|
|
func.count(case((Application.status == 'applied', 1))).label('applied_count'),
|
|
func.count(case((Application.status == 'interview', 1))).label('interview_count'),
|
|
func.count(case((Application.status == 'offer', 1))).label('offer_count'),
|
|
func.avg(
|
|
case((Application.created_at.isnot(None),
|
|
func.extract('epoch', func.now() - Application.created_at)))
|
|
).label('avg_age_days')
|
|
).where(Application.user_id == user_id)
|
|
|
|
result = await db.execute(query)
|
|
row = result.first()
|
|
|
|
return {
|
|
'total_applications': row.total_applications or 0,
|
|
'applied_count': row.applied_count or 0,
|
|
'interview_count': row.interview_count or 0,
|
|
'offer_count': row.offer_count or 0,
|
|
'avg_age_days': round((row.avg_age_days or 0) / 86400, 1) # Convert to days
|
|
}
|
|
```
|
|
|
|
### 2. Caching Strategies
|
|
```python
|
|
# Good: Implement caching for expensive operations
|
|
from functools import lru_cache
|
|
from typing import Dict, Any
|
|
import asyncio
|
|
|
|
@lru_cache(maxsize=128)
|
|
def get_job_keywords(job_description: str) -> List[str]:
|
|
"""Extract keywords from job description (cached)."""
|
|
# Expensive NLP processing here
|
|
return extract_keywords(job_description)
|
|
|
|
class CachedAIService:
|
|
"""AI service with caching."""
|
|
|
|
def __init__(self):
|
|
self._cache: Dict[str, Any] = {}
|
|
self._cache_ttl = 3600 # 1 hour
|
|
|
|
async def generate_cover_letter_cached(
|
|
self,
|
|
user_profile: dict,
|
|
job_description: str
|
|
) -> str:
|
|
"""Generate cover letter with caching."""
|
|
|
|
cache_key = f"{hash(str(user_profile))}_{hash(job_description)}"
|
|
|
|
if cache_key in self._cache:
|
|
cached_result, timestamp = self._cache[cache_key]
|
|
if time.time() - timestamp < self._cache_ttl:
|
|
return cached_result
|
|
|
|
# Generate new cover letter
|
|
result = await self.generate_cover_letter(user_profile, job_description)
|
|
|
|
# Cache result
|
|
self._cache[cache_key] = (result, time.time())
|
|
|
|
return result
|
|
```
|
|
|
|
These coding standards ensure that Job Forge maintains high code quality, consistency, and performance across all components of the application. |