Files
hive/backend/app/api/templates.py
anthonyrawlins 268214d971 Major WHOOSH system refactoring and feature enhancements
- Migrated from HIVE branding to WHOOSH across all components
- Enhanced backend API with new services: AI models, BZZZ integration, templates, members
- Added comprehensive testing suite with security, performance, and integration tests
- Improved frontend with new components for project setup, AI models, and team management
- Updated MCP server implementation with WHOOSH-specific tools and resources
- Enhanced deployment configurations with production-ready Docker setups
- Added comprehensive documentation and setup guides
- Implemented age encryption service and UCXL integration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-27 08:34:48 +10:00

504 lines
19 KiB
Python

"""
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)}")