""" Course Model - Loads and manages course structure from course.json """ import json from pathlib import Path from typing import Dict, List, Optional from app.config import COURSE_JSON, LESSONS_DIR class Lesson: """Represents a single lesson""" def __init__(self, data: dict, part_id: str, section_id: str, order: int = 0): self.id = data['id'] self.filename = data['filename'] self.title = data['title'] self.estimated_time = data['estimated_time'] self.difficulty = data['difficulty'] self.part_id = part_id self.section_id = section_id self.order = order # Sequential order in course (1-30) self.points = data.get('points', 0) # Points for completion # Construct full path to lesson file section_path = LESSONS_DIR / section_id.replace('-', '_').replace('fundamentals', '01-fundamentals').replace('optimization', '02-optimization').replace('spark_physics', '03-spark-physics').replace('advanced_modeling', '04-advanced-modeling') self.file_path = section_path / self.filename def __repr__(self): return f"" class Section: """Represents a course section (e.g., 'fundamentals')""" def __init__(self, data: dict, part_id: str, lesson_order_start: int = 1): self.id = data['id'] self.title = data['title'] self.path = data['path'] self.description = data['description'] self.part_id = part_id # Load lessons with sequential ordering self.lessons = [] for i, lesson_data in enumerate(data['lessons']): lesson = Lesson(lesson_data, part_id, self.id, lesson_order_start + i) self.lessons.append(lesson) self.exercises = data.get('exercises', []) self.key_concepts = data.get('key_concepts', []) def get_lesson(self, lesson_id: str) -> Optional[Lesson]: """Get lesson by ID""" for lesson in self.lessons: if lesson.id == lesson_id: return lesson return None def __repr__(self): return f"
" class Part: """Represents a course part (e.g., 'Part 1: Fundamentals')""" def __init__(self, data: dict, part_number: int, lesson_order_start: int = 1): self.id = data['id'] self.title = data['title'] self.description = data['description'] self.estimated_time = data['estimated_time'] self.number = part_number # Part number (1-4) # Load sections with sequential lesson ordering self.sections = [] current_order = lesson_order_start for section_data in data['sections']: section = Section(section_data, self.id, current_order) self.sections.append(section) current_order += len(section.lessons) # Create a convenience property for accessing all lessons in this part self.lessons = self.get_all_lessons() def get_lesson(self, lesson_id: str) -> Optional[Lesson]: """Get lesson by ID from any section in this part""" for section in self.sections: lesson = section.get_lesson(lesson_id) if lesson: return lesson return None def get_all_lessons(self) -> List[Lesson]: """Get all lessons in this part""" lessons = [] for section in self.sections: lessons.extend(section.lessons) return lessons def __repr__(self): return f"" class LearningPath: """Represents a learning path (e.g., 'beginner', 'complete')""" def __init__(self, data: dict): self.id = data['id'] self.title = data['title'] self.description = data['description'] self.lessons = data.get('lessons', []) self.skip = data.get('skip', []) def includes_lesson(self, lesson_id: str) -> bool: """Check if lesson is included in this path""" if self.lessons == 'all': return lesson_id not in self.skip return lesson_id in self.lessons def __repr__(self): return f"" class Course: """Main course model - loads and manages entire course structure""" def __init__(self, course_json_path: Path = None): """ Load course from course.json Args: course_json_path: Path to course.json file (default: from config) """ if course_json_path is None: course_json_path = COURSE_JSON self.json_path = course_json_path self._load_course() def _load_course(self): """Load and parse course.json""" try: with open(self.json_path, 'r', encoding='utf-8') as f: data = json.load(f) # Course metadata self.title = data['title'] self.version = data['version'] self.author = data['author'] self.description = data['description'] self.estimated_total_time = data['estimated_total_time'] self.total_lessons = data['total_lessons'] self.total_exercises = data['total_exercises'] self.total_points = data['total_points'] # Prerequisites self.prerequisites_required = data['prerequisites']['required'] self.prerequisites_recommended = data['prerequisites']['recommended'] # Load course structure (4 parts) with sequential numbering self.parts = [] current_order = 1 for i, part_data in enumerate(data['structure']): part = Part(part_data, i + 1, current_order) self.parts.append(part) current_order += len(part.get_all_lessons()) # Reference materials self.reference_materials = data['reference_materials'] # Worked examples self.worked_examples = data['worked_examples'] # Learning paths self.learning_paths = [ LearningPath(path_data) for path_data in data['learning_paths'] ] # Tags self.tags = data.get('tags', {}) # Metadata self.metadata = data.get('metadata', {}) # Build lesson index for quick lookup self._build_lesson_index() print(f"[Course] Loaded: {self.title}") print(f"[Course] {self.total_lessons} lessons across {len(self.parts)} parts") except FileNotFoundError: print(f"[Course ERROR] course.json not found: {self.json_path}") raise except json.JSONDecodeError as e: print(f"[Course ERROR] Invalid JSON: {e}") raise except KeyError as e: print(f"[Course ERROR] Missing required field: {e}") raise def _build_lesson_index(self): """Build index for fast lesson lookup by ID""" self._lesson_index = {} for part in self.parts: for section in part.sections: for lesson in section.lessons: self._lesson_index[lesson.id] = lesson def get_lesson(self, lesson_id: str) -> Optional[Lesson]: """Get lesson by ID (fast lookup)""" return self._lesson_index.get(lesson_id) def get_all_lessons(self) -> List[Lesson]: """Get all lessons in course order""" lessons = [] for part in self.parts: lessons.extend(part.get_all_lessons()) return lessons def get_lesson_by_index(self, index: int) -> Optional[Lesson]: """Get lesson by sequential index (0-29)""" all_lessons = self.get_all_lessons() if 0 <= index < len(all_lessons): return all_lessons[index] return None def get_lesson_index(self, lesson_id: str) -> Optional[int]: """Get sequential index of a lesson (0-29)""" all_lessons = self.get_all_lessons() for i, lesson in enumerate(all_lessons): if lesson.id == lesson_id: return i return None def get_next_lesson(self, lesson_id: str) -> Optional[Lesson]: """Get next lesson in sequence""" index = self.get_lesson_index(lesson_id) if index is not None: return self.get_lesson_by_index(index + 1) return None def get_prev_lesson(self, lesson_id: str) -> Optional[Lesson]: """Get previous lesson in sequence""" index = self.get_lesson_index(lesson_id) if index is not None and index > 0: return self.get_lesson_by_index(index - 1) return None def get_part(self, part_id: str) -> Optional[Part]: """Get part by ID""" for part in self.parts: if part.id == part_id: return part return None def get_learning_path(self, path_id: str) -> Optional[LearningPath]: """Get learning path by ID""" for path in self.learning_paths: if path.id == path_id: return path return None def get_lessons_for_path(self, path_id: str) -> List[Lesson]: """Get all lessons for a specific learning path""" path = self.get_learning_path(path_id) if not path: return [] all_lessons = self.get_all_lessons() if path.lessons == 'all': return [l for l in all_lessons if l.id not in path.skip] else: return [l for l in all_lessons if l.id in path.lessons] def get_lessons_by_tag(self, tag: str) -> List[Lesson]: """Get all lessons with a specific tag""" if tag not in self.tags: return [] lesson_ids = self.tags[tag] return [self.get_lesson(lid) for lid in lesson_ids if self.get_lesson(lid)] def get_part_for_lesson(self, lesson_id: str) -> Optional[Part]: """Get the part that contains a lesson""" for part in self.parts: if part.get_lesson(lesson_id): return part return None def search_lessons(self, query: str) -> List[Lesson]: """Simple search by lesson title""" query = query.lower() results = [] for lesson in self.get_all_lessons(): if query in lesson.title.lower() or query in lesson.id.lower(): results.append(lesson) return results def validate(self) -> bool: """Validate that all lesson files exist""" all_valid = True for lesson in self.get_all_lessons(): if not lesson.file_path.exists(): print(f"[Course WARN] Missing lesson file: {lesson.file_path}") all_valid = False return all_valid def __repr__(self): return f"" # Global course instance _course_instance = None def get_course() -> Course: """Get global course instance (singleton)""" global _course_instance if _course_instance is None: _course_instance = Course() return _course_instance