#!/usr/bin/env python3 """ HuggingFace Spaces configuration validator for Felix Framework. Validates configuration files and deployment readiness. """ import os import sys import json import re from pathlib import Path from typing import Dict, List, Tuple, Any, Optional import ast def validate_app_py() -> Tuple[bool, List[str]]: """Validate app.py for HuggingFace Spaces compatibility.""" issues = [] app_py_path = Path(__file__).parent.parent / "app.py" if not app_py_path.exists(): return False, ["app.py not found - required for HF Spaces deployment"] try: with open(app_py_path, 'r', encoding='utf-8') as f: content = f.read() # Check for required imports required_imports = [ 'gradio', 'spaces', 'torch' ] for import_name in required_imports: if import_name not in content: issues.append(f"Missing required import: {import_name}") # Check for ZeroGPU decorator usage if '@spaces.GPU' not in content: issues.append("No @spaces.GPU decorators found - ZeroGPU features not utilized") # Check for proper main function if 'if __name__ == "__main__"' not in content: issues.append("No main execution block found") # Check for Gradio app launch if 'launch(' not in content and 'queue(' not in content: issues.append("No Gradio app launch found") # Check environment variable handling env_vars = ['HF_TOKEN', 'SPACES_ZERO_GPU'] for var in env_vars: if var not in content: issues.append(f"Environment variable {var} not handled") # Validate Python syntax try: ast.parse(content) except SyntaxError as e: issues.append(f"Syntax error in app.py: {e}") except Exception as e: issues.append(f"Error reading app.py: {e}") return len(issues) == 0, issues def validate_requirements() -> Tuple[bool, List[str]]: """Validate requirements.txt for HF Spaces compatibility.""" issues = [] requirements_path = Path(__file__).parent.parent / "requirements.txt" if not requirements_path.exists(): return False, ["requirements.txt not found"] try: with open(requirements_path, 'r') as f: requirements = f.read() # Required packages for HF Spaces required_packages = [ 'gradio', 'spaces', 'torch', 'transformers', 'numpy', 'plotly' ] for package in required_packages: if package not in requirements.lower(): issues.append(f"Missing required package: {package}") # Check for problematic packages problematic = ['tensorflow', 'jax'] # Memory intensive for package in problematic: if package in requirements.lower(): issues.append(f"Potentially problematic package for ZeroGPU: {package}") # Check version specifications lines = [line.strip() for line in requirements.split('\n') if line.strip()] for line in lines: if line.startswith('#'): continue if '==' in line: package, version = line.split('==', 1) # Check for known incompatible versions if package.strip() == 'gradio' and version.strip().startswith('3.'): issues.append("Gradio version 3.x may not be compatible with latest Spaces features") except Exception as e: issues.append(f"Error reading requirements.txt: {e}") return len(issues) == 0, issues def validate_dockerfile() -> Tuple[bool, List[str]]: """Validate Dockerfile for HF Spaces if present.""" issues = [] dockerfile_path = Path(__file__).parent.parent / "Dockerfile" if not dockerfile_path.exists(): # Dockerfile is optional for HF Spaces return True, [] try: with open(dockerfile_path, 'r') as f: content = f.read() # Check for Python base image if 'FROM python:' not in content and 'FROM pytorch/' not in content: issues.append("No Python base image found in Dockerfile") # Check for port exposure if 'EXPOSE 7860' not in content: issues.append("Port 7860 not exposed (required for HF Spaces)") # Check for requirements installation if 'pip install' not in content and 'requirements.txt' not in content: issues.append("No dependency installation found") # Check for proper entrypoint if 'CMD' not in content and 'ENTRYPOINT' not in content: issues.append("No CMD or ENTRYPOINT specified") except Exception as e: issues.append(f"Error reading Dockerfile: {e}") return len(issues) == 0, issues def validate_readme_header() -> Tuple[bool, List[str]]: """Validate README.md has proper HF Spaces metadata.""" issues = [] readme_path = Path(__file__).parent.parent / "README.md" if not readme_path.exists(): return False, ["README.md not found"] try: with open(readme_path, 'r', encoding='utf-8') as f: content = f.read() # Check for YAML frontmatter if not content.startswith('---'): issues.append("README.md missing YAML frontmatter for HF Spaces") return False, issues # Extract frontmatter parts = content.split('---', 2) if len(parts) < 3: issues.append("Invalid YAML frontmatter structure") return False, issues frontmatter = parts[1] # Required fields for HF Spaces required_fields = ['title', 'emoji', 'colorFrom', 'colorTo', 'sdk'] for field in required_fields: if f'{field}:' not in frontmatter: issues.append(f"Missing required field in frontmatter: {field}") # Check SDK is specified correctly if 'sdk:' in frontmatter: if 'gradio' not in frontmatter and 'docker' not in frontmatter: issues.append("SDK should be 'gradio' or 'docker' for HF Spaces") # Check for app_file if using gradio SDK if 'sdk: gradio' in frontmatter and 'app_file:' not in frontmatter: issues.append("app_file not specified for Gradio SDK") except Exception as e: issues.append(f"Error reading README.md: {e}") return len(issues) == 0, issues def validate_environment_variables() -> Tuple[bool, List[str]]: """Validate environment variable handling.""" issues = [] # Check for .env file (should not be committed) env_file = Path(__file__).parent.parent / ".env" if env_file.exists(): issues.append(".env file found - remove from repository before deployment") # Check .gitignore includes .env gitignore_path = Path(__file__).parent.parent / ".gitignore" if gitignore_path.exists(): try: with open(gitignore_path, 'r') as f: gitignore_content = f.read() if '.env' not in gitignore_content: issues.append(".env not in .gitignore - add to prevent accidental commits") except Exception as e: issues.append(f"Error reading .gitignore: {e}") return len(issues) == 0, issues def validate_file_sizes() -> Tuple[bool, List[str]]: """Check for files that might be too large for HF Spaces.""" issues = [] size_limit_mb = 100 # HF Spaces file size limit project_root = Path(__file__).parent.parent large_files = [] for file_path in project_root.rglob('*'): if file_path.is_file(): try: size_mb = file_path.stat().st_size / (1024 * 1024) if size_mb > size_limit_mb: large_files.append((str(file_path), size_mb)) except Exception: continue if large_files: for file_path, size in large_files: issues.append(f"Large file detected: {file_path} ({size:.1f} MB)") return len(issues) == 0, issues def validate_gpu_memory_usage() -> Tuple[bool, List[str]]: """Validate GPU memory usage patterns in code.""" issues = [] project_root = Path(__file__).parent.parent python_files = list(project_root.glob("**/*.py")) memory_patterns = { 'torch.cuda.empty_cache()': 'Good: GPU memory cleanup', 'torch.cuda.memory_allocated()': 'Good: Memory monitoring', '.cuda()': 'Check: Explicit GPU allocation', 'device="cuda"': 'Check: GPU device specification', 'torch.load': 'Check: Model loading (ensure proper device mapping)' } for file_path in python_files: try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() for pattern, note in memory_patterns.items(): if pattern in content: if note.startswith('Check:'): # Could be issue, manual review needed issues.append(f"{file_path}: {note}") except Exception: continue return len(issues) == 0, issues def generate_deployment_checklist() -> Dict[str, Any]: """Generate a comprehensive deployment checklist.""" checklist = { 'timestamp': str(datetime.now()), 'checks': {}, 'overall_status': True, 'critical_issues': [], 'warnings': [], 'recommendations': [] } checks = [ ('app_py', "app.py validation", validate_app_py), ('requirements', "requirements.txt validation", validate_requirements), ('dockerfile', "Dockerfile validation", validate_dockerfile), ('readme', "README.md validation", validate_readme_header), ('environment', "Environment variables", validate_environment_variables), ('file_sizes', "File size validation", validate_file_sizes), ('gpu_memory', "GPU memory usage", validate_gpu_memory_usage), ] for check_id, check_name, check_func in checks: try: passed, issues = check_func() checklist['checks'][check_id] = { 'name': check_name, 'passed': passed, 'issues': issues } if not passed: checklist['overall_status'] = False if check_id in ['app_py', 'requirements', 'readme']: checklist['critical_issues'].extend(issues) else: checklist['warnings'].extend(issues) except Exception as e: checklist['checks'][check_id] = { 'name': check_name, 'passed': False, 'issues': [f"Check failed with error: {e}"] } checklist['overall_status'] = False checklist['critical_issues'].append(f"{check_name}: {e}") # Add recommendations if checklist['overall_status']: checklist['recommendations'] = [ "āœ… Configuration appears ready for HF Spaces deployment", "Consider testing locally with gradio first", "Monitor GPU memory usage after deployment", "Set up monitoring for deployment health" ] else: checklist['recommendations'] = [ "šŸ”§ Fix critical issues before deployment", "Review HF Spaces documentation for requirements", "Test configuration changes locally", "Consider gradual deployment with feature flags" ] return checklist def main(): """Run HuggingFace Spaces configuration validation.""" print("šŸš€ Validating HuggingFace Spaces configuration for Felix Framework...") checklist = generate_deployment_checklist() # Print results print(f"\nšŸ“Š Validation Results:") print(f"Overall Status: {'āœ… READY' if checklist['overall_status'] else 'āŒ NEEDS FIXES'}") print(f"\nšŸ” Individual Checks:") for check_id, check_info in checklist['checks'].items(): status = "āœ…" if check_info['passed'] else "āŒ" print(f" {status} {check_info['name']}") for issue in check_info['issues']: print(f" - {issue}") if checklist['critical_issues']: print(f"\n🚨 Critical Issues (must fix):") for issue in checklist['critical_issues']: print(f" - {issue}") if checklist['warnings']: print(f"\nāš ļø Warnings:") for warning in checklist['warnings']: print(f" - {warning}") print(f"\nšŸ’” Recommendations:") for rec in checklist['recommendations']: print(f" - {rec}") # Save detailed report report_path = Path(__file__).parent.parent / "hf-spaces-validation-report.json" with open(report_path, 'w') as f: json.dump(checklist, f, indent=2) print(f"\nšŸ“„ Detailed report saved to: {report_path}") # Exit with appropriate code sys.exit(0 if checklist['overall_status'] else 1) if __name__ == "__main__": from datetime import datetime main()