"""Database models and operations for instructor system.""" import json from datetime import datetime from typing import Any from sqlalchemy import ( Column, DateTime, Float, Integer, String, Text, create_engine, ) from sqlalchemy.orm import declarative_base, sessionmaker from shared.config import settings from shared.logger import setup_logger logger = setup_logger(__name__) Base = declarative_base() class Task(Base): """Task records sent to students.""" __tablename__ = "tasks" id = Column(Integer, primary_key=True) timestamp = Column(DateTime, default=datetime.utcnow, nullable=False) email = Column(String(255), nullable=False, index=True) task = Column(String(255), nullable=False, index=True) round = Column(Integer, nullable=False) nonce = Column(String(255), nullable=False, unique=True) brief = Column(Text, nullable=False) attachments = Column(Text, nullable=False) # JSON serialized checks = Column(Text, nullable=False) # JSON serialized evaluation_url = Column(String(512), nullable=False) endpoint = Column(String(512), nullable=False) statuscode = Column(Integer, nullable=True) secret = Column(String(255), nullable=False) def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "id": self.id, "timestamp": self.timestamp.isoformat() if self.timestamp else None, "email": self.email, "task": self.task, "round": self.round, "nonce": self.nonce, "brief": self.brief, "attachments": json.loads(self.attachments) if self.attachments else [], "checks": json.loads(self.checks) if self.checks else [], "evaluation_url": self.evaluation_url, "endpoint": self.endpoint, "statuscode": self.statuscode, } class Repo(Base): """Repository submissions from students.""" __tablename__ = "repos" id = Column(Integer, primary_key=True) timestamp = Column(DateTime, default=datetime.utcnow, nullable=False) email = Column(String(255), nullable=False, index=True) task = Column(String(255), nullable=False, index=True) round = Column(Integer, nullable=False) nonce = Column(String(255), nullable=False, unique=True) repo_url = Column(String(512), nullable=False) commit_sha = Column(String(255), nullable=False) pages_url = Column(String(512), nullable=False) def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "id": self.id, "timestamp": self.timestamp.isoformat() if self.timestamp else None, "email": self.email, "task": self.task, "round": self.round, "nonce": self.nonce, "repo_url": self.repo_url, "commit_sha": self.commit_sha, "pages_url": self.pages_url, } class Result(Base): """Evaluation results.""" __tablename__ = "results" id = Column(Integer, primary_key=True) timestamp = Column(DateTime, default=datetime.utcnow, nullable=False) email = Column(String(255), nullable=False, index=True) task = Column(String(255), nullable=False, index=True) round = Column(Integer, nullable=False) repo_url = Column(String(512), nullable=False) commit_sha = Column(String(255), nullable=False) pages_url = Column(String(512), nullable=False) check = Column(String(512), nullable=False) score = Column(Float, nullable=False) reason = Column(Text, nullable=False) logs = Column(Text, nullable=True) def to_dict(self) -> dict[str, Any]: """Convert to dictionary.""" return { "id": self.id, "timestamp": self.timestamp.isoformat() if self.timestamp else None, "email": self.email, "task": self.task, "round": self.round, "repo_url": self.repo_url, "commit_sha": self.commit_sha, "pages_url": self.pages_url, "check": self.check, "score": self.score, "reason": self.reason, "logs": self.logs, } class Database: """Database manager for instructor system.""" def __init__(self, database_url: str | None = None) -> None: """Initialize database connection. Args: database_url: Database URL (uses settings if not provided) """ self.database_url = database_url or settings.database_url self.engine = create_engine(self.database_url, echo=False) self.SessionLocal = sessionmaker(bind=self.engine) logger.info(f"Initialized database: {self.database_url}") def create_tables(self) -> None: """Create all tables.""" Base.metadata.create_all(self.engine) logger.info("Created database tables") def drop_tables(self) -> None: """Drop all tables (use with caution).""" Base.metadata.drop_all(self.engine) logger.warning("Dropped all database tables") def get_session(self): """Get database session.""" return self.SessionLocal() # Task operations def add_task(self, task_data: dict[str, Any]) -> Task: """Add a task record. Args: task_data: Task data dictionary Returns: Created task record """ session = self.get_session() try: task = Task( email=task_data["email"], task=task_data["task"], round=task_data["round"], nonce=task_data["nonce"], brief=task_data["brief"], attachments=json.dumps(task_data.get("attachments", [])), checks=json.dumps(task_data.get("checks", [])), evaluation_url=task_data["evaluation_url"], endpoint=task_data["endpoint"], statuscode=task_data.get("statuscode"), secret=task_data["secret"], ) session.add(task) session.commit() session.refresh(task) logger.info(f"Added task: {task.task}, round {task.round}") return task finally: session.close() def get_task_by_nonce(self, nonce: str) -> Task | None: """Get task by nonce. Args: nonce: Task nonce Returns: Task or None """ session = self.get_session() try: return session.query(Task).filter(Task.nonce == nonce).first() finally: session.close() def task_exists(self, email: str, task: str, round: int) -> bool: """Check if task exists. Args: email: Student email task: Task ID round: Round number Returns: True if exists """ session = self.get_session() try: return ( session.query(Task) .filter(Task.email == email, Task.task == task, Task.round == round) .first() is not None ) finally: session.close() # Repo operations def add_repo(self, repo_data: dict[str, Any]) -> Repo: """Add a repo submission. Args: repo_data: Repo data dictionary Returns: Created repo record """ session = self.get_session() try: repo = Repo(**repo_data) session.add(repo) session.commit() session.refresh(repo) logger.info(f"Added repo: {repo.task}, round {repo.round}") return repo finally: session.close() def get_repos(self, email: str | None = None) -> list[Repo]: """Get repo submissions. Args: email: Filter by email (optional) Returns: List of repos """ session = self.get_session() try: query = session.query(Repo) if email: query = query.filter(Repo.email == email) return query.all() finally: session.close() # Result operations def add_result(self, result_data: dict[str, Any]) -> Result: """Add an evaluation result. Args: result_data: Result data dictionary Returns: Created result record """ session = self.get_session() try: result = Result(**result_data) session.add(result) session.commit() session.refresh(result) logger.debug(f"Added result: {result.task}, {result.check}") return result finally: session.close() def get_results( self, email: str | None = None, task: str | None = None ) -> list[Result]: """Get evaluation results. Args: email: Filter by email (optional) task: Filter by task (optional) Returns: List of results """ session = self.get_session() try: query = session.query(Result) if email: query = query.filter(Result.email == email) if task: query = query.filter(Result.task == task) return query.all() finally: session.close()