Spaces:
Paused
Paused
File size: 13,121 Bytes
fb867c3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
#!/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() |