Files
dss/tools/api/services/project_manager.py
Bruno Sarlo d6c25cb4db Simplify code documentation, remove organism terminology
- Remove biological metaphors from docstrings (organism, sensory, genetic, nutrient, etc.)
- Simplify documentation to be minimal and structured for fast model parsing
- Complete SQLite to JSON storage migration (project_manager.py, json_store.py)
- Add Integrations and IntegrationHealth classes to json_store.py
- Add kill_port() function to server.py for port conflict handling
- All 33 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 11:02:00 -03:00

262 lines
7.9 KiB
Python

"""
ProjectManager - Project Registry Service
Manages the server-side registry of projects, including:
- Project registration with path validation
- Root path storage and retrieval
- Project lifecycle management
"""
import os
from pathlib import Path
from typing import Optional, List, Dict, Any
import logging
logger = logging.getLogger(__name__)
class ProjectManager:
"""
Manages project registry with root path validation.
Works with the existing Projects database class to add root_path support.
Validates paths exist and are accessible before registration.
"""
def __init__(self, projects_db, config_service=None):
"""
Initialize project manager.
Args:
projects_db: Projects database class (from storage.database)
config_service: Optional ConfigService for config initialization
"""
self.db = projects_db
self.config_service = config_service
logger.info("ProjectManager initialized")
def register_project(
self,
name: str,
root_path: str,
description: str = "",
figma_file_key: str = ""
) -> Dict[str, Any]:
"""
Register a new project with validated root path.
Args:
name: Human-readable project name
root_path: Absolute path to project directory
description: Optional project description
figma_file_key: Optional Figma file key
Returns:
Created project dict
Raises:
ValueError: If path doesn't exist or isn't a directory
PermissionError: If no write access to path
"""
# Resolve and validate path
root_path = os.path.abspath(root_path)
if not os.path.isdir(root_path):
raise ValueError(f"Path does not exist or is not a directory: {root_path}")
if not os.access(root_path, os.W_OK):
raise PermissionError(f"No write access to path: {root_path}")
# Check if path already registered
existing = self.get_by_path(root_path)
if existing:
raise ValueError(f"Path already registered as project: {existing['name']}")
# Generate project ID
import uuid
project_id = str(uuid.uuid4())[:8]
# Create project in database
project = self.db.create(
id=project_id,
name=name,
description=description,
figma_file_key=figma_file_key
)
# Update with root_path (need to add this column)
self._update_root_path(project_id, root_path)
project['root_path'] = root_path
# Initialize .dss folder and config if config_service available
if self.config_service:
try:
self.config_service.init_config(root_path)
logger.info(f"Initialized .dss config for project {name}")
except Exception as e:
logger.warning(f"Failed to init config for {name}: {e}")
logger.info(f"Registered project: {name} at {root_path}")
return project
def get_project(self, project_id: str) -> Optional[Dict[str, Any]]:
"""
Get project by ID with path validation.
Args:
project_id: Project UUID
Returns:
Project dict or None if not found
Raises:
ValueError: If project path no longer exists
"""
project = self.db.get(project_id)
if not project:
return None
root_path = project.get('root_path')
if root_path and not os.path.isdir(root_path):
logger.warning(f"Project path no longer exists: {root_path}")
# Don't raise, just mark it
project['path_valid'] = False
else:
project['path_valid'] = True
return project
def list_projects(self, status: str = None, valid_only: bool = False) -> List[Dict[str, Any]]:
"""
List all projects with optional filtering.
Args:
status: Filter by status (active, archived, etc.)
valid_only: Only return projects with valid paths
Returns:
List of project dicts
"""
projects = self.db.list(status=status)
# Add path validation status
for project in projects:
root_path = project.get('root_path')
project['path_valid'] = bool(root_path and os.path.isdir(root_path))
if valid_only:
projects = [p for p in projects if p.get('path_valid', False)]
return projects
def get_by_path(self, root_path: str) -> Optional[Dict[str, Any]]:
"""
Find project by root path.
Args:
root_path: Absolute path to search for
Returns:
Project dict or None if not found
"""
root_path = os.path.abspath(root_path)
projects = self.list_projects()
for project in projects:
if project.get('root_path') == root_path:
return project
return None
def update_project(
self,
project_id: str,
name: str = None,
description: str = None,
root_path: str = None,
figma_file_key: str = None,
status: str = None
) -> Optional[Dict[str, Any]]:
"""
Update project fields.
Args:
project_id: Project UUID
name: Optional new name
description: Optional new description
root_path: Optional new root path (validated)
figma_file_key: Optional new Figma key
status: Optional new status
Returns:
Updated project dict or None if not found
"""
project = self.db.get(project_id)
if not project:
return None
# Validate new root_path if provided
if root_path:
root_path = os.path.abspath(root_path)
if not os.path.isdir(root_path):
raise ValueError(f"Path does not exist: {root_path}")
if not os.access(root_path, os.W_OK):
raise PermissionError(f"No write access: {root_path}")
self._update_root_path(project_id, root_path)
# Update other fields via existing update method
updates = {}
if name is not None:
updates['name'] = name
if description is not None:
updates['description'] = description
if figma_file_key is not None:
updates['figma_file_key'] = figma_file_key
if status is not None:
updates['status'] = status
if updates:
self.db.update(project_id, **updates)
return self.get_project(project_id)
def delete_project(self, project_id: str, delete_config: bool = False) -> bool:
"""
Delete a project from registry.
Args:
project_id: Project UUID
delete_config: If True, also delete .dss folder
Returns:
True if deleted, False if not found
"""
project = self.db.get(project_id)
if not project:
return False
if delete_config and project.get('root_path'):
import shutil
dss_path = Path(project['root_path']) / '.dss'
if dss_path.exists():
shutil.rmtree(dss_path)
logger.info(f"Deleted .dss folder at {dss_path}")
self.db.delete(project_id)
logger.info(f"Deleted project: {project_id}")
return True
def _update_root_path(self, project_id: str, root_path: str) -> None:
"""
Update root_path in JSON storage.
"""
self.db.update(project_id, root_path=root_path)
@staticmethod
def ensure_schema():
"""
Legacy schema migration - no longer needed with JSON storage.
Kept for API compatibility.
"""
logger.debug("Schema check: Using JSON storage, no migration needed")