Files
dss/dss/project/models.py

173 lines
6.1 KiB
Python

"""
DSS Project Models
Pydantic models for project configuration and state.
"""
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel, Field, field_validator
class ProjectStatus(str, Enum):
"""Project lifecycle status."""
CREATED = "created"
CONFIGURED = "configured"
SYNCED = "synced"
BUILT = "built"
ERROR = "error"
class FigmaFile(BaseModel):
"""A single Figma file reference."""
key: str = Field(..., description="Figma file key from URL")
name: str = Field(..., description="Human-readable file name")
last_synced: Optional[datetime] = Field(None, description="Last sync timestamp")
thumbnail_url: Optional[str] = Field(None, description="Figma thumbnail URL")
class Config:
json_encoders = {datetime: lambda v: v.isoformat() if v else None}
class FigmaSource(BaseModel):
"""Figma project source configuration.
The team folder is the main Figma resource. Projects within the team
contain design files. The 'uikit' file (if present) is the primary
reference for design tokens.
"""
team_id: Optional[str] = Field(None, description="Figma team ID (main resource)")
project_id: Optional[str] = Field(None, description="Figma project ID within team")
project_name: Optional[str] = Field(None, description="Figma project name")
files: List[FigmaFile] = Field(default_factory=list, description="List of Figma files")
uikit_file_key: Optional[str] = Field(None, description="Key of the UIKit reference file")
auto_sync: bool = Field(False, description="Enable automatic sync on changes")
def add_file(self, key: str, name: str, thumbnail_url: Optional[str] = None) -> FigmaFile:
"""Add a file to the source."""
file = FigmaFile(key=key, name=name, thumbnail_url=thumbnail_url)
# Check for duplicates
if not any(f.key == key for f in self.files):
self.files.append(file)
return file
def get_file(self, key: str) -> Optional[FigmaFile]:
"""Get a file by key."""
for f in self.files:
if f.key == key:
return f
return None
class OutputConfig(BaseModel):
"""Output configuration for generated files."""
tokens_dir: str = Field("./tokens", description="Directory for token files")
themes_dir: str = Field("./themes", description="Directory for theme files")
components_dir: str = Field("./components", description="Directory for component files")
formats: List[str] = Field(
default_factory=lambda: ["css", "scss", "json"],
description="Output formats to generate"
)
@field_validator("formats")
@classmethod
def validate_formats(cls, v):
valid = {"css", "scss", "json", "js", "ts"}
for fmt in v:
if fmt not in valid:
raise ValueError(f"Invalid format: {fmt}. Must be one of {valid}")
return v
class ProjectConfig(BaseModel):
"""Main project configuration (ds.config.json)."""
name: str = Field(..., description="Project name")
version: str = Field("1.0.0", description="Project version")
description: Optional[str] = Field(None, description="Project description")
# Sources
figma: Optional[FigmaSource] = Field(None, description="Figma source configuration")
# Design system settings
skin: Optional[str] = Field(None, description="Base skin/theme to extend (e.g., 'shadcn', 'material')")
base_theme: str = Field("light", description="Default theme variant")
# Output configuration
output: OutputConfig = Field(default_factory=OutputConfig, description="Output settings")
# Metadata
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
class Config:
json_encoders = {datetime: lambda v: v.isoformat() if v else None}
from dss.models.component import Component
class DSSProject(BaseModel):
"""
Complete DSS Project representation.
Combines configuration with runtime state.
"""
config: ProjectConfig = Field(..., description="Project configuration")
path: Path = Field(..., description="Absolute path to project directory")
status: ProjectStatus = Field(ProjectStatus.CREATED, description="Current project status")
# Runtime state
errors: List[str] = Field(default_factory=list, description="Error messages")
warnings: List[str] = Field(default_factory=list, description="Warning messages")
# Extracted data (populated after sync)
extracted_tokens: Optional[Dict[str, Any]] = Field(None, description="Tokens from sources")
components: List[Component] = Field(default_factory=list, description="List of extracted components")
class Config:
arbitrary_types_allowed = True
json_encoders = {
datetime: lambda v: v.isoformat() if v else None,
Path: str,
}
@property
def config_path(self) -> Path:
"""Path to ds.config.json."""
return self.path / "ds.config.json"
@property
def tokens_path(self) -> Path:
"""Path to tokens directory."""
return self.path / self.config.output.tokens_dir
@property
def themes_path(self) -> Path:
"""Path to themes directory."""
return self.path / self.config.output.themes_dir
def to_config_dict(self) -> Dict[str, Any]:
"""Export configuration for saving to ds.config.json."""
return self.config.model_dump(mode="json", exclude_none=True)
@classmethod
def from_config_file(cls, config_path: Path) -> "DSSProject":
"""Load project from ds.config.json file."""
import json
if not config_path.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_path, "r") as f:
config_data = json.load(f)
config = ProjectConfig(**config_data)
project_path = config_path.parent
return cls(
config=config,
path=project_path,
status=ProjectStatus.CONFIGURED,
)