from datetime import datetime from typing import Optional, List, Dict, Any, Generic, TypeVar from pydantic import BaseModel from uuid import UUID T = TypeVar("T") # Pagination schemas class PaginationMeta(BaseModel): page: int limit: int total: int total_pages: int class PaginatedResponse(BaseModel, Generic[T]): items: List[T] pagination: PaginationMeta # Project schemas class ProjectCreate(BaseModel): name: str description: Optional[str] = None is_public: bool = True class ProjectResponse(BaseModel): id: UUID name: str description: Optional[str] is_public: bool created_at: datetime updated_at: datetime created_by: str class Config: from_attributes = True # Package format and platform enums PACKAGE_FORMATS = ["generic", "npm", "pypi", "docker", "deb", "rpm", "maven", "nuget", "helm"] PACKAGE_PLATFORMS = ["any", "linux", "darwin", "windows", "linux-amd64", "linux-arm64", "darwin-amd64", "darwin-arm64", "windows-amd64"] # Package schemas class PackageCreate(BaseModel): name: str description: Optional[str] = None format: str = "generic" platform: str = "any" class PackageResponse(BaseModel): id: UUID project_id: UUID name: str description: Optional[str] format: str platform: str created_at: datetime updated_at: datetime class Config: from_attributes = True class TagSummary(BaseModel): """Lightweight tag info for embedding in package responses""" name: str artifact_id: str created_at: datetime class PackageDetailResponse(BaseModel): """Package with aggregated metadata""" id: UUID project_id: UUID name: str description: Optional[str] format: str platform: str created_at: datetime updated_at: datetime # Aggregated fields tag_count: int = 0 artifact_count: int = 0 total_size: int = 0 latest_tag: Optional[str] = None latest_upload_at: Optional[datetime] = None # Recent tags (limit 5) recent_tags: List[TagSummary] = [] class Config: from_attributes = True # Artifact schemas class ArtifactResponse(BaseModel): id: str size: int content_type: Optional[str] original_name: Optional[str] created_at: datetime created_by: str ref_count: int format_metadata: Optional[Dict[str, Any]] = None class Config: from_attributes = True # Tag schemas class TagCreate(BaseModel): name: str artifact_id: str class TagResponse(BaseModel): id: UUID package_id: UUID name: str artifact_id: str created_at: datetime created_by: str class Config: from_attributes = True # Upload response class UploadResponse(BaseModel): artifact_id: str size: int project: str package: str tag: Optional[str] format_metadata: Optional[Dict[str, Any]] = None deduplicated: bool = False # Resumable upload schemas class ResumableUploadInitRequest(BaseModel): """Request to initiate a resumable upload""" expected_hash: str # SHA256 hash of the file (client must compute) filename: str content_type: Optional[str] = None size: int tag: Optional[str] = None class ResumableUploadInitResponse(BaseModel): """Response from initiating a resumable upload""" upload_id: Optional[str] # None if file already exists already_exists: bool artifact_id: Optional[str] = None # Set if already_exists is True chunk_size: int # Recommended chunk size for parts class ResumableUploadPartResponse(BaseModel): """Response from uploading a part""" part_number: int etag: str class ResumableUploadCompleteRequest(BaseModel): """Request to complete a resumable upload""" tag: Optional[str] = None class ResumableUploadCompleteResponse(BaseModel): """Response from completing a resumable upload""" artifact_id: str size: int project: str package: str tag: Optional[str] class ResumableUploadStatusResponse(BaseModel): """Status of a resumable upload""" upload_id: str uploaded_parts: List[int] total_uploaded_bytes: int # Consumer schemas class ConsumerResponse(BaseModel): id: UUID package_id: UUID project_url: str last_access: datetime created_at: datetime class Config: from_attributes = True # Health check class HealthResponse(BaseModel): status: str version: str = "1.0.0"