#!/usr/bin/env python3 """ Project Structure Validation Script This script validates the final project structure after cleanup, checking for compliance with requirements and best practices. """ import os import json from pathlib import Path from datetime import datetime from typing import Dict, List, Tuple, Any class ProjectStructureValidator: def __init__(self, root_path: str = "."): self.root_path = Path(root_path) self.validation_results = {} self.issues = [] self.recommendations = [] def count_root_items(self) -> Tuple[int, List[str]]: """Count items in root directory, excluding hidden files and directories.""" items = [] hidden_items = [] for item in self.root_path.iterdir(): if item.name.startswith('.'): hidden_items.append(item.name) else: items.append(item.name) return len(items), items, hidden_items def validate_directory_organization(self) -> Dict[str, Any]: """Validate logical directory organization.""" expected_structure = { 'app.py': 'file', # Required at root for HF Spaces 'requirements.txt': 'file', # Required at root for HF Spaces 'README.md': 'file', # Project documentation 'analytics': 'directory', # Core analytics module 'tests': 'directory', # All test files 'docs': 'directory', # Documentation 'scripts': 'directory', # Utility scripts 'archive': 'directory', # Archived files } organization_results = { 'expected_present': {}, 'unexpected_items': [], 'missing_items': [] } # Check for expected items for item_name, item_type in expected_structure.items(): item_path = self.root_path / item_name if item_path.exists(): actual_type = 'directory' if item_path.is_dir() else 'file' organization_results['expected_present'][item_name] = { 'expected_type': item_type, 'actual_type': actual_type, 'correct': actual_type == item_type } else: organization_results['missing_items'].append(item_name) # Check for unexpected items in root _, root_items, _ = self.count_root_items() for item in root_items: if item not in expected_structure: organization_results['unexpected_items'].append(item) return organization_results def validate_file_naming_conventions(self) -> Dict[str, List[str]]: """Validate consistent file naming throughout project.""" naming_issues = { 'non_snake_case_files': [], 'inconsistent_naming': [], 'good_examples': [] } # Check Python files for snake_case naming for py_file in self.root_path.rglob('*.py'): # Skip __init__.py and other special files if py_file.name.startswith('__') and py_file.name.endswith('__'): continue # Check if filename follows snake_case filename = py_file.stem if not self._is_snake_case(filename): relative_path = py_file.relative_to(self.root_path) naming_issues['non_snake_case_files'].append(str(relative_path)) else: relative_path = py_file.relative_to(self.root_path) naming_issues['good_examples'].append(str(relative_path)) # Check directory names for directory in self.root_path.rglob('*'): if directory.is_dir() and not directory.name.startswith('.'): if not self._is_snake_case(directory.name) and directory.name not in ['docs', 'tests', 'scripts', 'archive', 'analytics']: relative_path = directory.relative_to(self.root_path) naming_issues['inconsistent_naming'].append(f"Directory: {relative_path}") return naming_issues def _is_snake_case(self, name: str) -> bool: """Check if a name follows snake_case convention.""" # Allow numbers and underscores, but no uppercase or hyphens return name.islower() and '_' not in name or (name.replace('_', '').replace('-', '').isalnum() and name.islower()) def analyze_directory_contents(self) -> Dict[str, Any]: """Analyze contents of key directories.""" directory_analysis = {} key_directories = ['tests', 'docs', 'scripts', 'analytics', 'archive'] for dir_name in key_directories: dir_path = self.root_path / dir_name if dir_path.exists(): directory_analysis[dir_name] = { 'exists': True, 'file_count': len(list(dir_path.rglob('*'))), 'subdirectories': [d.name for d in dir_path.iterdir() if d.is_dir()], 'python_files': [f.name for f in dir_path.rglob('*.py')], 'other_files': [f.name for f in dir_path.rglob('*') if f.is_file() and not f.name.endswith('.py')] } else: directory_analysis[dir_name] = {'exists': False} return directory_analysis def check_hugging_face_compatibility(self) -> Dict[str, Any]: """Check compatibility with Hugging Face Spaces requirements.""" hf_compatibility = { 'app_py_at_root': (self.root_path / 'app.py').exists(), 'requirements_txt_at_root': (self.root_path / 'requirements.txt').exists(), 'readme_present': (self.root_path / 'README.md').exists(), 'simple_import_structure': True, # Will be validated separately 'issues': [] } # Check for complex nested structures that might cause import issues deep_nesting = [] for path in self.root_path.rglob('*.py'): relative_path = path.relative_to(self.root_path) if len(relative_path.parts) > 3: # More than 3 levels deep deep_nesting.append(str(relative_path)) if deep_nesting: hf_compatibility['simple_import_structure'] = False hf_compatibility['issues'].append(f"Deep nesting found: {deep_nesting}") return hf_compatibility def generate_structure_summary(self) -> Dict[str, Any]: """Generate a summary of the current project structure.""" summary = { 'timestamp': datetime.now().isoformat(), 'root_item_count': 0, 'root_items': [], 'hidden_items': [], 'directory_organization': {}, 'naming_validation': {}, 'directory_contents': {}, 'hf_compatibility': {}, 'overall_score': 0, 'recommendations': [] } # Count root items count, items, hidden = self.count_root_items() summary['root_item_count'] = count summary['root_items'] = sorted(items) summary['hidden_items'] = sorted(hidden) # Validate organization summary['directory_organization'] = self.validate_directory_organization() # Validate naming summary['naming_validation'] = self.validate_file_naming_conventions() # Analyze directory contents summary['directory_contents'] = self.analyze_directory_contents() # Check HF compatibility summary['hf_compatibility'] = self.check_hugging_face_compatibility() # Calculate overall score and recommendations summary['overall_score'] = self._calculate_score(summary) summary['recommendations'] = self._generate_recommendations(summary) return summary def _calculate_score(self, summary: Dict[str, Any]) -> int: """Calculate overall project structure score (0-100).""" score = 100 # Deduct points for too many root items if summary['root_item_count'] > 10: score -= (summary['root_item_count'] - 10) * 5 # Deduct points for missing expected items missing_items = len(summary['directory_organization']['missing_items']) score -= missing_items * 10 # Deduct points for naming issues naming_issues = len(summary['naming_validation']['non_snake_case_files']) score -= naming_issues * 2 # Deduct points for HF compatibility issues if not summary['hf_compatibility']['app_py_at_root']: score -= 20 if not summary['hf_compatibility']['requirements_txt_at_root']: score -= 15 if not summary['hf_compatibility']['simple_import_structure']: score -= 10 return max(0, score) def _generate_recommendations(self, summary: Dict[str, Any]) -> List[str]: """Generate recommendations based on validation results.""" recommendations = [] if summary['root_item_count'] > 10: recommendations.append(f"Reduce root directory items from {summary['root_item_count']} to under 10") if summary['directory_organization']['missing_items']: recommendations.append(f"Add missing expected items: {', '.join(summary['directory_organization']['missing_items'])}") if summary['directory_organization']['unexpected_items']: recommendations.append(f"Consider moving or removing unexpected root items: {', '.join(summary['directory_organization']['unexpected_items'])}") if summary['naming_validation']['non_snake_case_files']: recommendations.append("Rename files to follow snake_case convention") if not summary['hf_compatibility']['app_py_at_root']: recommendations.append("Ensure app.py is at root level for Hugging Face Spaces compatibility") return recommendations def main(): """Main function to run project structure validation.""" validator = ProjectStructureValidator() print("šŸ” Validating Project Structure...") print("=" * 50) # Generate comprehensive summary summary = validator.generate_structure_summary() # Display results print(f"\nšŸ“Š VALIDATION RESULTS") print(f"Overall Score: {summary['overall_score']}/100") print(f"Root Directory Items: {summary['root_item_count']}/10 (target: <10)") print(f"\nšŸ“ Root Directory Contents:") for item in summary['root_items']: print(f" • {item}") if summary['hidden_items']: print(f"\nšŸ”’ Hidden Items (not counted):") for item in summary['hidden_items']: print(f" • {item}") print(f"\nāœ… Expected Items Present:") for item, details in summary['directory_organization']['expected_present'].items(): status = "āœ“" if details['correct'] else "āš ļø" print(f" {status} {item} ({details['actual_type']})") if summary['directory_organization']['missing_items']: print(f"\nāŒ Missing Expected Items:") for item in summary['directory_organization']['missing_items']: print(f" • {item}") if summary['directory_organization']['unexpected_items']: print(f"\nāš ļø Unexpected Root Items:") for item in summary['directory_organization']['unexpected_items']: print(f" • {item}") print(f"\nšŸ—ļø Directory Structure Analysis:") for dir_name, details in summary['directory_contents'].items(): if details['exists']: print(f" šŸ“‚ {dir_name}/") print(f" Files: {details['file_count']}") if details['subdirectories']: print(f" Subdirs: {', '.join(details['subdirectories'])}") print(f"\nšŸš€ Hugging Face Spaces Compatibility:") hf = summary['hf_compatibility'] print(f" app.py at root: {'āœ“' if hf['app_py_at_root'] else 'āŒ'}") print(f" requirements.txt at root: {'āœ“' if hf['requirements_txt_at_root'] else 'āŒ'}") print(f" README.md present: {'āœ“' if hf['readme_present'] else 'āŒ'}") print(f" Simple import structure: {'āœ“' if hf['simple_import_structure'] else 'āŒ'}") if summary['recommendations']: print(f"\nšŸ’” Recommendations:") for i, rec in enumerate(summary['recommendations'], 1): print(f" {i}. {rec}") # Save detailed results to file output_file = "FINAL_STRUCTURE_VALIDATION.json" with open(output_file, 'w') as f: json.dump(summary, f, indent=2) print(f"\nšŸ“„ Detailed results saved to: {output_file}") return summary if __name__ == "__main__": main()