diff --git a/.gitignore b/.gitignore index f8d187f..ceaeadf 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,7 @@ htmlcov/ **/*~ **/#*# -**/*.json +/*.json !example.json -!tests/fixtures/**/*.json -!fern/fern.config.json **/.DS_Store diff --git a/AGENT.md b/AGENT.md index f2ca96e..f48a5e2 100644 --- a/AGENT.md +++ b/AGENT.md @@ -1,6 +1,7 @@ # Agent guide — Course Constraint Scheduler This file orients coding agents to the repository: how to build, test, and change the right places without guesswork. +If you are a human contributor, start with [README.md](README.md) and [CONTRIBUTING.md](CONTRIBUTING.md) for user-facing and process details. ## What this project is @@ -44,7 +45,7 @@ skills/ # task playbooks (SKILL.md per subfolder) for assistants and ## Conventions that trip people up 1. **`Course` naming**: `scheduler.config` uses `Course` as a **course-id string** type in JSON config. `scheduler.models` defines a **`Course` class** (credits, meetings, etc.). `CourseInstance.course` is the model; use **`.course.course_id`** for the config-style id. (See README “Note on naming”.) -2. **Generated artifacts**: After changing **`server.py`** or API-facing models, refresh **`fern/openapi.json`**. After **`CombinedConfig`** / config models change, refresh **`fern/docs/assets/combined-config.schema.json`**. After public **docstrings** change, refresh **`fern/docs/pages/python/reference.mdx`** — see CONTRIBUTING. +2. **Generated artifacts**: After changing **`src/scheduler/server.py`** or API-facing models, refresh **`fern/openapi.json`**. After **`CombinedConfig`** / config models change, refresh **`fern/docs/assets/combined-config.schema.json`**. After public **docstrings** change, refresh **`fern/docs/pages/python/reference.mdx`** — see CONTRIBUTING. 3. **Style**: **Ruff** is authoritative (`pyproject.toml`: line length **120**, `py312`). CONTRIBUTING matches this; when in doubt follow **`pyproject.toml`**. ## Skills diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d336828..308e9f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,10 +107,10 @@ Configuration lives in `.pre-commit-config.yaml` (prek is compatible with this f ```bash # Test the scheduler -python -m scheduler.main --help +uv run python -m scheduler.main --help # Test the server -python -m scheduler.server --help +uv run python -m scheduler.server --help # Run tests uv run pytest @@ -266,7 +266,8 @@ def generate_schedule(config: SchedulerConfig) -> List[CourseInstance]: - RuntimeError: If no valid schedule can be generated. **Example:** - >>> config = load_config_from_file("config.json") + >>> from scheduler.config import CombinedConfig + >>> config = load_config_from_file(CombinedConfig, "config.json") >>> schedule = generate_schedule(config, limit=5) >>> print(f"Generated {len(schedule)} courses") """ @@ -322,6 +323,7 @@ if not faculty_available: - Update **Fern** pages under **`fern/docs/pages/`** (configuration, welcome, development) - Update README.md for new features - Regenerate **`fern/docs/assets/combined-config.schema.json`** with `uv run python scripts/export_config_schema.py` when `CombinedConfig` changes +- Keep generated Fern artifacts committed when they are used by docs publishing (`fern/openapi.json`, `fern/docs/assets/combined-config.schema.json`, `fern/docs/pages/python/reference.mdx`) - Preview locally: `npm install -g fern-api` then `fern docs dev` (after the generate scripts above) ## Submitting Changes @@ -389,7 +391,7 @@ We use [Semantic Versioning](https://semver.org/): ### Release Steps 1. **Update Version**: Update version in `pyproject.toml` -2. **Changelog**: Update `CHANGELOG.md` with new changes +2. **Release Notes**: Prepare release notes in the GitHub release description (or update `CHANGELOG.md` if your release includes one) 3. **Tag Release**: Create git tag for the version 4. **Build Package**: Build and test the package 5. **Publish**: Publish to PyPI diff --git a/README.md b/README.md index 8d6784b..1230039 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,18 @@ A powerful constraint satisfaction solver for generating academic course schedules using the Z3 theorem prover. +## Start Here by Audience + +- **Coding agents and contributors**: read [AGENT.md](AGENT.md), [CONTRIBUTING.md](CONTRIBUTING.md), and [skills/README.md](skills/README.md). +- **Python API users**: jump to [Python API](#python-api). +- **REST API users**: jump to [REST API](#rest-api). + +### Naming Cheat Sheet + +- **Repository**: `mucsci/scheduler` +- **Package on PyPI**: `course-constraint-scheduler` +- **Python import**: `scheduler` + ## Overview The Course Constraint Scheduler is designed to solve complex academic scheduling problems by modeling them as constraint satisfaction problems. It can handle: @@ -77,16 +89,22 @@ for schedule in scheduler.get_models(): # Start the server with custom options scheduler-server --port 8000 --host 0.0.0.0 --log-level info --workers 16 -# Submit a schedule request +# 1) Submit a schedule request curl -X POST "http://localhost:8000/submit" \ -H "Content-Type: application/json" \ -d @example.json -# Get the next schedule -curl -X POST "http://localhost:8000/schedules/{schedule_id}/next" +# 2) The submit response includes the schedule_id you need +# {"schedule_id":"f9f2...","endpoint":"/schedules/f9f2..."} + +# 3) Use that schedule_id for schedule generation and progress +curl -X POST "http://localhost:8000/schedules/f9f2.../next" +curl -X GET "http://localhost:8000/schedules/f9f2.../count" -# Check generation progress -curl -X GET "http://localhost:8000/schedules/{schedule_id}/count" +# Optional endpoints +curl -X GET "http://localhost:8000/schedules/f9f2.../details" +curl -X GET "http://localhost:8000/schedules/f9f2.../index/0" +curl -X DELETE "http://localhost:8000/schedules/f9f2.../delete" ``` ### Server deployment and security @@ -104,6 +122,12 @@ See [SECURITY.md](SECURITY.md) for how to report vulnerabilities. **Published docs (configuration, Python API, REST API, development):** [https://mucsci-scheduler.docs.buildwithfern.com](https://mucsci-scheduler.docs.buildwithfern.com) +If you are changing code or docs in this repo: + +- Contributor workflow: [CONTRIBUTING.md](CONTRIBUTING.md) +- Agent workflow: [AGENT.md](AGENT.md) +- Skill playbooks: [skills/README.md](skills/README.md) + CI publishes this site on pushes to `main` that touch `fern/`, `scripts/`, or `src/scheduler/` (see `.github/workflows/docs.yml`). The repository needs a **`FERN_TOKEN`** Actions secret from the Fern CLI (`fern token`) or dashboard. ### Local REST API (OpenAPI UI) @@ -127,10 +151,20 @@ uv run python scripts/gen_python_api_mdx.py fern docs dev ``` +Generated artifacts used by Fern: + +- `fern/openapi.json` (from FastAPI routes/models) +- `fern/docs/assets/combined-config.schema.json` (from `CombinedConfig`) +- `fern/docs/pages/python/reference.mdx` (from public docstrings) + +Regenerate these with the scripts above after API/config/docstring changes. + ### Configuration quick link A short pointer to the new docs location: [docs/configuration.md](./docs/configuration.md). +`example.json` is included in this repository for local cloning workflows. If you need a minimal sample, use `tests/fixtures/minimal_config.json`. + ## Configuration The scheduler uses a JSON configuration file that defines: @@ -227,8 +261,8 @@ The scheduler is built with a modular architecture: ```bash # Clone the repository -git clone -cd course-constraint-scheduler +git clone https://github.com/mucsci/scheduler.git +cd scheduler # Install dependencies (includes dev tools via uv default-groups; see pyproject.toml) uv sync diff --git a/docs/configuration.md b/docs/configuration.md index e12bcd9..1eabb5a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,3 +7,9 @@ The configuration documentation now lives on the **Fern** docs site: Source for those pages is in the repository under [`fern/docs/pages/configuration/`](../fern/docs/pages/configuration/). A machine-readable JSON Schema for the full config is at [`fern/docs/assets/combined-config.schema.json`](../fern/docs/assets/combined-config.schema.json). + +If you are working from a fresh checkout and the schema file is missing, regenerate it with: + +```bash +uv run python scripts/export_config_schema.py +``` diff --git a/fern/docs.yml b/fern/docs.yml index be69714..ba38530 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -62,6 +62,10 @@ navigation: path: docs/pages/python/overview.mdx - page: Reference path: docs/pages/python/reference.mdx + - section: REST API + contents: + - page: Integration quickstart + path: docs/pages/rest/quickstart.mdx - api: REST API - section: Development contents: diff --git a/fern/docs/assets/combined-config.schema.json b/fern/docs/assets/combined-config.schema.json new file mode 100644 index 0000000..561f1b3 --- /dev/null +++ b/fern/docs/assets/combined-config.schema.json @@ -0,0 +1,689 @@ +{ + "$defs": { + "ClassPattern": { + "additionalProperties": false, + "description": "Represents a class pattern.\n\n**Usage:**\n```python\nClassPattern(credits=3, meetings=[...])\n```", + "properties": { + "credits": { + "description": "Number of credit hours", + "example": 3, + "title": "Credits", + "type": "integer" + }, + "meetings": { + "description": "List of meeting times", + "example": [ + { + "day": "MON", + "duration": 150, + "lab": false + } + ], + "items": { + "$ref": "#/$defs/Meeting" + }, + "title": "Meetings", + "type": "array" + }, + "disabled": { + "default": false, + "description": "Whether the pattern is disabled", + "title": "Disabled", + "type": "boolean" + }, + "start_time": { + "anyOf": [ + { + "$ref": "#/$defs/TimeString" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Specific start time constraint" + } + }, + "required": [ + "credits", + "meetings" + ], + "title": "ClassPattern", + "type": "object" + }, + "Course": { + "description": "Course name", + "example": "CS 101", + "type": "string" + }, + "CourseConfig": { + "additionalProperties": false, + "description": "Represents a course configuration.\n\n**Usage:**\n```python\nCourseConfig(course_id=\"CS 101\", credits=3, room=[...], lab=[...], conflicts=[], faculty=[])\n```", + "properties": { + "course_id": { + "$ref": "#/$defs/Course", + "description": "Unique identifier for the course" + }, + "credits": { + "description": "Number of credit hours", + "example": 3, + "title": "Credits", + "type": "integer" + }, + "room": { + "description": "List of acceptable room names", + "example": [ + "Room 101" + ], + "items": { + "$ref": "#/$defs/Room" + }, + "title": "Room", + "type": "array" + }, + "lab": { + "description": "List of acceptable lab names", + "example": [ + "Lab 101" + ], + "items": { + "$ref": "#/$defs/Lab" + }, + "title": "Lab", + "type": "array" + }, + "conflicts": { + "description": "List of course IDs that cannot be scheduled simultaneously", + "items": { + "$ref": "#/$defs/Course" + }, + "title": "Conflicts", + "type": "array" + }, + "faculty": { + "description": "List of faculty names", + "example": [ + "Dr. Smith" + ], + "items": { + "$ref": "#/$defs/Faculty" + }, + "title": "Faculty", + "type": "array" + } + }, + "required": [ + "course_id", + "credits", + "room", + "lab", + "conflicts", + "faculty" + ], + "title": "CourseConfig", + "type": "object" + }, + "Day": { + "description": "Day of the week", + "enum": [ + "MON", + "TUE", + "WED", + "THU", + "FRI" + ], + "example": "MON", + "type": "string" + }, + "Faculty": { + "description": "Faculty name", + "example": "Dr. Smith", + "type": "string" + }, + "FacultyConfig": { + "additionalProperties": false, + "description": "Represents a faculty configuration.\n\n**Usage:**\n```python\nFacultyConfig(name=\"Dr. Smith\", maximum_credits=12, minimum_credits=3, ...)\n```", + "properties": { + "name": { + "$ref": "#/$defs/Faculty", + "description": "Faculty member\"s name" + }, + "maximum_credits": { + "description": "Maximum credit hours they can teach", + "example": 12, + "minimum": 0, + "title": "Maximum Credits", + "type": "integer" + }, + "maximum_days": { + "default": 5, + "description": "Maximum number of days they are willing to teach (0-5, optional)", + "example": 3, + "maximum": 5, + "minimum": 0, + "title": "Maximum Days", + "type": "integer" + }, + "minimum_credits": { + "description": "Minimum credit hours they must teach", + "example": 3, + "minimum": 0, + "title": "Minimum Credits", + "type": "integer" + }, + "unique_course_limit": { + "description": "Maximum number of different courses they can teach", + "example": 3, + "exclusiveMinimum": 0, + "title": "Unique Course Limit", + "type": "integer" + }, + "times": { + "additionalProperties": { + "items": { + "$ref": "#/$defs/TimeRange" + }, + "type": "array" + }, + "description": "Dictionary mapping day names to time ranges", + "example": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "propertyNames": { + "$ref": "#/$defs/Day" + }, + "title": "Times", + "type": "object" + }, + "course_preferences": { + "additionalProperties": { + "$ref": "#/$defs/Preference" + }, + "description": "Dictionary mapping course IDs to preference scores", + "example": { + "CS 101": 5 + }, + "propertyNames": { + "$ref": "#/$defs/Course" + }, + "title": "Course Preferences", + "type": "object" + }, + "room_preferences": { + "additionalProperties": { + "$ref": "#/$defs/Preference" + }, + "description": "Dictionary mapping room IDs to preference scores", + "example": { + "Room 101": 5 + }, + "propertyNames": { + "$ref": "#/$defs/Room" + }, + "title": "Room Preferences", + "type": "object" + }, + "lab_preferences": { + "additionalProperties": { + "$ref": "#/$defs/Preference" + }, + "description": "Dictionary mapping lab IDs to preference scores", + "example": { + "Lab 101": 5 + }, + "propertyNames": { + "$ref": "#/$defs/Lab" + }, + "title": "Lab Preferences", + "type": "object" + }, + "mandatory_days": { + "description": "Set of days the faculty must teach on", + "example": [ + "MON", + "WED" + ], + "items": { + "$ref": "#/$defs/Day" + }, + "title": "Mandatory Days", + "type": "array", + "uniqueItems": true + } + }, + "required": [ + "name", + "maximum_credits", + "minimum_credits", + "unique_course_limit", + "times" + ], + "title": "FacultyConfig", + "type": "object" + }, + "Lab": { + "description": "Lab name", + "example": "Lab 101", + "type": "string" + }, + "Meeting": { + "additionalProperties": false, + "description": "Represents a single meeting instance.\n\n**Usage:**\n```python\nMeeting(day=\"MON\", duration=90, lab=False)\n```", + "properties": { + "day": { + "$ref": "#/$defs/Day" + }, + "start_time": { + "anyOf": [ + { + "$ref": "#/$defs/TimeString" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Specific start time constraint" + }, + "duration": { + "description": "Duration of the meeting in minutes", + "example": 150, + "exclusiveMinimum": 0, + "title": "Duration", + "type": "integer" + }, + "lab": { + "default": false, + "description": "Whether the meeting is in a lab", + "title": "Lab", + "type": "boolean" + } + }, + "required": [ + "day", + "duration" + ], + "title": "Meeting", + "type": "object" + }, + "OptimizerFlags": { + "enum": [ + "faculty_course", + "faculty_room", + "faculty_lab", + "same_room", + "same_lab", + "pack_rooms", + "pack_labs" + ], + "title": "OptimizerFlags", + "type": "string" + }, + "Preference": { + "description": "Preference score between 0 and 10", + "example": 5, + "maximum": 10, + "minimum": 0, + "type": "integer" + }, + "Room": { + "description": "Room name", + "example": "Room 101", + "type": "string" + }, + "SchedulerConfig": { + "additionalProperties": false, + "description": "Represents a scheduler configuration.\n\n**Usage:**\n```python\nSchedulerConfig(rooms=[...], labs=[...], courses=[...], faculty=[...])\n```", + "properties": { + "rooms": { + "description": "List of available room names", + "example": [ + "Room 101" + ], + "items": { + "$ref": "#/$defs/Room" + }, + "title": "Rooms", + "type": "array" + }, + "labs": { + "description": "List of available lab names", + "example": [ + "Lab 101" + ], + "items": { + "$ref": "#/$defs/Lab" + }, + "title": "Labs", + "type": "array" + }, + "courses": { + "description": "List of course configurations", + "example": [ + { + "conflicts": [ + "CS 102" + ], + "course_id": "CS 101", + "credits": 3, + "faculty": [ + "Dr. Smith" + ], + "lab": [ + "Lab 101" + ], + "room": [ + "Room 101" + ] + } + ], + "items": { + "$ref": "#/$defs/CourseConfig" + }, + "title": "Courses", + "type": "array" + }, + "faculty": { + "description": "List of faculty configurations", + "example": [ + { + "course_preferences": { + "CS 101": 5 + }, + "lab_preferences": { + "Lab 101": 5 + }, + "mandatory_days": [ + "MON" + ], + "maximum_credits": 12, + "maximum_days": 3, + "minimum_credits": 3, + "name": "Dr. Smith", + "room_preferences": { + "Room 101": 5 + }, + "times": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "unique_course_limit": 3 + } + ], + "items": { + "$ref": "#/$defs/FacultyConfig" + }, + "title": "Faculty", + "type": "array" + } + }, + "required": [ + "rooms", + "labs", + "courses", + "faculty" + ], + "title": "SchedulerConfig", + "type": "object" + }, + "TimeBlock": { + "additionalProperties": false, + "description": "Represents a time block within a day.\n\n**Usage:**\n```python\nTimeBlock(start=\"09:00\", spacing=60, end=\"17:00\")\n```", + "properties": { + "start": { + "$ref": "#/$defs/TimeString", + "description": "Start time of the time block" + }, + "spacing": { + "description": "Time spacing between slots in minutes", + "example": 60, + "exclusiveMinimum": 0, + "title": "Spacing", + "type": "integer" + }, + "end": { + "$ref": "#/$defs/TimeString", + "description": "End time of the time block", + "example": "17:00" + } + }, + "required": [ + "start", + "spacing", + "end" + ], + "title": "TimeBlock", + "type": "object" + }, + "TimeRange": { + "additionalProperties": false, + "description": "A time range with start and end times, ensuring start < end.\n\n**Usage:**\n```python\nTimeRange(start=\"09:00\", end=\"17:00\")\n```", + "properties": { + "start": { + "$ref": "#/$defs/TimeString", + "description": "Start time of the time range" + }, + "end": { + "$ref": "#/$defs/TimeString", + "description": "End time of the time range", + "example": "17:00" + } + }, + "required": [ + "start", + "end" + ], + "title": "TimeRange", + "type": "object" + }, + "TimeSlotConfig": { + "additionalProperties": false, + "description": "Represents a time slot configuration.\n\n**Usage:**\n```python\nTimeSlotConfig(times={...}, classes=[...])\n```", + "properties": { + "times": { + "additionalProperties": { + "items": { + "$ref": "#/$defs/TimeBlock" + }, + "type": "array" + }, + "description": "Dictionary mapping day names to time blocks", + "propertyNames": { + "$ref": "#/$defs/Day" + }, + "title": "Times", + "type": "object" + }, + "classes": { + "description": "List of class patterns", + "items": { + "$ref": "#/$defs/ClassPattern" + }, + "title": "Classes", + "type": "array" + }, + "max_time_gap": { + "default": 30, + "description": "Maximum time gap between time slots to determine if they are adjacent", + "example": 30, + "exclusiveMinimum": 0, + "minimum": 0, + "title": "Max Time Gap", + "type": "integer" + }, + "min_time_overlap": { + "default": 45, + "description": "Minimum overlap between time slots", + "example": 45, + "exclusiveMinimum": 0, + "title": "Min Time Overlap", + "type": "integer" + } + }, + "required": [ + "times", + "classes" + ], + "title": "TimeSlotConfig", + "type": "object" + }, + "TimeString": { + "description": "Time in HH:MM format", + "example": "10:00", + "pattern": "^([0-1][0-9]|2[0-3]):[0-5][0-9]$", + "type": "string" + } + }, + "additionalProperties": false, + "description": "Represents a combined configuration.\n\n**Usage:**\n```python\nCombinedConfig(config=..., time_slot_config=..., limit=10)\n```", + "properties": { + "config": { + "$ref": "#/$defs/SchedulerConfig", + "description": "Scheduler configuration", + "example": { + "courses": [ + { + "conflicts": [], + "course_id": "CS 101", + "credits": 3, + "faculty": [ + "Dr. Smith" + ], + "lab": [ + "Lab 101" + ], + "room": [ + "Room 101" + ] + } + ], + "faculty": [ + { + "course_preferences": { + "CS 101": 5 + }, + "lab_preferences": { + "Lab 101": 5 + }, + "maximum_credits": 12, + "minimum_credits": 3, + "name": "Dr. Smith", + "room_preferences": { + "Room 101": 5 + }, + "times": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "unique_course_limit": 3 + } + ], + "labs": [ + "Lab 101" + ], + "rooms": [ + "Room 101" + ] + } + }, + "time_slot_config": { + "$ref": "#/$defs/TimeSlotConfig", + "description": "Time slot configuration", + "example": { + "classes": [ + { + "credits": 3, + "meetings": [ + { + "day": "MON", + "duration": 150, + "lab": false + } + ] + } + ], + "times": { + "FRI": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "MON": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "THU": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "TUE": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "WED": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ] + } + } + }, + "limit": { + "default": 10, + "description": "Maximum number of schedules to generate", + "example": 10, + "exclusiveMinimum": 0, + "title": "Limit", + "type": "integer" + }, + "optimizer_flags": { + "description": "List of optimizer flags", + "example": [ + "faculty_course", + "faculty_room", + "faculty_lab", + "same_room", + "same_lab", + "pack_rooms", + "pack_labs" + ], + "items": { + "$ref": "#/$defs/OptimizerFlags" + }, + "title": "Optimizer Flags", + "type": "array" + } + }, + "required": [ + "config", + "time_slot_config" + ], + "title": "CombinedConfig", + "type": "object" +} diff --git a/fern/docs/pages/configuration/overview.mdx b/fern/docs/pages/configuration/overview.mdx index 4426e3f..7daa92f 100644 --- a/fern/docs/pages/configuration/overview.mdx +++ b/fern/docs/pages/configuration/overview.mdx @@ -39,6 +39,12 @@ A generated **JSON Schema** for the full document (`CombinedConfig`) is committe You can also fetch it from GitHub: [combined-config.schema.json (main)](https://github.com/mucsci/scheduler/raw/main/fern/docs/assets/combined-config.schema.json) +If you changed config models and need to refresh the schema locally: + +```bash +uv run python scripts/export_config_schema.py +``` + The **REST API** `POST /submit` body matches this shape (same model as `CombinedConfig`). ## Full example diff --git a/fern/docs/pages/python/overview.mdx b/fern/docs/pages/python/overview.mdx index 1e53332..228a02e 100644 --- a/fern/docs/pages/python/overview.mdx +++ b/fern/docs/pages/python/overview.mdx @@ -25,12 +25,25 @@ for schedule in scheduler.get_models(): print(course_instance.as_csv()) ``` +If you are working from this repository, start with `example.json` for a richer scenario or `tests/fixtures/minimal_config.json` for a smaller fixture. + - **`load_config_from_file`** — JSON from disk into any Pydantic model (usually `CombinedConfig`). - **`Scheduler`** — Builds the Z3 problem once; **`get_models()`** is a generator yielding each feasible schedule as a list of **`CourseInstance`** objects. ## Writers -JSON and CSV serializers live under **`scheduler.writers`** (`json_writer`, `csv_writer`). Use them when you need file output beyond ad hoc printing. +JSON and CSV serializers live under **`scheduler.writers`**. + +```python +from scheduler.writers import CSVWriter, JSONWriter + +with JSONWriter("schedules.json") as json_writer, CSVWriter("schedules.csv") as csv_writer: + for schedule in scheduler.get_models(): + json_writer.add_schedule(schedule) + csv_writer.add_schedule(schedule) +``` + +Use writers when you need stable machine-readable output files instead of printing rows directly. ## Naming: two different `Course` concepts diff --git a/fern/docs/pages/python/reference.mdx b/fern/docs/pages/python/reference.mdx index bccae77..25d4506 100644 --- a/fern/docs/pages/python/reference.mdx +++ b/fern/docs/pages/python/reference.mdx @@ -25,10 +25,36 @@ Called by the scheduler CLI and scheduler-server at startup. Not invoked on library import, so applications that embed the scheduler control their own logging configuration. +**Usage:** +```python +from scheduler.logging import configure_logging +configure_logging() +``` + # scheduler.server + + +## TimeInstanceResponse Objects + +```python +class TimeInstanceResponse(BaseModel) +``` + +One meeting time block within a scheduled course (JSON shape). + + + +## CourseInstanceResponse Objects + +```python +class CourseInstanceResponse(BaseModel) +``` + +One course row in a generated schedule (`CourseInstance.model_dump` JSON shape). + ## HealthCheck Objects @@ -39,6 +65,11 @@ class HealthCheck(BaseModel) Health check response model. +**Usage:** +```python +HealthCheck(status="healthy", active_sessions=0) +``` + **Fields:** - status: Health status of the service - active_sessions: Number of active schedule generation sessions @@ -53,6 +84,11 @@ class SubmitResponse(BaseModel) Response model for schedule submission requests. +**Usage:** +```python +SubmitResponse(schedule_id="...", endpoint="/schedules/...") +``` + **Fields:** - schedule_id: Unique identifier for the generated schedule session - endpoint: URL endpoint to access the schedule @@ -67,6 +103,11 @@ class MessageResponse(BaseModel) Generic message response model. +**Usage:** +```python +MessageResponse(message="ok") +``` + **Fields:** - message: Response message text @@ -80,6 +121,11 @@ class GenerateAllResponse(BaseModel) Response model for generate-all schedule requests. +**Usage:** +```python +GenerateAllResponse(message='...', current_count=1, target_count=10) +``` + **Fields:** - message: Status message about the generation process - current_count: Number of schedules already generated @@ -95,9 +141,14 @@ class ScheduleResponse(BaseModel) Response model for schedule retrieval requests. +**Usage:** +```python +ScheduleResponse(schedule_id='...', schedule=[...], index=0, total_generated=1) +``` + **Fields:** - schedule_id: Unique identifier for the schedule session -- schedule: The generated schedule as a list of course instances +- schedule: Generated schedule as `list[CourseInstanceResponse]` (typed JSON rows) - index: Index of this schedule in the generation sequence - total_generated: Total number of schedules generated so far @@ -113,6 +164,11 @@ Response model for schedule details requests. Inherits all fields from CombinedConfig and adds: +**Usage:** +```python +ScheduleDetailsResponse(schedule_id='...', total_generated=0, **combined.model_dump()) +``` + **Fields:** - schedule_id: Unique identifier for the schedule session - total_generated: Total number of schedules generated @@ -127,6 +183,11 @@ class ScheduleCountResponse(BaseModel) Response model for schedule count requests. +**Usage:** +```python +ScheduleCountResponse(schedule_id='...', current_count=2, limit=10, is_complete=False) +``` + **Fields:** - schedule_id: Unique identifier for the schedule session - current_count: Number of schedules currently generated @@ -143,6 +204,11 @@ class ErrorResponse(BaseModel) Error response model for API errors. +**Usage:** +```python +ErrorResponse(error="bad_request", message="...") +``` + **Fields:** - error: Error type or code - message: Detailed error message @@ -158,6 +224,11 @@ class ScheduleSession() Represents an active schedule generation session. +**Usage:** +```python +# Internal session object for the HTTP API +``` + #### cleanup\_session @@ -170,6 +241,9 @@ Remove a session from memory and clean up associated resources. **Args:** - schedule_id: Unique identifier for the schedule session to clean up +```python +cleanup_session(schedule_id) +``` @@ -185,6 +259,9 @@ Ensure the scheduler is initialized for a session. **Args:** - session_id: Unique identifier for the schedule session - session: The ScheduleSession object to initialize +```python +await ensure_scheduler_initialized(session_id, session) +``` @@ -203,6 +280,9 @@ Ensure the generator is initialized for a session. **Raises:** - HTTPException: If generator initialization fails or times out +```python +await ensure_generator_initialized(session_id, session) +``` @@ -215,6 +295,11 @@ async def lifespan(app: FastAPI) Application lifespan manager for cleanup. +**Usage:** +```python +# FastAPI(..., lifespan=lifespan) +``` + #### submit\_schedule @@ -226,6 +311,11 @@ async def submit_schedule(request: SubmitRequest) Submit a new schedule generation request. +**Usage:** +```python +httpx.post("http://localhost:8000/submit", json=body) +``` + #### get\_schedule\_details @@ -238,6 +328,11 @@ async def get_schedule_details(schedule_id: str) Get details about a schedule session. +**Usage:** +```python +httpx.get(f"http://localhost:8000/schedules/{sid}/details") +``` + #### get\_next\_schedule @@ -249,6 +344,11 @@ async def get_next_schedule(schedule_id: str) Get the next generated schedule. +**Usage:** +```python +httpx.post(f"http://localhost:8000/schedules/{sid}/next") +``` + #### generate\_all\_schedules @@ -261,6 +361,11 @@ async def generate_all_schedules(schedule_id: str) Generate all remaining schedules for a session asynchronously. +**Usage:** +```python +httpx.post(f"http://localhost:8000/schedules/{sid}/generate_all") +``` + #### get\_schedule\_count @@ -273,6 +378,11 @@ async def get_schedule_count(schedule_id: str) Get the current count of generated schedules for a session. +**Usage:** +```python +httpx.get(f"http://localhost:8000/schedules/{sid}/count") +``` + #### get\_schedule\_by\_index @@ -285,6 +395,11 @@ async def get_schedule_by_index(schedule_id: str, index: int) Get a previously generated schedule by index. +**Usage:** +```python +httpx.get(f"http://localhost:8000/schedules/{sid}/index/0") +``` + #### delete\_schedule\_session @@ -297,6 +412,11 @@ async def delete_schedule_session(schedule_id: str, Delete a schedule session. +**Usage:** +```python +httpx.delete(f"http://localhost:8000/schedules/{sid}/delete") +``` + #### cleanup\_schedule\_session @@ -308,6 +428,11 @@ async def cleanup_schedule_session(schedule_id: str) Immediate cleanup of a schedule session. +**Usage:** +```python +httpx.post(f"http://localhost:8000/schedules/{sid}/cleanup") +``` + #### health\_check @@ -319,6 +444,11 @@ async def health_check() Health check endpoint. +**Usage:** +```python +httpx.get("http://localhost:8000/health") +``` + #### main @@ -351,6 +481,11 @@ def main(port: int, log_level: str, host: str, workers: int) Run the Course Scheduler HTTP API server. +**Usage:** +```python +python -m scheduler.server --port 8000 +``` + # scheduler.config @@ -365,6 +500,12 @@ class StrictBaseModel(BaseModel) Base class for all models which need strict validation. +**Usage:** +```python +class MyConfig(StrictBaseModel): + ... +``` + **Fields:** - model_config: Configuration for the model @@ -385,8 +526,6 @@ def edit_mode() Context manager for making multiple changes with automatic rollback on validation failure. -**Usage:** - **Raises:** - ValueError: If any configuration validation fails (with automatic rollback) ```python @@ -406,6 +545,11 @@ class TimeBlock(StrictBaseModel) Represents a time block within a day. +**Usage:** +```python +TimeBlock(start="09:00", spacing=60, end="17:00") +``` + #### start @@ -434,6 +578,11 @@ class TimeRange(StrictBaseModel) A time range with start and end times, ensuring start < end. +**Usage:** +```python +TimeRange(start="09:00", end="17:00") +``` + #### start @@ -457,6 +606,11 @@ def from_string(cls, time_range_str: TimeRangeString) -> "TimeRange" Create TimeRange from string format "HH:MM-HH:MM" +**Usage:** +```python +TimeRange.from_string("10:00-12:00") +``` + ## Meeting Objects @@ -467,6 +621,11 @@ class Meeting(StrictBaseModel) Represents a single meeting instance. +**Usage:** +```python +Meeting(day="MON", duration=90, lab=False) +``` + #### day @@ -501,6 +660,11 @@ class ClassPattern(StrictBaseModel) Represents a class pattern. +**Usage:** +```python +ClassPattern(credits=3, meetings=[...]) +``` + #### credits @@ -535,6 +699,11 @@ class TimeSlotConfig(StrictBaseModel) Represents a time slot configuration. +**Usage:** +```python +TimeSlotConfig(times={...}, classes=[...]) +``` + #### times @@ -570,6 +739,11 @@ def validate() Validate that time slot config is consistent and complete. +**Usage:** +```python +TimeSlotConfig.model_validate({...}) +``` + ## CourseConfig Objects @@ -580,6 +754,11 @@ class CourseConfig(StrictBaseModel) Represents a course configuration. +**Usage:** +```python +CourseConfig(course_id="CS 101", credits=3, room=[...], lab=[...], conflicts=[], faculty=[]) +``` + #### course\_id @@ -626,6 +805,11 @@ class FacultyConfig(StrictBaseModel) Represents a faculty configuration. +**Usage:** +```python +FacultyConfig(name="Dr. Smith", maximum_credits=12, minimum_credits=3, ...) +``` + #### name @@ -697,6 +881,11 @@ def validate() Validate the model state. +**Usage:** +```python +FacultyConfig.model_validate({...}) +``` + ## SchedulerConfig Objects @@ -707,6 +896,11 @@ class SchedulerConfig(StrictBaseModel) Represents a scheduler configuration. +**Usage:** +```python +SchedulerConfig(rooms=[...], labs=[...], courses=[...], faculty=[...]) +``` + #### rooms @@ -743,8 +937,6 @@ def validate() Validate all cross-references between child models. This method can be called manually or is used by Pydantic validators. -**Usage:** - **Raises:** - ValueError: If any cross-reference validation fails ```python @@ -766,42 +958,84 @@ class OptimizerFlags(StrEnum) Optimize faculty course assignments using preferences +**Usage:** +```python +OptimizerFlags.FACULTY_COURSE +["FACULTY_COURSE"] # accepted in CombinedConfig JSON +``` + #### FACULTY\_ROOM Optimize faculty room assignments using preferences +**Usage:** +```python +OptimizerFlags.FACULTY_ROOM +["FACULTY_ROOM"] # accepted in CombinedConfig JSON +``` + #### FACULTY\_LAB Optimize faculty lab assignments using preferences +**Usage:** +```python +OptimizerFlags.FACULTY_LAB +["FACULTY_LAB"] # accepted in CombinedConfig JSON +``` + #### SAME\_ROOM Force same room usage for courses taught by the same faculty +**Usage:** +```python +OptimizerFlags.SAME_ROOM +["SAME_ROOM"] # accepted in CombinedConfig JSON +``` + #### SAME\_LAB Force same lab usage for courses taught by the same faculty +**Usage:** +```python +OptimizerFlags.SAME_LAB +["SAME_LAB"] # accepted in CombinedConfig JSON +``` + #### PACK\_ROOMS Optimize packing of rooms for courses taught +**Usage:** +```python +OptimizerFlags.PACK_ROOMS +["PACK_ROOMS"] # accepted in CombinedConfig JSON +``` + #### PACK\_LABS Optimize packing of labs for courses taught +**Usage:** +```python +OptimizerFlags.PACK_LABS +["PACK_LABS"] # accepted in CombinedConfig JSON +``` + ## CombinedConfig Objects @@ -812,6 +1046,11 @@ class CombinedConfig(StrictBaseModel) Represents a combined configuration. +**Usage:** +```python +CombinedConfig(config=..., time_slot_config=..., limit=10) +``` + #### config @@ -857,6 +1096,12 @@ Writer class for CSV output with consistent interface. This class provides a context manager interface for writing course schedules to CSV format, either to a file or stdout. +**Usage:** +```python +with CSVWriter("out.csv") as w: + w.add_schedule(model) +``` + #### \_\_init\_\_ @@ -869,6 +1114,9 @@ Initialize the CSVWriter. **Args:** - filename: The name of the file to write CSV data to, or None for stdout +```python +CSVWriter("out.csv") +``` @@ -882,6 +1130,9 @@ Enter the context manager. **Returns:** The CSVWriter instance +```python +with CSVWriter(None) as w: +``` @@ -895,6 +1146,9 @@ Add a schedule to be written. **Args:** - schedule: List of CourseInstance objects representing a complete schedule +```python +writer.add_schedule(schedule) +``` @@ -910,6 +1164,9 @@ Exit the context manager and write all accumulated schedules. - exc_type: Exception type if an exception occurred - exc_value: Exception value if an exception occurred - traceback: Traceback if an exception occurred +```python +# Flushes accumulated rows to disk +``` @@ -928,6 +1185,12 @@ Writer class for JSON output with consistent interface. This class provides a context manager interface for writing course schedules to JSON format, either to a file or stdout. +**Usage:** +```python +with JSONWriter("out.json") as w: + w.add_schedule(model) +``` + #### \_\_init\_\_ @@ -940,6 +1203,9 @@ Initialize the JSONWriter. **Args:** - filename: The name of the file to write the JSON to +```python +JSONWriter("out.json") +``` @@ -953,6 +1219,9 @@ Enter the context manager. **Returns:** The JSONWriter instance +```python +with JSONWriter(None) as w: +``` @@ -966,6 +1235,9 @@ Add a schedule to be written to the JSON file. **Args:** - schedule: The schedule to be written +```python +writer.add_schedule(schedule) +``` @@ -981,6 +1253,9 @@ Exit the context manager and write all accumulated schedules as one JSON array. - exc_type: Exception type if an exception occurred - exc_value: Exception value if an exception occurred - traceback: Traceback if an exception occurred +```python +# Writes JSON array of schedules +``` @@ -991,6 +1266,12 @@ TypedDict definitions for JSON structures used throughout the scheduler. This module provides type-safe definitions for all JSON data structures, including configuration, API requests/responses, and schedule outputs. +**Usage:** +```python +from scheduler.json_types import CourseInstanceJSON +# TypedDict shapes for JSON payloads +``` + ## TimeInstanceJSON Objects @@ -1001,24 +1282,44 @@ class TimeInstanceJSON(TypedDict) JSON representation of a TimeInstance. +**Usage:** +```python +{"day": 0, "start": 480, "duration": 90} +``` + #### day Day enum value (e.g., 0) +**Usage:** +```python +{"day": 0, "start": 480, "duration": 90} +``` + #### start Timepoint in minutes (e.g., 0) +**Usage:** +```python +{"day": 0, "start": 480, "duration": 90} +``` + #### duration Duration in minutes (e.g., 120) +**Usage:** +```python +{"day": 0, "start": 480, "duration": 120} +``` + ## CourseInstanceJSON Objects @@ -1029,42 +1330,77 @@ class CourseInstanceJSON(TypedDict) JSON representation of a CourseInstance. +**Usage:** +```python +{"course": "CS101.01", "faculty": "...", "times": [...]} +``` + #### course Course string representation (e.g., "CS101.01") +**Usage:** +```python +{"course": "CS101.01", "faculty": "Dr. Smith", "times": []} +``` + #### faculty Faculty string representation (e.g., "Dr. Smith") +**Usage:** +```python +{"course": "CS101.01", "faculty": "Dr. Smith", "times": []} +``` + #### room Room string representation (e.g., "Room 101") +**Usage:** +```python +{"course": "CS101.01", "faculty": "Dr. Smith", "room": "Room 101", "times": []} +``` + #### lab Lab string representation (e.g., "Lab 101") +**Usage:** +```python +{"course": "CS101.01", "faculty": "Dr. Smith", "lab": "Lab 101", "times": []} +``` + #### times List of time instances (e.g., `[{"day": 0, "start": 0, "duration": 120}]`) +**Usage:** +```python +{"course": "CS101.01", "faculty": "Dr. Smith", "times": [{"day": 0, "start": 0, "duration": 120}]} +``` + #### lab\_index Lab index (e.g., 0) +**Usage:** +```python +{"course": "CS101.01", "faculty": "Dr. Smith", "times": [], "lab_index": 0} +``` + # scheduler.models.course @@ -1080,6 +1416,11 @@ class Course() A course with a course_id, section, credits, conflicts, potential labs, potential rooms, and potential faculty. +**Usage:** +```python +Course(course_id="CS 101", section=1, labs=[], rooms=[], conflicts=[], faculties=[]) +``` + #### course\_id @@ -1156,6 +1497,11 @@ def __str__() -> str Pretty Print representation of a course is its course_id and section +**Usage:** +```python +str(course) +``` + ## CourseInstance Objects @@ -1166,6 +1512,11 @@ class CourseInstance(BaseModel) A course instance with a course, time, faculty, room, and lab. +**Usage:** +```python +CourseInstance(course=c, time=slot, faculty="Dr. Smith", room="R1", lab=None) +``` + #### model\_config @@ -1216,6 +1567,9 @@ The string representation of the course **Returns:** The string representation of the course +```python +payload["course"] +``` @@ -1231,6 +1585,9 @@ The list of times assigned to the course instance **Returns:** The list of times assigned to the course instance +```python +instance.times +``` @@ -1247,6 +1604,9 @@ The index of the lab assigned to the course instance **Returns:** The index of the lab assigned to the course instance. None if the course instance does not have a lab +```python +instance.lab_index +``` @@ -1262,6 +1622,9 @@ The CSV representation of the course instance in the format: **Returns:** The CSV representation of the course instance +```python +row = instance.as_csv() +``` @@ -1285,6 +1648,12 @@ This enum provides integer values for each weekday, starting from 1 (Monday) and incrementing through Friday. Used throughout the scheduler for day-based time slot management. +**Usage:** +```python +Day.MON +Day.MON.name +``` + #### \_\_repr\_\_ @@ -1295,6 +1664,11 @@ def __repr__() -> str Pretty Print representation of a day +**Usage:** +```python +repr(Day.MON) +``` + # scheduler.models.time\_slot @@ -1309,6 +1683,11 @@ class Duration(BaseModel) A duration of a time slot in minutes. +**Usage:** +```python +Duration(duration=90) +``` + #### model\_config @@ -1332,6 +1711,11 @@ def value() -> int The value of the duration in minutes since midnight +**Usage:** +```python +d.value +``` + ## TimePoint Objects @@ -1342,6 +1726,11 @@ class TimePoint(BaseModel) A time point in minutes since midnight. +**Usage:** +```python +TimePoint.make_from(10, 30) +``` + #### model\_config @@ -1365,6 +1754,11 @@ def make_from(hr: int, min: int) -> "TimePoint" Make a time point from an hour and minute +**Usage:** +```python +TimePoint.make_from(9, 45) +``` + #### hour @@ -1376,6 +1770,11 @@ def hour() The hour of the time point +**Usage:** +```python +tp.hour +``` + #### minute @@ -1387,6 +1786,11 @@ def minute() The minute of the time point +**Usage:** +```python +tp.minute +``` + #### value @@ -1398,6 +1802,11 @@ def value() -> int The value of the time point in minutes since midnight +**Usage:** +```python +d.value +``` + ## TimeInstance Objects @@ -1408,6 +1817,11 @@ class TimeInstance(BaseModel) A time instance with a day, start time, and duration. +**Usage:** +```python +TimeInstance(day=Day.MON, start=tp, duration=Duration(duration=90)) +``` + #### model\_config @@ -1443,6 +1857,11 @@ def stop() -> TimePoint The stop time of the time instance +**Usage:** +```python +end = inst.stop +``` + ## TimeSlot Objects @@ -1453,6 +1872,11 @@ class TimeSlot(BaseModel) A time slot with a list of time instances and a lab index. +**Usage:** +```python +TimeSlot(times=[...], lab_index=0) +``` + #### model\_config @@ -1487,6 +1911,11 @@ def __hash__() -> int Hash the time slot by its string representation +**Usage:** +```python +hash(slot) +``` + #### lab\_time @@ -1500,6 +1929,9 @@ Returns the time instance corresponding to the lab time slot **Returns:** The time instance of the lab None if the time slot does not have a lab +```python +slot.lab_time() +``` @@ -1514,6 +1946,9 @@ Check if the time slot has a lab **Returns:** True if the time slot has a lab. False otherwise +```python +slot.has_lab() +``` @@ -1528,6 +1963,9 @@ Check if the time slot has a lab that is next to another time slot **Returns:** True if the time slot has a lab that is next to another time slot. False otherwise +```python +a.lab_next_to(b) +``` @@ -1542,6 +1980,9 @@ Check if a time slot is logically next to another **Returns:** True if the time slot is logically next to another. False otherwise +```python +a.lecture_next_to(b) +``` @@ -1556,6 +1997,9 @@ Check if a time slot has any overlap with another time slot **Returns:** True if the time slot has any overlap with another time slot. False otherwise +```python +a.overlaps(b) +``` @@ -1570,6 +2014,9 @@ Check if a course's lab time slot has any overlap with another course's lab time **Returns:** True if the course's lab time slot has any overlap with another course's lab time slot. False otherwise +```python +a.lab_overlaps(b) +``` @@ -1584,6 +2031,9 @@ Check if a time slot fits into a list of time ranges **Returns:** True if the time slot fits into the list of time ranges False otherwise +```python +slot.in_time_ranges(availability_instances) +``` @@ -1610,6 +2060,10 @@ The loaded configuration **Example:** >>> load_config_from_file(CombinedConfig, "config.json") >>> load_config_from_file(CombinedConfig, Path("config.json")) +```python +from scheduler import CombinedConfig, load_config_from_file +cfg = load_config_from_file(CombinedConfig, "config.json") +``` @@ -1627,6 +2081,9 @@ Calculate the availability of a faculty as a list of `TimeInstance` objects. **Returns:** The availability of the faculty as a list of `TimeInstance` objects +```python +slots = get_faculty_availability(faculty_config) +``` @@ -1638,6 +2095,12 @@ class Scheduler() Scheduler class for generating schedules. +**Usage:** +```python +sched = Scheduler(full_config) +next(sched.get_models()) +``` + #### \_\_init\_\_ @@ -1655,6 +2118,9 @@ limit, and optimizer flags **Raises:** - ValueError: If the optimizer flags are invalid +```python +Scheduler(full_config) +``` @@ -1675,6 +2141,10 @@ Generator of lists of `CourseInstance` objects representing complete schedules >>> for model in scheduler.get_models(): ... for course in model: ... print(course.as_csv()) +```python +for schedule in sched.get_models(): + ... +``` @@ -1714,6 +2184,11 @@ def main(config: str, limit: int, format: str, output: str, Generate course schedules using constraint satisfaction solving. +**Usage:** +```python +scheduler path/to/config.json -f csv -o out +``` + # scheduler.time\_slot\_generator @@ -1732,6 +2207,12 @@ This class generates all possible time slot combinations for courses based on the provided TimeSlotConfig, ensuring valid scheduling patterns and constraints are met. +**Usage:** +```python +gen = TimeSlotGenerator(time_slot_config) +gen.time_slots(3) +``` + #### \_\_init\_\_ @@ -1744,6 +2225,9 @@ Initialize the TimeSlotGenerator. **Args:** - config: The TimeSlotConfig containing time blocks and class patterns +```python +TimeSlotGenerator(time_slot_config) +``` @@ -1761,3 +2245,6 @@ Generate all possible time slots for a given credit level. **Returns:** A list of TimeSlot objects +```python +generator.time_slots(credits=3) +``` diff --git a/fern/docs/pages/rest/quickstart.mdx b/fern/docs/pages/rest/quickstart.mdx new file mode 100644 index 0000000..e7b243e --- /dev/null +++ b/fern/docs/pages/rest/quickstart.mdx @@ -0,0 +1,76 @@ +--- +title: REST API quickstart +description: Submit a config, iterate generated schedules, and manage session lifecycle from the HTTP API. +--- + +The REST API is session-based. You submit a configuration once, receive a `schedule_id`, then use that id to generate and inspect schedules. + +## Start the server + +```bash +scheduler-server --port 8000 --host 0.0.0.0 --log-level info +``` + +Interactive docs are available at `http://localhost:8000/docs/`. + +## 1) Submit a configuration + +```bash +curl -X POST "http://localhost:8000/submit" \ + -H "Content-Type: application/json" \ + -d @example.json +``` + +Example response: + +```json +{ + "schedule_id": "f9f2c906-5a00-4b78-a0dd-31f3240fbf4b", + "endpoint": "/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b" +} +``` + +The `schedule_id` is the key for all follow-up requests. + +## 2) Generate schedules + +```bash +curl -X POST "http://localhost:8000/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b/next" +``` + +- Call `next` repeatedly to enumerate feasible schedules. +- Once generation is exhausted, the endpoint returns an error indicating no additional schedules are available. + +## 3) Inspect progress and existing schedules + +```bash +curl -X GET "http://localhost:8000/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b/count" +curl -X GET "http://localhost:8000/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b/index/0" +curl -X GET "http://localhost:8000/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b/details" +``` + +Useful endpoints: + +- `count`: Current generated count, configured limit, completion status. +- `index/{index}`: Retrieve a previously generated schedule by index. +- `details`: Return the original combined config plus generation metadata. + +## Optional: background generation + +```bash +curl -X POST "http://localhost:8000/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b/generate_all" +``` + +This starts background generation for all remaining schedules. + +## Cleanup + +```bash +curl -X DELETE "http://localhost:8000/schedules/f9f2c906-5a00-4b78-a0dd-31f3240fbf4b/delete" +``` + +Sessions are stored in-memory and are not shared across processes. Restarting the server clears active sessions. + +## Request/response schema + +The `POST /submit` request body matches `CombinedConfig` (same as CLI and Python API). The full endpoint contract is available under **REST API reference** in the sidebar. diff --git a/fern/docs/pages/welcome.mdx b/fern/docs/pages/welcome.mdx index f852318..9a3d441 100644 --- a/fern/docs/pages/welcome.mdx +++ b/fern/docs/pages/welcome.mdx @@ -45,6 +45,8 @@ scheduler-server --port 8000 --host 0.0.0.0 --log-level info Interactive OpenAPI UI: `http://localhost:8000/docs/` +For end-to-end session flow (submit -> schedule_id -> next/count/delete), use **REST API -> Integration quickstart**. + ## Python library ```python @@ -62,7 +64,7 @@ More detail: **Python API → Using the library** and **Reference**. ## Example configuration -The repository includes [`example.json`](https://github.com/mucsci/scheduler/blob/main/example.json) and a minimal fixture under `tests/fixtures/minimal_config.json`. +The repository includes [`example.json`](https://github.com/mucsci/scheduler/blob/main/example.json) for full demos and `tests/fixtures/minimal_config.json` for a smaller fixture. ## Next steps diff --git a/fern/openapi.json b/fern/openapi.json new file mode 100644 index 0000000..284bff0 --- /dev/null +++ b/fern/openapi.json @@ -0,0 +1,1930 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Course Scheduler API", + "description": "HTTP API for generating course schedules using constraint satisfaction solving", + "version": "1.0.0" + }, + "paths": { + "/submit": { + "post": { + "summary": "Submit Schedule", + "description": "Submit a new schedule generation request.\n\n**Usage:**\n```python\nhttpx.post(\"http://localhost:8000/submit\", json=body)\n```", + "operationId": "submit_schedule_submit_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CombinedConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/details": { + "get": { + "summary": "Get Schedule Details", + "description": "Get details about a schedule session.\n\n**Usage:**\n```python\nhttpx.get(f\"http://localhost:8000/schedules/{sid}/details\")\n```", + "operationId": "get_schedule_details_schedules__schedule_id__details_get", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleDetailsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/next": { + "post": { + "summary": "Get Next Schedule", + "description": "Get the next generated schedule.\n\n**Usage:**\n```python\nhttpx.post(f\"http://localhost:8000/schedules/{sid}/next\")\n```", + "operationId": "get_next_schedule_schedules__schedule_id__next_post", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/generate_all": { + "post": { + "summary": "Generate All Schedules", + "description": "Generate all remaining schedules for a session asynchronously.\n\n**Usage:**\n```python\nhttpx.post(f\"http://localhost:8000/schedules/{sid}/generate_all\")\n```", + "operationId": "generate_all_schedules_schedules__schedule_id__generate_all_post", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenerateAllResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/count": { + "get": { + "summary": "Get Schedule Count", + "description": "Get the current count of generated schedules for a session.\n\n**Usage:**\n```python\nhttpx.get(f\"http://localhost:8000/schedules/{sid}/count\")\n```", + "operationId": "get_schedule_count_schedules__schedule_id__count_get", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleCountResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/index/{index}": { + "get": { + "summary": "Get Schedule By Index", + "description": "Get a previously generated schedule by index.\n\n**Usage:**\n```python\nhttpx.get(f\"http://localhost:8000/schedules/{sid}/index/0\")\n```", + "operationId": "get_schedule_by_index_schedules__schedule_id__index__index__get", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + }, + { + "name": "index", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Index" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/delete": { + "delete": { + "summary": "Delete Schedule Session", + "description": "Delete a schedule session.\n\n**Usage:**\n```python\nhttpx.delete(f\"http://localhost:8000/schedules/{sid}/delete\")\n```", + "operationId": "delete_schedule_session_schedules__schedule_id__delete_delete", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/schedules/{schedule_id}/cleanup": { + "post": { + "summary": "Cleanup Schedule Session", + "description": "Immediate cleanup of a schedule session.\n\n**Usage:**\n```python\nhttpx.post(f\"http://localhost:8000/schedules/{sid}/cleanup\")\n```", + "operationId": "cleanup_schedule_session_schedules__schedule_id__cleanup_post", + "parameters": [ + { + "name": "schedule_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Schedule Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/health": { + "get": { + "summary": "Health Check", + "description": "Health check endpoint.\n\n**Usage:**\n```python\nhttpx.get(\"http://localhost:8000/health\")\n```", + "operationId": "health_check_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthCheck" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ClassPattern-Input": { + "properties": { + "credits": { + "type": "integer", + "title": "Credits", + "description": "Number of credit hours", + "example": 3 + }, + "meetings": { + "items": { + "$ref": "#/components/schemas/Meeting" + }, + "type": "array", + "title": "Meetings", + "description": "List of meeting times", + "example": [ + { + "day": "MON", + "duration": 150, + "lab": false + } + ] + }, + "disabled": { + "type": "boolean", + "title": "Disabled", + "description": "Whether the pattern is disabled", + "default": false + }, + "start_time": { + "anyOf": [ + { + "$ref": "#/components/schemas/TimeString" + }, + { + "type": "null" + } + ], + "description": "Specific start time constraint" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "credits", + "meetings" + ], + "title": "ClassPattern", + "description": "Represents a class pattern.\n\n**Usage:**\n```python\nClassPattern(credits=3, meetings=[...])\n```" + }, + "ClassPattern-Output": { + "properties": { + "credits": { + "type": "integer", + "title": "Credits", + "description": "Number of credit hours", + "example": 3 + }, + "meetings": { + "items": { + "$ref": "#/components/schemas/Meeting" + }, + "type": "array", + "title": "Meetings", + "description": "List of meeting times", + "example": [ + { + "day": "MON", + "duration": 150, + "lab": false + } + ] + }, + "disabled": { + "type": "boolean", + "title": "Disabled", + "description": "Whether the pattern is disabled", + "default": false + }, + "start_time": { + "anyOf": [ + { + "$ref": "#/components/schemas/TimeString" + }, + { + "type": "null" + } + ], + "description": "Specific start time constraint" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "credits", + "meetings" + ], + "title": "ClassPattern", + "description": "Represents a class pattern.\n\n**Usage:**\n```python\nClassPattern(credits=3, meetings=[...])\n```" + }, + "CombinedConfig": { + "properties": { + "config": { + "$ref": "#/components/schemas/SchedulerConfig-Input", + "description": "Scheduler configuration", + "example": { + "courses": [ + { + "conflicts": [], + "course_id": "CS 101", + "credits": 3, + "faculty": [ + "Dr. Smith" + ], + "lab": [ + "Lab 101" + ], + "room": [ + "Room 101" + ] + } + ], + "faculty": [ + { + "course_preferences": { + "CS 101": 5 + }, + "lab_preferences": { + "Lab 101": 5 + }, + "maximum_credits": 12, + "minimum_credits": 3, + "name": "Dr. Smith", + "room_preferences": { + "Room 101": 5 + }, + "times": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "unique_course_limit": 3 + } + ], + "labs": [ + "Lab 101" + ], + "rooms": [ + "Room 101" + ] + } + }, + "time_slot_config": { + "$ref": "#/components/schemas/TimeSlotConfig-Input", + "description": "Time slot configuration", + "example": { + "classes": [ + { + "credits": 3, + "meetings": [ + { + "day": "MON", + "duration": 150, + "lab": false + } + ] + } + ], + "times": { + "FRI": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "MON": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "THU": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "TUE": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "WED": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ] + } + } + }, + "limit": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Limit", + "description": "Maximum number of schedules to generate", + "default": 10, + "example": 10 + }, + "optimizer_flags": { + "items": { + "$ref": "#/components/schemas/OptimizerFlags" + }, + "type": "array", + "title": "Optimizer Flags", + "description": "List of optimizer flags", + "example": [ + "faculty_course", + "faculty_room", + "faculty_lab", + "same_room", + "same_lab", + "pack_rooms", + "pack_labs" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "config", + "time_slot_config" + ], + "title": "CombinedConfig", + "description": "Represents a combined configuration.\n\n**Usage:**\n```python\nCombinedConfig(config=..., time_slot_config=..., limit=10)\n```" + }, + "Course": { + "type": "string", + "description": "Course name", + "example": "CS 101" + }, + "CourseConfig-Input": { + "properties": { + "course_id": { + "$ref": "#/components/schemas/Course", + "description": "Unique identifier for the course", + "example": "CS 101" + }, + "credits": { + "type": "integer", + "title": "Credits", + "description": "Number of credit hours", + "example": 3 + }, + "room": { + "items": { + "$ref": "#/components/schemas/Room" + }, + "type": "array", + "title": "Room", + "description": "List of acceptable room names", + "example": [ + "Room 101" + ] + }, + "lab": { + "items": { + "$ref": "#/components/schemas/Lab" + }, + "type": "array", + "title": "Lab", + "description": "List of acceptable lab names", + "example": [ + "Lab 101" + ] + }, + "conflicts": { + "items": { + "$ref": "#/components/schemas/Course" + }, + "type": "array", + "title": "Conflicts", + "description": "List of course IDs that cannot be scheduled simultaneously" + }, + "faculty": { + "items": { + "$ref": "#/components/schemas/Faculty" + }, + "type": "array", + "title": "Faculty", + "description": "List of faculty names", + "example": [ + "Dr. Smith" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "course_id", + "credits", + "room", + "lab", + "conflicts", + "faculty" + ], + "title": "CourseConfig", + "description": "Represents a course configuration.\n\n**Usage:**\n```python\nCourseConfig(course_id=\"CS 101\", credits=3, room=[...], lab=[...], conflicts=[], faculty=[])\n```" + }, + "CourseConfig-Output": { + "properties": { + "course_id": { + "$ref": "#/components/schemas/Course", + "description": "Unique identifier for the course" + }, + "credits": { + "type": "integer", + "title": "Credits", + "description": "Number of credit hours", + "example": 3 + }, + "room": { + "items": { + "$ref": "#/components/schemas/Room" + }, + "type": "array", + "title": "Room", + "description": "List of acceptable room names", + "example": [ + "Room 101" + ] + }, + "lab": { + "items": { + "$ref": "#/components/schemas/Lab" + }, + "type": "array", + "title": "Lab", + "description": "List of acceptable lab names", + "example": [ + "Lab 101" + ] + }, + "conflicts": { + "items": { + "$ref": "#/components/schemas/Course" + }, + "type": "array", + "title": "Conflicts", + "description": "List of course IDs that cannot be scheduled simultaneously" + }, + "faculty": { + "items": { + "$ref": "#/components/schemas/Faculty" + }, + "type": "array", + "title": "Faculty", + "description": "List of faculty names", + "example": [ + "Dr. Smith" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "course_id", + "credits", + "room", + "lab", + "conflicts", + "faculty" + ], + "title": "CourseConfig", + "description": "Represents a course configuration.\n\n**Usage:**\n```python\nCourseConfig(course_id=\"CS 101\", credits=3, room=[...], lab=[...], conflicts=[], faculty=[])\n```" + }, + "CourseInstanceResponse": { + "properties": { + "course": { + "type": "string", + "title": "Course", + "description": "Course id with section, e.g. `\"CS101.01\"`." + }, + "faculty": { + "type": "string", + "title": "Faculty" + }, + "times": { + "items": { + "$ref": "#/components/schemas/TimeInstanceResponse" + }, + "type": "array", + "title": "Times" + }, + "room": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Room", + "description": "Assigned room when present." + }, + "lab": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Lab", + "description": "Assigned lab when present." + }, + "lab_index": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Lab Index", + "description": "When `lab` is set, index into `times` for the lab meeting." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "course", + "faculty", + "times" + ], + "title": "CourseInstanceResponse", + "description": "One course row in a generated schedule (`CourseInstance.model_dump` JSON shape)." + }, + "Day": { + "type": "string", + "enum": [ + "MON", + "TUE", + "WED", + "THU", + "FRI" + ], + "description": "Day of the week", + "example": "MON" + }, + "Faculty": { + "type": "string", + "description": "Faculty name", + "example": "Dr. Smith" + }, + "FacultyConfig-Input": { + "properties": { + "name": { + "$ref": "#/components/schemas/Faculty", + "description": "Faculty member\"s name" + }, + "maximum_credits": { + "type": "integer", + "minimum": 0.0, + "title": "Maximum Credits", + "description": "Maximum credit hours they can teach", + "example": 12 + }, + "maximum_days": { + "type": "integer", + "maximum": 5.0, + "minimum": 0.0, + "title": "Maximum Days", + "description": "Maximum number of days they are willing to teach (0-5, optional)", + "default": 5, + "example": 3 + }, + "minimum_credits": { + "type": "integer", + "minimum": 0.0, + "title": "Minimum Credits", + "description": "Minimum credit hours they must teach", + "example": 3 + }, + "unique_course_limit": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Unique Course Limit", + "description": "Maximum number of different courses they can teach", + "example": 3 + }, + "times": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/TimeRange-Input" + }, + "type": "array" + }, + "propertyNames": { + "$ref": "#/components/schemas/Day" + }, + "type": "object", + "title": "Times", + "description": "Dictionary mapping day names to time ranges", + "example": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + } + }, + "course_preferences": { + "additionalProperties": { + "$ref": "#/components/schemas/Preference" + }, + "propertyNames": { + "$ref": "#/components/schemas/Course" + }, + "type": "object", + "title": "Course Preferences", + "description": "Dictionary mapping course IDs to preference scores", + "example": { + "CS 101": 5 + } + }, + "room_preferences": { + "additionalProperties": { + "$ref": "#/components/schemas/Preference" + }, + "propertyNames": { + "$ref": "#/components/schemas/Room" + }, + "type": "object", + "title": "Room Preferences", + "description": "Dictionary mapping room IDs to preference scores", + "example": { + "Room 101": 5 + } + }, + "lab_preferences": { + "additionalProperties": { + "$ref": "#/components/schemas/Preference" + }, + "propertyNames": { + "$ref": "#/components/schemas/Lab" + }, + "type": "object", + "title": "Lab Preferences", + "description": "Dictionary mapping lab IDs to preference scores", + "example": { + "Lab 101": 5 + } + }, + "mandatory_days": { + "items": { + "$ref": "#/components/schemas/Day" + }, + "type": "array", + "uniqueItems": true, + "title": "Mandatory Days", + "description": "Set of days the faculty must teach on", + "example": [ + "MON", + "WED" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "maximum_credits", + "minimum_credits", + "unique_course_limit", + "times" + ], + "title": "FacultyConfig", + "description": "Represents a faculty configuration.\n\n**Usage:**\n```python\nFacultyConfig(name=\"Dr. Smith\", maximum_credits=12, minimum_credits=3, ...)\n```" + }, + "FacultyConfig-Output": { + "properties": { + "name": { + "$ref": "#/components/schemas/Faculty", + "description": "Faculty member\"s name" + }, + "maximum_credits": { + "type": "integer", + "minimum": 0.0, + "title": "Maximum Credits", + "description": "Maximum credit hours they can teach", + "example": 12 + }, + "maximum_days": { + "type": "integer", + "maximum": 5.0, + "minimum": 0.0, + "title": "Maximum Days", + "description": "Maximum number of days they are willing to teach (0-5, optional)", + "default": 5, + "example": 3 + }, + "minimum_credits": { + "type": "integer", + "minimum": 0.0, + "title": "Minimum Credits", + "description": "Minimum credit hours they must teach", + "example": 3 + }, + "unique_course_limit": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Unique Course Limit", + "description": "Maximum number of different courses they can teach", + "example": 3 + }, + "times": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/TimeRange-Output" + }, + "type": "array" + }, + "propertyNames": { + "$ref": "#/components/schemas/Day" + }, + "type": "object", + "title": "Times", + "description": "Dictionary mapping day names to time ranges", + "example": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + } + }, + "course_preferences": { + "additionalProperties": { + "$ref": "#/components/schemas/Preference" + }, + "propertyNames": { + "$ref": "#/components/schemas/Course" + }, + "type": "object", + "title": "Course Preferences", + "description": "Dictionary mapping course IDs to preference scores", + "example": { + "CS 101": 5 + } + }, + "room_preferences": { + "additionalProperties": { + "$ref": "#/components/schemas/Preference" + }, + "propertyNames": { + "$ref": "#/components/schemas/Room" + }, + "type": "object", + "title": "Room Preferences", + "description": "Dictionary mapping room IDs to preference scores", + "example": { + "Room 101": 5 + } + }, + "lab_preferences": { + "additionalProperties": { + "$ref": "#/components/schemas/Preference" + }, + "propertyNames": { + "$ref": "#/components/schemas/Lab" + }, + "type": "object", + "title": "Lab Preferences", + "description": "Dictionary mapping lab IDs to preference scores", + "example": { + "Lab 101": 5 + } + }, + "mandatory_days": { + "items": { + "$ref": "#/components/schemas/Day" + }, + "type": "array", + "uniqueItems": true, + "title": "Mandatory Days", + "description": "Set of days the faculty must teach on", + "example": [ + "MON", + "WED" + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "maximum_credits", + "minimum_credits", + "unique_course_limit", + "times" + ], + "title": "FacultyConfig", + "description": "Represents a faculty configuration.\n\n**Usage:**\n```python\nFacultyConfig(name=\"Dr. Smith\", maximum_credits=12, minimum_credits=3, ...)\n```" + }, + "GenerateAllResponse": { + "properties": { + "message": { + "type": "string", + "title": "Message" + }, + "current_count": { + "type": "integer", + "title": "Current Count" + }, + "target_count": { + "type": "integer", + "title": "Target Count" + } + }, + "type": "object", + "required": [ + "message", + "current_count", + "target_count" + ], + "title": "GenerateAllResponse", + "description": "Response model for generate-all schedule requests.\n\n**Usage:**\n```python\nGenerateAllResponse(message='...', current_count=1, target_count=10)\n```\n\n**Fields:**\n- message: Status message about the generation process\n- current_count: Number of schedules already generated\n- target_count: Target number of schedules to generate" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "HealthCheck": { + "properties": { + "status": { + "type": "string", + "title": "Status" + }, + "active_sessions": { + "type": "integer", + "title": "Active Sessions" + } + }, + "type": "object", + "required": [ + "status", + "active_sessions" + ], + "title": "HealthCheck", + "description": "Health check response model.\n\n**Usage:**\n```python\nHealthCheck(status=\"healthy\", active_sessions=0)\n```\n\n**Fields:**\n- status: Health status of the service\n- active_sessions: Number of active schedule generation sessions" + }, + "Lab": { + "type": "string", + "description": "Lab name", + "example": "Lab 101" + }, + "Meeting": { + "properties": { + "day": { + "$ref": "#/components/schemas/Day" + }, + "start_time": { + "anyOf": [ + { + "$ref": "#/components/schemas/TimeString" + }, + { + "type": "null" + } + ], + "description": "Specific start time constraint" + }, + "duration": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Duration", + "description": "Duration of the meeting in minutes", + "example": 150 + }, + "lab": { + "type": "boolean", + "title": "Lab", + "description": "Whether the meeting is in a lab", + "default": false + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "day", + "duration" + ], + "title": "Meeting", + "description": "Represents a single meeting instance.\n\n**Usage:**\n```python\nMeeting(day=\"MON\", duration=90, lab=False)\n```" + }, + "MessageResponse": { + "properties": { + "message": { + "type": "string", + "title": "Message" + } + }, + "type": "object", + "required": [ + "message" + ], + "title": "MessageResponse", + "description": "Generic message response model.\n\n**Usage:**\n```python\nMessageResponse(message=\"ok\")\n```\n\n**Fields:**\n- message: Response message text" + }, + "OptimizerFlags": { + "type": "string", + "enum": [ + "faculty_course", + "faculty_room", + "faculty_lab", + "same_room", + "same_lab", + "pack_rooms", + "pack_labs" + ], + "title": "OptimizerFlags" + }, + "Preference": { + "type": "integer", + "maximum": 10.0, + "minimum": 0.0, + "description": "Preference score between 0 and 10", + "example": 5 + }, + "Room": { + "type": "string", + "description": "Room name", + "example": "Room 101" + }, + "ScheduleCountResponse": { + "properties": { + "schedule_id": { + "type": "string", + "title": "Schedule Id" + }, + "current_count": { + "type": "integer", + "title": "Current Count" + }, + "limit": { + "type": "integer", + "title": "Limit" + }, + "is_complete": { + "type": "boolean", + "title": "Is Complete" + } + }, + "type": "object", + "required": [ + "schedule_id", + "current_count", + "limit", + "is_complete" + ], + "title": "ScheduleCountResponse", + "description": "Response model for schedule count requests.\n\n**Usage:**\n```python\nScheduleCountResponse(schedule_id='...', current_count=2, limit=10, is_complete=False)\n```\n\n**Fields:**\n- schedule_id: Unique identifier for the schedule session\n- current_count: Number of schedules currently generated\n- limit: Maximum number of schedules to generate\n- is_complete: Whether all schedules have been generated" + }, + "ScheduleDetailsResponse": { + "properties": { + "config": { + "$ref": "#/components/schemas/SchedulerConfig-Output", + "description": "Scheduler configuration", + "example": { + "courses": [ + { + "conflicts": [], + "course_id": "CS 101", + "credits": 3, + "faculty": [ + "Dr. Smith" + ], + "lab": [ + "Lab 101" + ], + "room": [ + "Room 101" + ] + } + ], + "faculty": [ + { + "course_preferences": { + "CS 101": 5 + }, + "lab_preferences": { + "Lab 101": 5 + }, + "maximum_credits": 12, + "minimum_credits": 3, + "name": "Dr. Smith", + "room_preferences": { + "Room 101": 5 + }, + "times": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "unique_course_limit": 3 + } + ], + "labs": [ + "Lab 101" + ], + "rooms": [ + "Room 101" + ] + } + }, + "time_slot_config": { + "$ref": "#/components/schemas/TimeSlotConfig-Output", + "description": "Time slot configuration", + "example": { + "classes": [ + { + "credits": 3, + "meetings": [ + { + "day": "MON", + "duration": 150, + "lab": false + } + ] + } + ], + "times": { + "FRI": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "MON": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "THU": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "TUE": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ], + "WED": [ + { + "end": "12:00", + "spacing": 60, + "start": "10:00" + } + ] + } + } + }, + "limit": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Limit", + "description": "Maximum number of schedules to generate", + "default": 10, + "example": 10 + }, + "optimizer_flags": { + "items": { + "$ref": "#/components/schemas/OptimizerFlags" + }, + "type": "array", + "title": "Optimizer Flags", + "description": "List of optimizer flags", + "example": [ + "faculty_course", + "faculty_room", + "faculty_lab", + "same_room", + "same_lab", + "pack_rooms", + "pack_labs" + ] + }, + "schedule_id": { + "type": "string", + "title": "Schedule Id" + }, + "total_generated": { + "type": "integer", + "title": "Total Generated" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "config", + "time_slot_config", + "schedule_id", + "total_generated" + ], + "title": "ScheduleDetailsResponse", + "description": "Response model for schedule details requests.\n\nInherits all fields from CombinedConfig and adds:\n\n**Usage:**\n```python\nScheduleDetailsResponse(schedule_id='...', total_generated=0, **combined.model_dump())\n```\n\n**Fields:**\n- schedule_id: Unique identifier for the schedule session\n- total_generated: Total number of schedules generated" + }, + "ScheduleResponse": { + "properties": { + "schedule_id": { + "type": "string", + "title": "Schedule Id" + }, + "schedule": { + "items": { + "$ref": "#/components/schemas/CourseInstanceResponse" + }, + "type": "array", + "title": "Schedule" + }, + "index": { + "type": "integer", + "title": "Index" + }, + "total_generated": { + "type": "integer", + "title": "Total Generated" + } + }, + "type": "object", + "required": [ + "schedule_id", + "schedule", + "index", + "total_generated" + ], + "title": "ScheduleResponse", + "description": "Response model for schedule retrieval requests.\n\n**Usage:**\n```python\nScheduleResponse(schedule_id='...', schedule=[...], index=0, total_generated=1)\n```\n\n**Fields:**\n- schedule_id: Unique identifier for the schedule session\n- schedule: Generated schedule as `list[CourseInstanceResponse]` (typed JSON rows)\n- index: Index of this schedule in the generation sequence\n- total_generated: Total number of schedules generated so far" + }, + "SchedulerConfig-Input": { + "properties": { + "rooms": { + "items": { + "$ref": "#/components/schemas/Room" + }, + "type": "array", + "title": "Rooms", + "description": "List of available room names", + "example": [ + "Room 101" + ] + }, + "labs": { + "items": { + "$ref": "#/components/schemas/Lab" + }, + "type": "array", + "title": "Labs", + "description": "List of available lab names", + "example": [ + "Lab 101" + ] + }, + "courses": { + "items": { + "$ref": "#/components/schemas/CourseConfig-Input" + }, + "type": "array", + "title": "Courses", + "description": "List of course configurations", + "example": [ + { + "conflicts": [ + "CS 102" + ], + "course_id": "CS 101", + "credits": 3, + "faculty": [ + "Dr. Smith" + ], + "lab": [ + "Lab 101" + ], + "room": [ + "Room 101" + ] + } + ] + }, + "faculty": { + "items": { + "$ref": "#/components/schemas/FacultyConfig-Input" + }, + "type": "array", + "title": "Faculty", + "description": "List of faculty configurations", + "example": [ + { + "course_preferences": { + "CS 101": 5 + }, + "lab_preferences": { + "Lab 101": 5 + }, + "mandatory_days": [ + "MON" + ], + "maximum_credits": 12, + "maximum_days": 3, + "minimum_credits": 3, + "name": "Dr. Smith", + "room_preferences": { + "Room 101": 5 + }, + "times": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "unique_course_limit": 3 + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "rooms", + "labs", + "courses", + "faculty" + ], + "title": "SchedulerConfig", + "description": "Represents a scheduler configuration.\n\n**Usage:**\n```python\nSchedulerConfig(rooms=[...], labs=[...], courses=[...], faculty=[...])\n```" + }, + "SchedulerConfig-Output": { + "properties": { + "rooms": { + "items": { + "$ref": "#/components/schemas/Room" + }, + "type": "array", + "title": "Rooms", + "description": "List of available room names", + "example": [ + "Room 101" + ] + }, + "labs": { + "items": { + "$ref": "#/components/schemas/Lab" + }, + "type": "array", + "title": "Labs", + "description": "List of available lab names", + "example": [ + "Lab 101" + ] + }, + "courses": { + "items": { + "$ref": "#/components/schemas/CourseConfig-Output" + }, + "type": "array", + "title": "Courses", + "description": "List of course configurations", + "example": [ + { + "conflicts": [ + "CS 102" + ], + "course_id": "CS 101", + "credits": 3, + "faculty": [ + "Dr. Smith" + ], + "lab": [ + "Lab 101" + ], + "room": [ + "Room 101" + ] + } + ] + }, + "faculty": { + "items": { + "$ref": "#/components/schemas/FacultyConfig-Output" + }, + "type": "array", + "title": "Faculty", + "description": "List of faculty configurations", + "example": [ + { + "course_preferences": { + "CS 101": 5 + }, + "lab_preferences": { + "Lab 101": 5 + }, + "mandatory_days": [ + "MON" + ], + "maximum_credits": 12, + "maximum_days": 3, + "minimum_credits": 3, + "name": "Dr. Smith", + "room_preferences": { + "Room 101": 5 + }, + "times": { + "MON": [ + "10:00-12:00" + ], + "TUE": [ + "10:00-12:00" + ] + }, + "unique_course_limit": 3 + } + ] + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "rooms", + "labs", + "courses", + "faculty" + ], + "title": "SchedulerConfig", + "description": "Represents a scheduler configuration.\n\n**Usage:**\n```python\nSchedulerConfig(rooms=[...], labs=[...], courses=[...], faculty=[...])\n```" + }, + "SubmitResponse": { + "properties": { + "schedule_id": { + "type": "string", + "title": "Schedule Id" + }, + "endpoint": { + "type": "string", + "title": "Endpoint" + } + }, + "type": "object", + "required": [ + "schedule_id", + "endpoint" + ], + "title": "SubmitResponse", + "description": "Response model for schedule submission requests.\n\n**Usage:**\n```python\nSubmitResponse(schedule_id=\"...\", endpoint=\"/schedules/...\")\n```\n\n**Fields:**\n- schedule_id: Unique identifier for the generated schedule session\n- endpoint: URL endpoint to access the schedule" + }, + "TimeBlock-Input": { + "properties": { + "start": { + "$ref": "#/components/schemas/TimeString", + "description": "Start time of the time block", + "example": "10:00" + }, + "spacing": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Spacing", + "description": "Time spacing between slots in minutes", + "example": 60 + }, + "end": { + "$ref": "#/components/schemas/TimeString", + "description": "End time of the time block", + "example": "17:00" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "start", + "spacing", + "end" + ], + "title": "TimeBlock", + "description": "Represents a time block within a day.\n\n**Usage:**\n```python\nTimeBlock(start=\"09:00\", spacing=60, end=\"17:00\")\n```" + }, + "TimeBlock-Output": { + "properties": { + "start": { + "$ref": "#/components/schemas/TimeString", + "description": "Start time of the time block" + }, + "spacing": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Spacing", + "description": "Time spacing between slots in minutes", + "example": 60 + }, + "end": { + "$ref": "#/components/schemas/TimeString", + "description": "End time of the time block", + "example": "17:00" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "start", + "spacing", + "end" + ], + "title": "TimeBlock", + "description": "Represents a time block within a day.\n\n**Usage:**\n```python\nTimeBlock(start=\"09:00\", spacing=60, end=\"17:00\")\n```" + }, + "TimeInstanceResponse": { + "properties": { + "day": { + "type": "integer", + "title": "Day", + "description": "Weekday as `Day` enum value (1=Monday \u2026 5=Friday)." + }, + "start": { + "type": "integer", + "title": "Start", + "description": "Start time in minutes since midnight." + }, + "duration": { + "type": "integer", + "title": "Duration", + "description": "Duration in minutes." + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "day", + "start", + "duration" + ], + "title": "TimeInstanceResponse", + "description": "One meeting time block within a scheduled course (JSON shape)." + }, + "TimeRange-Input": { + "properties": { + "start": { + "$ref": "#/components/schemas/TimeString", + "description": "Start time of the time range", + "example": "10:00" + }, + "end": { + "$ref": "#/components/schemas/TimeString", + "description": "End time of the time range", + "example": "17:00" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "start", + "end" + ], + "title": "TimeRange", + "description": "A time range with start and end times, ensuring start < end.\n\n**Usage:**\n```python\nTimeRange(start=\"09:00\", end=\"17:00\")\n```" + }, + "TimeRange-Output": { + "properties": { + "start": { + "$ref": "#/components/schemas/TimeString", + "description": "Start time of the time range" + }, + "end": { + "$ref": "#/components/schemas/TimeString", + "description": "End time of the time range", + "example": "17:00" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "start", + "end" + ], + "title": "TimeRange", + "description": "A time range with start and end times, ensuring start < end.\n\n**Usage:**\n```python\nTimeRange(start=\"09:00\", end=\"17:00\")\n```" + }, + "TimeSlotConfig-Input": { + "properties": { + "times": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/TimeBlock-Input" + }, + "type": "array" + }, + "propertyNames": { + "$ref": "#/components/schemas/Day" + }, + "type": "object", + "title": "Times", + "description": "Dictionary mapping day names to time blocks" + }, + "classes": { + "items": { + "$ref": "#/components/schemas/ClassPattern-Input" + }, + "type": "array", + "title": "Classes", + "description": "List of class patterns" + }, + "max_time_gap": { + "type": "integer", + "minimum": 0.0, + "exclusiveMinimum": 0.0, + "title": "Max Time Gap", + "description": "Maximum time gap between time slots to determine if they are adjacent", + "default": 30, + "example": 30 + }, + "min_time_overlap": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Min Time Overlap", + "description": "Minimum overlap between time slots", + "default": 45, + "example": 45 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "times", + "classes" + ], + "title": "TimeSlotConfig", + "description": "Represents a time slot configuration.\n\n**Usage:**\n```python\nTimeSlotConfig(times={...}, classes=[...])\n```" + }, + "TimeSlotConfig-Output": { + "properties": { + "times": { + "additionalProperties": { + "items": { + "$ref": "#/components/schemas/TimeBlock-Output" + }, + "type": "array" + }, + "propertyNames": { + "$ref": "#/components/schemas/Day" + }, + "type": "object", + "title": "Times", + "description": "Dictionary mapping day names to time blocks" + }, + "classes": { + "items": { + "$ref": "#/components/schemas/ClassPattern-Output" + }, + "type": "array", + "title": "Classes", + "description": "List of class patterns" + }, + "max_time_gap": { + "type": "integer", + "minimum": 0.0, + "exclusiveMinimum": 0.0, + "title": "Max Time Gap", + "description": "Maximum time gap between time slots to determine if they are adjacent", + "default": 30, + "example": 30 + }, + "min_time_overlap": { + "type": "integer", + "exclusiveMinimum": 0.0, + "title": "Min Time Overlap", + "description": "Minimum overlap between time slots", + "default": 45, + "example": 45 + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "times", + "classes" + ], + "title": "TimeSlotConfig", + "description": "Represents a time slot configuration.\n\n**Usage:**\n```python\nTimeSlotConfig(times={...}, classes=[...])\n```" + }, + "TimeString": { + "type": "string", + "pattern": "^([0-1][0-9]|2[0-3]):[0-5][0-9]$", + "description": "Time in HH:MM format", + "example": "10:00" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + } + } +} diff --git a/scripts/gen_python_api_mdx.py b/scripts/gen_python_api_mdx.py index b48861c..db7e9e2 100644 --- a/scripts/gen_python_api_mdx.py +++ b/scripts/gen_python_api_mdx.py @@ -2,6 +2,7 @@ from __future__ import annotations +import re import shutil import subprocess import sys @@ -17,6 +18,18 @@ """ +EMPTY_USAGE_PATTERN = re.compile(r"\*\*Usage:\*\*\n\n(?=\*\*(?:Args|Returns|Raises):\*\*)") + + +def strip_empty_usage_sections(text: str) -> str: + """Remove empty Usage headings emitted by pydoc-markdown. + + Some docstrings include a usage code block, but pydoc-markdown can reorder + recognized sections (`Args`, `Returns`, `Raises`) ahead of that block, + leaving a visually empty `**Usage:**` heading in rendered docs. + """ + return EMPTY_USAGE_PATTERN.sub("", text) + def main() -> None: pydoc = shutil.which("pydoc-markdown") @@ -31,7 +44,8 @@ def main() -> None: text=True, ) OUT.parent.mkdir(parents=True, exist_ok=True) - OUT.write_text(FRONTMATTER + proc.stdout, encoding="utf-8") + rendered = strip_empty_usage_sections(proc.stdout) + OUT.write_text(FRONTMATTER + rendered, encoding="utf-8") print(f"Wrote {OUT}") diff --git a/skills/sched-cli-main/SKILL.md b/skills/sched-cli-main/SKILL.md index 86e0052..ffe7f3d 100644 --- a/skills/sched-cli-main/SKILL.md +++ b/skills/sched-cli-main/SKILL.md @@ -22,3 +22,15 @@ description: >- - Keep **`--help`** accurate; Click option names stable when possible. - If default output or formats change, update **README**, **Fern** pages, and **tests**. - Large or slow paths: consider documenting use of **`pytest` markers** for integration tests. + +## Validation + +```bash +uv run python -m scheduler.main --help +uv run pytest +``` + +Cross-skill references: + +- [sched-output-writers](../sched-output-writers/SKILL.md) for JSON/CSV behavior +- [sched-testing-pytest](../sched-testing-pytest/SKILL.md) for test strategy diff --git a/skills/sched-domain-z3-config/SKILL.md b/skills/sched-domain-z3-config/SKILL.md index 509f893..798fa77 100644 --- a/skills/sched-domain-z3-config/SKILL.md +++ b/skills/sched-domain-z3-config/SKILL.md @@ -10,10 +10,10 @@ description: >- ## Core files -- **`scheduler.py`**: Z3 problem construction, solving, optimization flags. -- **`config.py`**: Pydantic models, validation, loading (`CombinedConfig`, etc.). -- **`time_slot_generator.py`**: Time slot generation utilities. -- **`models/`**: Runtime schedule representations (`CourseInstance`, `TimeSlot`, …). +- **`src/scheduler/scheduler.py`**: Z3 problem construction, solving, optimization flags. +- **`src/scheduler/config.py`**: Pydantic models, validation, loading (`CombinedConfig`, etc.). +- **`src/scheduler/time_slot_generator.py`**: Time slot generation utilities. +- **`src/scheduler/models/`**: Runtime schedule representations (`CourseInstance`, `TimeSlot`, ...). ## Naming trap: `Course` diff --git a/skills/sched-fastapi-server/SKILL.md b/skills/sched-fastapi-server/SKILL.md index 7b9160c..9d882b4 100644 --- a/skills/sched-fastapi-server/SKILL.md +++ b/skills/sched-fastapi-server/SKILL.md @@ -23,6 +23,13 @@ Run locally with `uv run python -m scheduler.server` (plus host/port flags from uv run python scripts/export_openapi.py ``` +## Typical workflow + +1. Edit route/model behavior in **`src/scheduler/server.py`**. +2. Regenerate OpenAPI with `uv run python scripts/export_openapi.py`. +3. Run validation (`uv run pytest` and/or focused API tests). +4. Update Fern narrative docs in `fern/docs/pages/` when user-facing behavior changed. + ## Integration tests - Use **`httpx`** (dev dependency) for async client tests where appropriate; see existing patterns in `tests/`. @@ -30,3 +37,9 @@ uv run python scripts/export_openapi.py ## Documentation - User-facing API narrative lives under **`fern/docs/pages/`**; machine-readable spec is **`fern/openapi.json`**. + +Cross-skill references: + +- [sched-fern-openapi-docs](../sched-fern-openapi-docs/SKILL.md) +- [sched-maintain-scripts](../sched-maintain-scripts/SKILL.md) +- [sched-testing-pytest](../sched-testing-pytest/SKILL.md) diff --git a/skills/sched-fern-openapi-docs/SKILL.md b/skills/sched-fern-openapi-docs/SKILL.md index d6762f6..8343d20 100644 --- a/skills/sched-fern-openapi-docs/SKILL.md +++ b/skills/sched-fern-openapi-docs/SKILL.md @@ -22,6 +22,12 @@ description: >- | `CombinedConfig` / config schema | `uv run python scripts/export_config_schema.py` | | Public API docstrings | `uv run python scripts/gen_python_api_mdx.py` | +Quick decision guide: + +- Editing **`src/scheduler/server.py`** -> run `export_openapi.py` +- Editing **`src/scheduler/config.py`** or related config models -> run `export_config_schema.py` +- Editing public package docstrings under `src/scheduler/` -> run `gen_python_api_mdx.py` + ## Authoring - **Guides / prose**: `fern/docs/pages/` (MDX), navigation in `fern/docs.yml`. @@ -34,3 +40,8 @@ After regenerating as needed: install Fern CLI (`npm install -g fern-api`), then ## CI Docs deployment is in `.github/workflows/docs.yml` — keep generated artifacts committed when CI or publishers expect them. + +Cross-skill references: + +- [sched-maintain-scripts](../sched-maintain-scripts/SKILL.md) for script maintenance details +- [sched-fastapi-server](../sched-fastapi-server/SKILL.md) for route/model change workflow diff --git a/skills/sched-github-ci/SKILL.md b/skills/sched-github-ci/SKILL.md index 61bcb0a..ad18565 100644 --- a/skills/sched-github-ci/SKILL.md +++ b/skills/sched-github-ci/SKILL.md @@ -10,7 +10,10 @@ description: >- ## Workflows -- **`.github/workflows/linting.yml`**: `uv sync --locked --group dev`, then `uv run prek run --all-files` and `uv run pytest`. +- **`.github/workflows/linting.yml`**: + - `lint` job: `uv sync --locked --group dev` then `uv run prek run --all-files` + - `test` job: `uv sync --locked --group dev` then `uv run pytest` + - Both jobs run on Python `3.12` and `3.13` - **`.github/workflows/docs.yml`**: Fern docs build/deploy (see file for triggers and secrets). - **`.github/workflows/publish.yml`**: Package publish pipeline. @@ -24,6 +27,8 @@ uv run prek run --all-files uv run pytest ``` +For quality tooling details, see [sched-ruff-ty-prek](../sched-ruff-ty-prek/SKILL.md). + ## Dependabot - **`.github/dependabot.yml`** schedules dependency updates; bump workflows if action major versions change. diff --git a/skills/sched-json-types/SKILL.md b/skills/sched-json-types/SKILL.md index 09d490a..e723e6e 100644 --- a/skills/sched-json-types/SKILL.md +++ b/skills/sched-json-types/SKILL.md @@ -8,6 +8,11 @@ description: >- # JSON types (`json_types.py`) +## Edit locations + +- **`src/scheduler/json_types.py`** (primary) +- Related shape sources: **`src/scheduler/config.py`**, **`src/scheduler/server.py`** + ## Role - Central place for **JSON structure typing** used across parsing, API, and docs. @@ -18,3 +23,17 @@ description: >- - Prefer **one source of truth**: when possible, derive or mirror shapes from Pydantic rather than duplicating divergent definitions. - After changes that affect the HTTP API surface, regenerate **`fern/openapi.json`**. - Run **ty** and **tests** — JSON typing mistakes often show up as runtime validation errors in tests. + +## Validation + +```bash +uv run ty check . --ignore unresolved-import +uv run pytest +uv run python scripts/export_openapi.py # if API request/response shapes changed +``` + +Cross-skill references: + +- [sched-ruff-ty-prek](../sched-ruff-ty-prek/SKILL.md) +- [sched-fastapi-server](../sched-fastapi-server/SKILL.md) +- [sched-fern-openapi-docs](../sched-fern-openapi-docs/SKILL.md) diff --git a/skills/sched-maintain-scripts/SKILL.md b/skills/sched-maintain-scripts/SKILL.md index f604aae..1b848c4 100644 --- a/skills/sched-maintain-scripts/SKILL.md +++ b/skills/sched-maintain-scripts/SKILL.md @@ -24,3 +24,8 @@ When editing these scripts: - Keep output paths stable unless **`fern/docs.yml`** / publishers are updated too. - Prefer deterministic ordering in generated JSON/MDX when possible to reduce noisy diffs. + +Cross-skill references: + +- [sched-fern-openapi-docs](../sched-fern-openapi-docs/SKILL.md) for authoring rules and local Fern preview +- [sched-fastapi-server](../sched-fastapi-server/SKILL.md) when changes originate from route/model updates diff --git a/skills/sched-output-writers/SKILL.md b/skills/sched-output-writers/SKILL.md index 8051d28..7030f10 100644 --- a/skills/sched-output-writers/SKILL.md +++ b/skills/sched-output-writers/SKILL.md @@ -14,5 +14,17 @@ description: >- ## Guidelines - Keep output **stable and documented** if users parse files in production. -- Thread changes through **CLI** (`main.py`) and any **API** responses that reuse the same structures. +- Thread changes through **CLI** (`src/scheduler/main.py`) and any **API** responses that reuse the same structures. - Add or extend **tests** under `tests/` for new fields or format changes. + +## Validation + +```bash +uv run python -m scheduler.main --help +uv run pytest +``` + +Cross-skill references: + +- [sched-cli-main](../sched-cli-main/SKILL.md) +- [sched-testing-pytest](../sched-testing-pytest/SKILL.md) diff --git a/skills/sched-pr-conventional-commits/SKILL.md b/skills/sched-pr-conventional-commits/SKILL.md index 7b88830..8a74c3a 100644 --- a/skills/sched-pr-conventional-commits/SKILL.md +++ b/skills/sched-pr-conventional-commits/SKILL.md @@ -28,6 +28,16 @@ Common **types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. - Docs and **generated Fern artifacts** updated when APIs or config schema change - Breaking changes called out in the PR description +For exact lint/type/prek command guidance, see [sched-ruff-ty-prek](../sched-ruff-ty-prek/SKILL.md). +For CI parity and workflow behavior, see [sched-github-ci](../sched-github-ci/SKILL.md). + +## Commit-time hook behavior + +- Pre-commit hooks may auto-modify files (for example EOF fixes or Ruff auto-fixes). +- If commit fails due to hook edits: + 1. re-stage (`git add ...`), + 2. re-run commit with the same message. + ## PR description - What changed and why diff --git a/skills/sched-ruff-ty-prek/SKILL.md b/skills/sched-ruff-ty-prek/SKILL.md index a6599fb..d68eaba 100644 --- a/skills/sched-ruff-ty-prek/SKILL.md +++ b/skills/sched-ruff-ty-prek/SKILL.md @@ -35,7 +35,10 @@ uv run prek run --all-files Install hooks locally after sync: `uv run prek install` (see CONTRIBUTING). +Important: hooks can auto-fix files (for example end-of-file or Ruff fixes). If that happens, run `git add` again before committing. + ## Practices - Run **ruff** after substantive edits; run **prek** before treating work as done. +- If hooks modify files during commit, re-stage changes and re-run `git commit`. - Do not “fix” `B019` in the two ignored files without maintainer intent. diff --git a/skills/sched-uv-workflow/SKILL.md b/skills/sched-uv-workflow/SKILL.md index 21b00b2..8601e56 100644 --- a/skills/sched-uv-workflow/SKILL.md +++ b/skills/sched-uv-workflow/SKILL.md @@ -32,3 +32,5 @@ uv run prek run --all-files ``` Use `uv run ` so the project venv and paths stay consistent. + +For CI parity and GitHub Actions behavior, see [sched-github-ci](../sched-github-ci/SKILL.md).