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 from models.user import DepartmentRole # 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 @validator('delivery_movie_specs_by_department') def validate_delivery_movie_specs_by_department(cls, v): if v is None: return {} allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing'] for dept in v.keys(): if dept not in allowed_departments: raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}') 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 @validator('delivery_movie_specs_by_department') def validate_delivery_movie_specs_by_department(cls, v): if v is None: return {} allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing'] for dept in v.keys(): if dept not in allowed_departments: raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}') 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 @validator('delivery_movie_specs_by_department') def validate_delivery_movie_specs_by_department(cls, v): if v is None: return v allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing'] for dept in v.keys(): if dept not in allowed_departments: raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}') return v class ProjectMemberBase(BaseModel): user_id: int department_role: Optional[DepartmentRole] = None class ProjectMemberCreate(ProjectMemberBase): pass class ProjectMemberUpdate(BaseModel): department_role: Optional[DepartmentRole] = 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 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