from pydantic import BaseModel, Field, validator from typing import Optional, List, Dict from datetime import date, datetime from enum import Enum import re from models.project import ProjectStatus, ProjectType # Technical Specifications Schemas class DeliveryMovieSpec(BaseModel): """Delivery movie specifications for a department""" resolution: str = Field(..., description="Movie resolution (e.g., '1920x1080', '2048x1080')") format: str = Field(..., description="Movie format (e.g., 'mov', 'mp4', 'exr')") codec: Optional[str] = Field(None, description="Video codec (e.g., 'h264', 'prores', 'dnxhd')") quality: Optional[str] = Field(None, description="Quality setting (e.g., 'high', 'medium', 'low')") @validator('resolution') def validate_resolution(cls, v): if not re.match(r'^\d+x\d+$', v): raise ValueError('Resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")') return v @validator('format') def validate_format(cls, v): allowed_formats = ['mov', 'mp4', 'exr', 'dpx', 'tiff'] if v.lower() not in allowed_formats: raise ValueError(f'Format must be one of: {", ".join(allowed_formats)}') return v.lower() @validator('codec') def validate_codec(cls, v): if v is None: return v allowed_codecs = ['h264', 'h265', 'prores', 'dnxhd', 'dnxhr', 'uncompressed'] if v.lower() not in allowed_codecs: raise ValueError(f'Codec must be one of: {", ".join(allowed_codecs)}') return v.lower() @validator('quality') def validate_quality(cls, v): if v is None: return v allowed_qualities = ['low', 'medium', 'high', 'lossless'] if v.lower() not in allowed_qualities: raise ValueError(f'Quality must be one of: {", ".join(allowed_qualities)}') return v.lower() class ProjectTechnicalSpecs(BaseModel): """Technical specifications for a project""" frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)") data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage") publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery") delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution (e.g., '1920x1080', '4096x2160')") delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field( default_factory=dict, description="Delivery movie resolution and format specifications per department" ) @validator('delivery_image_resolution') def validate_delivery_image_resolution(cls, v): if v is None: return v if not re.match(r'^\d+x\d+$', v): raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")') return v # Default delivery movie specifications per department DEFAULT_DELIVERY_MOVIE_SPECS = { "layout": DeliveryMovieSpec( resolution="1920x1080", format="mov", codec="h264", quality="medium" ), "animation": DeliveryMovieSpec( resolution="1920x1080", format="mov", codec="h264", quality="high" ), "lighting": DeliveryMovieSpec( resolution="2048x1080", format="exr", codec=None, quality="high" ), "composite": DeliveryMovieSpec( resolution="2048x1080", format="mov", codec="prores", quality="high" ) } class ProjectBase(BaseModel): name: str = Field(..., min_length=1, max_length=255) code_name: str = Field(..., min_length=1, max_length=50, description="Unique project code identifier") client_name: str = Field(..., min_length=1, max_length=255, description="Client or studio name") project_type: ProjectType = Field(..., description="Project type: TV, Cinema, or Game") description: Optional[str] = None status: ProjectStatus = ProjectStatus.PLANNING start_date: Optional[date] = None end_date: Optional[date] = None # Technical specifications frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)") data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage") publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery") delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution") delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field( default_factory=dict, description="Delivery movie specifications per department" ) @validator('delivery_image_resolution') def validate_delivery_image_resolution(cls, v): if v is None: return v if not re.match(r'^\d+x\d+$', v): raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")') return v class ProjectCreate(ProjectBase): pass class ProjectUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=255) code_name: Optional[str] = Field(None, min_length=1, max_length=50, description="Unique project code identifier") client_name: Optional[str] = Field(None, min_length=1, max_length=255, description="Client or studio name") project_type: Optional[ProjectType] = Field(None, description="Project type: TV, Cinema, or Game") description: Optional[str] = None status: Optional[ProjectStatus] = None start_date: Optional[date] = None end_date: Optional[date] = None # Technical specifications frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)") data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage") publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery") delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution") delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field( None, description="Delivery movie specifications per department" ) @validator('delivery_image_resolution') def validate_delivery_image_resolution(cls, v): if v is None: return v if not re.match(r'^\d+x\d+$', v): raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")') return v class ProjectMemberBase(BaseModel): user_id: int department_role: Optional[str] = None class ProjectMemberCreate(ProjectMemberBase): pass class ProjectMemberUpdate(BaseModel): department_role: Optional[str] = None class ProjectMemberResponse(ProjectMemberBase): id: int project_id: int joined_at: datetime # User information user_email: str user_first_name: str user_last_name: str user_avatar_url: Optional[str] = None class Config: from_attributes = True class ProjectResponse(ProjectBase): id: int created_at: datetime updated_at: datetime thumbnail_url: Optional[str] = None # Optional member list project_members: Optional[List[ProjectMemberResponse]] = None class Config: from_attributes = True class ProjectListResponse(BaseModel): id: int name: str code_name: str client_name: str project_type: ProjectType description: Optional[str] = None status: ProjectStatus start_date: Optional[date] = None end_date: Optional[date] = None created_at: datetime updated_at: datetime thumbnail_url: Optional[str] = None # Summary information member_count: int = 0 episode_count: int = 0 asset_count: int = 0 class Config: from_attributes = True # Default asset tasks by category DEFAULT_ASSET_TASKS = { "characters": ["modeling", "surfacing", "rigging"], "props": ["modeling", "surfacing"], "sets": ["modeling", "surfacing"], "vehicles": ["modeling", "surfacing", "rigging"] } # Default shot tasks DEFAULT_SHOT_TASKS = ["layout", "animation", "simulation", "lighting", "compositing"] class ProjectSettings(BaseModel): """Project-specific settings for upload location and task templates""" upload_data_location: Optional[str] = Field(None, description="Custom upload storage path for project files") asset_task_templates: Optional[Dict[str, List[str]]] = Field( default_factory=lambda: DEFAULT_ASSET_TASKS.copy(), description="Custom default tasks per asset category" ) shot_task_templates: Optional[List[str]] = Field( default_factory=lambda: DEFAULT_SHOT_TASKS.copy(), description="Custom default tasks for shots" ) enabled_asset_tasks: Optional[Dict[str, List[str]]] = Field( default_factory=dict, description="Enabled/disabled status for asset tasks per category" ) enabled_shot_tasks: Optional[List[str]] = Field( default_factory=lambda: DEFAULT_SHOT_TASKS.copy(), description="Enabled/disabled shot tasks" ) @validator('asset_task_templates') def validate_asset_task_templates(cls, v): if v is None: return DEFAULT_ASSET_TASKS.copy() allowed_categories = ['characters', 'props', 'sets', 'vehicles'] for category in v.keys(): if category not in allowed_categories: raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}') if not isinstance(v[category], list): raise ValueError(f'Task templates for {category} must be a list') return v @validator('shot_task_templates') def validate_shot_task_templates(cls, v): if v is None: return DEFAULT_SHOT_TASKS.copy() if not isinstance(v, list): raise ValueError('Shot task templates must be a list') return v @validator('enabled_asset_tasks') def validate_enabled_asset_tasks(cls, v): if v is None: return {} allowed_categories = ['characters', 'props', 'sets', 'vehicles'] for category in v.keys(): if category not in allowed_categories: raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}') if not isinstance(v[category], list): raise ValueError(f'Enabled tasks for {category} must be a list') return v @validator('enabled_shot_tasks') def validate_enabled_shot_tasks(cls, v): if v is None: return DEFAULT_SHOT_TASKS.copy() if not isinstance(v, list): raise ValueError('Enabled shot tasks must be a list') return v class ProjectSettingsUpdate(BaseModel): """Update project settings""" upload_data_location: Optional[str] = Field(None, description="Custom upload storage path for project files") asset_task_templates: Optional[Dict[str, List[str]]] = Field(None, description="Custom default tasks per asset category") shot_task_templates: Optional[List[str]] = Field(None, description="Custom default tasks for shots") enabled_asset_tasks: Optional[Dict[str, List[str]]] = Field(None, description="Enabled/disabled asset tasks per category") enabled_shot_tasks: Optional[List[str]] = Field(None, description="Enabled/disabled shot tasks") @validator('asset_task_templates') def validate_asset_task_templates(cls, v): if v is None: return v allowed_categories = ['characters', 'props', 'sets', 'vehicles'] for category in v.keys(): if category not in allowed_categories: raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}') if not isinstance(v[category], list): raise ValueError(f'Task templates for {category} must be a list') return v @validator('shot_task_templates') def validate_shot_task_templates(cls, v): if v is None: return v if not isinstance(v, list): raise ValueError('Shot task templates must be a list') return v @validator('enabled_asset_tasks') def validate_enabled_asset_tasks(cls, v): if v is None: return v allowed_categories = ['characters', 'props', 'sets', 'vehicles'] for category in v.keys(): if category not in allowed_categories: raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}') if not isinstance(v[category], list): raise ValueError(f'Enabled tasks for {category} must be a list') return v @validator('enabled_shot_tasks') def validate_enabled_shot_tasks(cls, v): if v is None: return v if not isinstance(v, list): raise ValueError('Enabled shot tasks must be a list') return v