Add Doris Schoolhouse and clean pending homelab changes
This commit is contained in:
139
home/doris-schoolhouse/app/services/d2l.py
Normal file
139
home/doris-schoolhouse/app/services/d2l.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from app.config import settings
|
||||
from app.services.repository import get_repository
|
||||
from app.services.store import JsonStore
|
||||
|
||||
|
||||
class D2LService:
|
||||
def __init__(self) -> None:
|
||||
self.store = JsonStore()
|
||||
self.repo = get_repository()
|
||||
|
||||
def _snapshot_path(self) -> Path:
|
||||
candidate_dirs = [
|
||||
Path(settings.d2l_scraper_workdir),
|
||||
Path('/home/fizzlepoof/.openclaw/workspace/school'),
|
||||
]
|
||||
for workdir in candidate_dirs:
|
||||
for name in ('d2l_current_snapshot.json', 'd2l_grades.json', 'd2l_scraper_output.json'):
|
||||
candidate = workdir / name
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
return candidate_dirs[0] / 'd2l_current_snapshot.json'
|
||||
|
||||
def load_snapshot(self) -> dict[str, Any]:
|
||||
path = self._snapshot_path()
|
||||
if not path.exists():
|
||||
return {"courses": [], "source_path": str(path), "missing": True}
|
||||
data = json.loads(path.read_text())
|
||||
if isinstance(data, dict):
|
||||
data["source_path"] = str(path)
|
||||
return data
|
||||
return {"courses": [], "source_path": str(path), "missing": True}
|
||||
|
||||
def _normalize_courses(self, snapshot: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
items = []
|
||||
for course in snapshot.get("courses", []):
|
||||
items.append({
|
||||
"id": str(course.get("id")),
|
||||
"d2l_course_id": str(course.get("id")),
|
||||
"name": course.get("name"),
|
||||
"code": course.get("code"),
|
||||
"finalSummary": course.get("finalSummary"),
|
||||
"gradeItemCount": course.get("gradeItemCount"),
|
||||
"synced_at": self.store.now(),
|
||||
})
|
||||
return items
|
||||
|
||||
def _normalize_assignments(self, snapshot: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
items = []
|
||||
for course in snapshot.get("courses", []):
|
||||
course_id = str(course.get("id"))
|
||||
for grade_item in course.get("gradeItems", []):
|
||||
if grade_item.get("isCategory"):
|
||||
continue
|
||||
title = grade_item.get("name") or "Untitled"
|
||||
items.append({
|
||||
"id": f"{course_id}:{title.lower().strip()}",
|
||||
"course_id": course_id,
|
||||
"course_name": course.get("name"),
|
||||
"title": title,
|
||||
"points": grade_item.get("points"),
|
||||
"grade": grade_item.get("grade"),
|
||||
"comments": grade_item.get("comments"),
|
||||
"source": "d2l-grade-item",
|
||||
"synced_at": self.store.now(),
|
||||
})
|
||||
return items
|
||||
|
||||
def sync_to_store(self) -> dict[str, Any]:
|
||||
snapshot = self.load_snapshot()
|
||||
courses = self._normalize_courses(snapshot)
|
||||
assignments = self._normalize_assignments(snapshot)
|
||||
existing_assignments = {item.get('id'): item for item in self.repo.list_assignments()}
|
||||
for item in assignments:
|
||||
prior = existing_assignments.get(item['id']) or {}
|
||||
item['status'] = prior.get('status', 'assigned')
|
||||
item['updated_at'] = self.store.now()
|
||||
item['created_at'] = prior.get('created_at', self.store.now())
|
||||
self.repo.replace_courses(courses)
|
||||
self.repo.replace_assignments(assignments)
|
||||
sync_runs = self.repo.list_sync_runs()
|
||||
sync_runs.append({
|
||||
"id": self.store.new_id("sync"),
|
||||
"snapshot_path": snapshot.get("source_path"),
|
||||
"course_count": len(courses),
|
||||
"assignment_count": len(assignments),
|
||||
"completed_at": self.store.now(),
|
||||
"status": "completed",
|
||||
})
|
||||
self.repo.save_sync_runs(sync_runs)
|
||||
return {
|
||||
"status": "ok",
|
||||
"snapshot_path": snapshot.get("source_path"),
|
||||
"course_count": len(courses),
|
||||
"assignment_count": len(assignments),
|
||||
"backend": self.repo.backend,
|
||||
}
|
||||
|
||||
def get_courses(self) -> list[dict[str, Any]]:
|
||||
items = self.repo.list_courses()
|
||||
if items:
|
||||
return items
|
||||
self.sync_to_store()
|
||||
return self.repo.list_courses()
|
||||
|
||||
def get_assignments(self, course_id: str) -> list[dict[str, Any]]:
|
||||
items = self.repo.list_assignments()
|
||||
if not items:
|
||||
self.sync_to_store()
|
||||
items = self.repo.list_assignments()
|
||||
return [item for item in items if str(item.get("course_id")) == str(course_id)]
|
||||
|
||||
def get_grades(self, course_id: str) -> dict[str, Any]:
|
||||
snapshot = self.load_snapshot()
|
||||
for course in snapshot.get("courses", []):
|
||||
if str(course.get("id")) == str(course_id):
|
||||
return {
|
||||
"course_id": str(course.get("id")),
|
||||
"course_name": course.get("name"),
|
||||
"final": course.get("final"),
|
||||
"finalSummary": course.get("finalSummary"),
|
||||
"gradeItems": course.get("gradeItems", []),
|
||||
}
|
||||
return {"course_id": course_id, "gradeItems": []}
|
||||
|
||||
def describe_sync_plan(self) -> dict:
|
||||
snapshot = self.load_snapshot()
|
||||
return {
|
||||
"status": "stub",
|
||||
"backend": self.repo.backend,
|
||||
"scraper_workdir": settings.d2l_scraper_workdir,
|
||||
"scraper_command": settings.d2l_scraper_command,
|
||||
"snapshot_path": snapshot.get("source_path"),
|
||||
"course_count": len(snapshot.get("courses", [])),
|
||||
"message": "Run existing D2L scraper, normalize courses/assignments/grades, then store into Schoolhouse persistence.",
|
||||
}
|
||||
Reference in New Issue
Block a user