Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[flake8]
max-line-length = 88
extend-ignore = E203, W503, F403, F401
exclude =
.git,
__pycache__,
build,
dist,
.venv,
env,
venv
28 changes: 14 additions & 14 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main, master]
branches: [ main ]
pull_request:
branches: [main, master]
branches: [ main ]

jobs:
test:
Expand All @@ -30,19 +30,19 @@ jobs:
pip install -r requirements.txt
pip install -r requirements-dev.txt

# - name: Lint with flake8
# run: flake8 .
- name: Lint with flake8
run: flake8 .

# - name: Format check with black
# run: black --check .
- name: Format check with black
run: black --check .

# - name: Type check with mypy
# run: mypy .
- name: Type check with mypy
run: mypy .

# - name: Run tests with coverage
# run: pytest --cov=./ --cov-report=xml --cov-report=term
- name: Run tests with coverage
run: pytest --cov=./ --cov-report=xml --cov-report=term

# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v3
# with:
# files: ./coverage.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ coverage
# Code quality
flake8
black
mypy
mypy
pydantic[mypy]==2.11.7
File renamed without changes.
File renamed without changes.
1 change: 0 additions & 1 deletion syncortexGA/exceptions.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

Empty file added syncortexGA/genetic/__init__.py
Empty file.
Empty file.
Empty file added syncortexGA/models/__init__.py
Empty file.
152 changes: 152 additions & 0 deletions syncortexGA/models/timetable_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from pydantic import BaseModel, model_validator, StringConstraints
from typing import List, Optional, Literal, Annotated

# Accepts only time strings in 24-hour format like "08:00", "23:59"
TimeStr = Annotated[str, StringConstraints(pattern=r"^(?:[01]\d|2[0-3]):[0-5]\d$")]


# ========== TimeSlot ==========
class TimeSlot(BaseModel):
"""Represents a specific time slot on a given day."""

day: Literal["Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday"]
start: TimeStr # Format: "HH:MM"
end: TimeStr # Format: "HH:MM"


# ========== Session Patterns ==========
class FixedSessionPattern(BaseModel):
"""Represents a fixed pattern with exactly 2 weekly sessions."""

slots: List[TimeSlot]

@model_validator(mode="after")
def check_two_slots(cls, model):
if len(model.slots) != 2:
raise ValueError("FixedSessionPattern must have exactly 2 slots")
return model


class AlternatingSessionPattern(BaseModel):
"""
Represents a pattern where a course alternates between two slots weekly.
Paired with another course and occurs on even or odd weeks.
"""

fixed_slot: TimeSlot
alternating_slot: TimeSlot
alternating_mode: Literal["odd", "even"]
paired_course_id: int


class SessionPattern(BaseModel):
"""
Wraps either a fixed or alternating session pattern.
Only one of the patterns must be set based on the type.
"""

type: Literal["fixed", "alternating"]
fixed_pattern: Optional[FixedSessionPattern] = None
alternating_pattern: Optional[AlternatingSessionPattern] = None

@model_validator(mode="after")
def validate_only_one_pattern(cls, model):
if model.type == "fixed":
if not model.fixed_pattern or model.alternating_pattern:
raise ValueError("For type='fixed', only fixed_pattern must be set")
elif model.type == "alternating":
if not model.alternating_pattern or model.fixed_pattern:
raise ValueError(
"For type='alternating', " "only alternating_pattern must be set"
)
return model


# ========== Course ==========
class Course(BaseModel):
"""
Represents a course that may or may not include a lab.
Lab courses must define a lab slot and cannot have a session pattern.
Non-lab courses must define a session pattern and cannot have a lab slot.
"""

id: int
name: str
code: str
instructor_id: int
has_lab: bool = False
lab_slot: Optional[TimeSlot] = None
lab_room_id: Optional[int] = None
lab_instructor_id: Optional[int] = None
session_pattern: Optional[SessionPattern] = None

@model_validator(mode="after")
def validate_course(cls, model):
if model.has_lab:
if not model.lab_slot:
raise ValueError("Lab slot must be defined for lab courses")
if model.session_pattern:
raise ValueError("Lab courses must not have session pattern")
else:
if not model.session_pattern:
raise ValueError("Non-lab courses must have a session pattern")
if model.lab_slot:
raise ValueError("Non-lab courses must not have lab slot")
return model


# ========== Student ==========
class Student(BaseModel):
"""Represents a student belonging to a specific group."""

full_name: str
student_number: str # e.g., "401234567"
group_id: int # Refers to StudentGroup.id


# ========== StudentGroup ==========
class StudentGroup(BaseModel):
"""Represents a group of students (e.g., a class or cohort)."""

id: int
name: str # e.g., "CS-401"
major: str # e.g., "Computer Engineering"
entry_year: int # e.g., 2022


# ========== Instructor ==========
class Instructor(BaseModel):
"""Represents an instructor with available and preferred time slots."""

id: int
full_name: str
available_slots: List[TimeSlot]
preferred_slots: Optional[List[TimeSlot]] = None


# ========== Room ==========
class Room(BaseModel):
"""Represents a classroom or lab with capacity constraints."""

id: int
name: str # e.g., "Room 101"
capacity: int
is_lab: bool = False


# ========== Scheduled Session & Timetable ==========
class ScheduledSession(BaseModel):
"""Represents a scheduled class session (theory or lab)."""

course_id: int
group_id: int
instructor_id: int
room_id: int
slot: TimeSlot
type: Literal["theory", "lab"]


class Timetable(BaseModel):
"""Represents the final schedule as a list of sessions."""

sessions: List[ScheduledSession]
Loading