""" Project Template API for WHOOSH - Advanced project template management. """ from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks from pydantic import BaseModel, Field from typing import List, Dict, Optional, Any from datetime import datetime import tempfile import shutil from pathlib import Path from app.services.template_service import ProjectTemplateService from app.services.gitea_service import GiteaService from app.core.auth_deps import get_current_user_context router = APIRouter(prefix="/api/templates", tags=["project-templates"]) # Pydantic models for request/response validation class TemplateInfo(BaseModel): template_id: str name: str description: str icon: str category: str tags: List[str] difficulty: str estimated_setup_time: str features: List[str] tech_stack: Dict[str, List[str]] requirements: Optional[Dict[str, str]] = None class TemplateListResponse(BaseModel): templates: List[TemplateInfo] categories: List[str] total_count: int class TemplateDetailResponse(BaseModel): metadata: TemplateInfo starter_files: Dict[str, str] file_structure: List[str] class ProjectFromTemplateRequest(BaseModel): template_id: str project_name: str = Field(..., min_length=1, max_length=100) project_description: Optional[str] = Field(None, max_length=500) author_name: Optional[str] = Field(None, max_length=100) custom_variables: Optional[Dict[str, str]] = None create_repository: bool = True repository_private: bool = False class ProjectFromTemplateResponse(BaseModel): success: bool project_id: str template_id: str files_created: List[str] repository_url: Optional[str] = None next_steps: List[str] setup_time: str error: Optional[str] = None class TemplateValidationRequest(BaseModel): template_id: str project_variables: Dict[str, str] class TemplateValidationResponse(BaseModel): valid: bool missing_requirements: List[str] warnings: List[str] estimated_size: str def get_template_service(): """Dependency injection for template service.""" return ProjectTemplateService() def get_gitea_service(): """Dependency injection for GITEA service.""" return GiteaService() @router.get("/", response_model=TemplateListResponse) async def list_templates( category: Optional[str] = None, tag: Optional[str] = None, difficulty: Optional[str] = None, template_service: ProjectTemplateService = Depends(get_template_service) ): """List all available project templates with optional filtering.""" try: templates = template_service.list_templates() # Apply filters if category: templates = [t for t in templates if t.get("category") == category] if tag: templates = [t for t in templates if tag in t.get("tags", [])] if difficulty: templates = [t for t in templates if t.get("difficulty") == difficulty] # Extract unique categories for filter options all_templates = template_service.list_templates() categories = list(set(t.get("category", "other") for t in all_templates)) # Convert to response format template_infos = [] for template in templates: template_info = TemplateInfo( template_id=template["template_id"], name=template["name"], description=template["description"], icon=template["icon"], category=template.get("category", "other"), tags=template.get("tags", []), difficulty=template.get("difficulty", "beginner"), estimated_setup_time=template.get("estimated_setup_time", "5-10 minutes"), features=template.get("features", []), tech_stack=template.get("tech_stack", {}), requirements=template.get("requirements") ) template_infos.append(template_info) return TemplateListResponse( templates=template_infos, categories=sorted(categories), total_count=len(template_infos) ) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to list templates: {str(e)}") @router.get("/{template_id}", response_model=TemplateDetailResponse) async def get_template_details( template_id: str, template_service: ProjectTemplateService = Depends(get_template_service) ): """Get detailed information about a specific template including files.""" try: template = template_service.get_template(template_id) if not template: raise HTTPException(status_code=404, detail=f"Template '{template_id}' not found") metadata = template["metadata"] starter_files = template["starter_files"] # Create file structure list file_structure = sorted(starter_files.keys()) template_info = TemplateInfo( template_id=metadata["template_id"], name=metadata["name"], description=metadata["description"], icon=metadata["icon"], category=metadata.get("category", "other"), tags=metadata.get("tags", []), difficulty=metadata.get("difficulty", "beginner"), estimated_setup_time=metadata.get("estimated_setup_time", "5-10 minutes"), features=metadata.get("features", []), tech_stack=metadata.get("tech_stack", {}), requirements=metadata.get("requirements") ) return TemplateDetailResponse( metadata=template_info, starter_files=starter_files, file_structure=file_structure ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get template details: {str(e)}") @router.post("/validate", response_model=TemplateValidationResponse) async def validate_template_setup( request: TemplateValidationRequest, template_service: ProjectTemplateService = Depends(get_template_service) ): """Validate template requirements and project variables before creation.""" try: template = template_service.get_template(request.template_id) if not template: raise HTTPException(status_code=404, detail=f"Template '{request.template_id}' not found") metadata = template["metadata"] requirements = metadata.get("requirements", {}) # Check for missing requirements missing_requirements = [] for req_name, req_version in requirements.items(): # This would check system requirements in a real implementation # For now, we'll simulate the check if req_name in ["docker", "nodejs", "python"]: # Assume these are available pass else: missing_requirements.append(f"{req_name} {req_version}") # Generate warnings warnings = [] if metadata.get("difficulty") == "advanced": warnings.append("This is an advanced template requiring significant setup time") if len(template["starter_files"]) > 50: warnings.append("This template creates many files and may take longer to set up") # Estimate project size total_files = len(template["starter_files"]) if total_files < 10: estimated_size = "Small (< 10 files)" elif total_files < 30: estimated_size = "Medium (10-30 files)" else: estimated_size = "Large (30+ files)" return TemplateValidationResponse( valid=len(missing_requirements) == 0, missing_requirements=missing_requirements, warnings=warnings, estimated_size=estimated_size ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Template validation failed: {str(e)}") @router.post("/create-project", response_model=ProjectFromTemplateResponse) async def create_project_from_template( request: ProjectFromTemplateRequest, background_tasks: BackgroundTasks, current_user: Dict[str, Any] = Depends(get_current_user_context), template_service: ProjectTemplateService = Depends(get_template_service), gitea_service: GiteaService = Depends(get_gitea_service) ): """Create a new project from a template with optional GITEA repository creation.""" start_time = datetime.now() try: # Validate template exists template = template_service.get_template(request.template_id) if not template: raise HTTPException(status_code=404, detail=f"Template '{request.template_id}' not found") # Prepare project variables project_variables = { "project_name": request.project_name, "project_description": request.project_description or "", "author_name": request.author_name or current_user.get("name", "WHOOSH User"), **(request.custom_variables or {}) } # Create temporary directory for project files with tempfile.TemporaryDirectory() as temp_dir: # Generate project from template result = template_service.create_project_from_template( request.template_id, project_variables, temp_dir ) repository_url = None # Create GITEA repository if requested if request.create_repository: try: repo_name = request.project_name.lower().replace(" ", "-").replace("_", "-") repo_info = gitea_service.create_repository( owner="whoosh", # Default organization repo_name=repo_name, description=request.project_description or f"Project created from {template['metadata']['name']} template", private=request.repository_private, auto_init=True ) if repo_info: repository_url = repo_info.get("html_url") # TODO: Upload generated files to repository # This would require git operations to push the template files # to the newly created repository else: # Repository creation failed, but continue with project creation pass except Exception as e: print(f"Warning: Repository creation failed: {e}") # Continue without repository # Calculate setup time setup_time = str(datetime.now() - start_time) # Generate project ID project_id = f"proj_{request.project_name.lower().replace(' ', '_')}_{int(start_time.timestamp())}" # Get next steps from template next_steps = template["metadata"].get("next_steps", [ "Review the generated project structure", "Install dependencies as specified in requirements files", "Configure environment variables", "Run initial setup scripts", "Start development server" ]) # Add repository-specific next steps if repository_url: next_steps.insert(0, f"Clone your repository: git clone {repository_url}") next_steps.append("Commit and push your initial changes") return ProjectFromTemplateResponse( success=True, project_id=project_id, template_id=request.template_id, files_created=result["files_created"], repository_url=repository_url, next_steps=next_steps, setup_time=setup_time ) except HTTPException: raise except Exception as e: setup_time = str(datetime.now() - start_time) return ProjectFromTemplateResponse( success=False, project_id="", template_id=request.template_id, files_created=[], repository_url=None, next_steps=[], setup_time=setup_time, error=str(e) ) @router.get("/categories", response_model=List[str]) async def get_template_categories( template_service: ProjectTemplateService = Depends(get_template_service) ): """Get all available template categories.""" try: templates = template_service.list_templates() categories = list(set(t.get("category", "other") for t in templates)) return sorted(categories) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get categories: {str(e)}") @router.get("/tags", response_model=List[str]) async def get_template_tags( template_service: ProjectTemplateService = Depends(get_template_service) ): """Get all available template tags.""" try: templates = template_service.list_templates() all_tags = [] for template in templates: all_tags.extend(template.get("tags", [])) # Remove duplicates and sort unique_tags = sorted(list(set(all_tags))) return unique_tags except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get tags: {str(e)}") @router.get("/{template_id}/preview", response_model=Dict[str, Any]) async def preview_template_files( template_id: str, file_path: Optional[str] = None, template_service: ProjectTemplateService = Depends(get_template_service) ): """Preview template files or get file structure.""" try: template = template_service.get_template(template_id) if not template: raise HTTPException(status_code=404, detail=f"Template '{template_id}' not found") if file_path: # Return specific file content starter_files = template["starter_files"] if file_path not in starter_files: raise HTTPException(status_code=404, detail=f"File '{file_path}' not found in template") return { "file_path": file_path, "content": starter_files[file_path], "size": len(starter_files[file_path]), "type": "text" if file_path.endswith(('.txt', '.md', '.py', '.js', '.ts', '.json', '.yml', '.yaml')) else "binary" } else: # Return file structure overview starter_files = template["starter_files"] file_structure = {} for file_path in sorted(starter_files.keys()): parts = Path(file_path).parts current = file_structure for part in parts[:-1]: if part not in current: current[part] = {} current = current[part] # Add file with metadata filename = parts[-1] current[filename] = { "type": "file", "size": len(starter_files[file_path]), "extension": Path(file_path).suffix } return { "template_id": template_id, "file_structure": file_structure, "total_files": len(starter_files), "total_size": sum(len(content) for content in starter_files.values()) } except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to preview template: {str(e)}") @router.post("/{template_id}/download") async def download_template_archive( template_id: str, template_service: ProjectTemplateService = Depends(get_template_service) ): """Download template as a ZIP archive.""" try: template = template_service.get_template(template_id) if not template: raise HTTPException(status_code=404, detail=f"Template '{template_id}' not found") # Create temporary ZIP file with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip: import zipfile with zipfile.ZipFile(temp_zip.name, 'w', zipfile.ZIP_DEFLATED) as zf: # Add template metadata zf.writestr("template.json", json.dumps(template["metadata"], indent=2)) # Add all starter files for file_path, content in template["starter_files"].items(): zf.writestr(file_path, content) # Return file for download from fastapi.responses import FileResponse return FileResponse( temp_zip.name, media_type="application/zip", filename=f"{template_id}-template.zip" ) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to download template: {str(e)}") # Template Statistics and Analytics @router.get("/stats/overview") async def get_template_statistics( template_service: ProjectTemplateService = Depends(get_template_service) ): """Get overview statistics about available templates.""" try: templates = template_service.list_templates() # Calculate statistics total_templates = len(templates) categories = {} difficulties = {} tech_stacks = {} for template in templates: # Count categories category = template.get("category", "other") categories[category] = categories.get(category, 0) + 1 # Count difficulties difficulty = template.get("difficulty", "beginner") difficulties[difficulty] = difficulties.get(difficulty, 0) + 1 # Count tech stack components tech_stack = template.get("tech_stack", {}) for category, technologies in tech_stack.items(): for tech in technologies: tech_stacks[tech] = tech_stacks.get(tech, 0) + 1 # Get most popular technologies popular_tech = sorted(tech_stacks.items(), key=lambda x: x[1], reverse=True)[:10] return { "total_templates": total_templates, "categories": categories, "difficulties": difficulties, "popular_technologies": dict(popular_tech), "average_features_per_template": sum(len(t.get("features", [])) for t in templates) / total_templates if templates else 0 } except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to get template statistics: {str(e)}")