Hello World with FastAPI
Add a REST API to your school timetabling solver.
This tutorial extends the Hello World example by adding a REST API using FastAPI. This is closer to how you’d deploy a solver in production.
Prerequisites
- Completed the Hello World tutorial
- FastAPI and Uvicorn installed:
pip install fastapi uvicorn
Project Structure
Extend your project:
hello_world/
├── domain.py # Same as before
├── constraints.py # Same as before
├── main.py # CLI version (optional)
├── rest_api.py # NEW: FastAPI application
└── pyproject.toml # Add fastapi, uvicorn
Step 1: Update Dependencies
Add FastAPI to your pyproject.toml:
[project]
dependencies = [
"solverforge-legacy == 1.24.1",
"fastapi >= 0.100.0",
"uvicorn >= 0.23.0",
"pytest == 8.2.2",
]
Step 2: Create the REST API
Create rest_api.py:
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from datetime import time
from solverforge_legacy.solver import SolverFactory, SolverManager
from solverforge_legacy.solver.config import (
SolverConfig,
ScoreDirectorFactoryConfig,
TerminationConfig,
Duration,
)
from .domain import Timetable, Timeslot, Room, Lesson
from .constraints import define_constraints
# Pydantic models for API validation
class TimeslotDTO(BaseModel):
day_of_week: str
start_time: str
end_time: str
def to_domain(self) -> Timeslot:
return Timeslot(
self.day_of_week,
time.fromisoformat(self.start_time),
time.fromisoformat(self.end_time),
)
class RoomDTO(BaseModel):
name: str
def to_domain(self) -> Room:
return Room(self.name)
class LessonDTO(BaseModel):
id: str
subject: str
teacher: str
student_group: str
timeslot: TimeslotDTO | None = None
room: RoomDTO | None = None
def to_domain(self, timeslots: list[Timeslot], rooms: list[Room]) -> Lesson:
ts = None
if self.timeslot:
ts = next(
(t for t in timeslots if t.day_of_week == self.timeslot.day_of_week
and t.start_time.isoformat() == self.timeslot.start_time),
None
)
rm = None
if self.room:
rm = next((r for r in rooms if r.name == self.room.name), None)
return Lesson(self.id, self.subject, self.teacher, self.student_group, ts, rm)
class TimetableDTO(BaseModel):
id: str
timeslots: list[TimeslotDTO]
rooms: list[RoomDTO]
lessons: list[LessonDTO]
score: str | None = None
def to_domain(self) -> Timetable:
timeslots = [ts.to_domain() for ts in self.timeslots]
rooms = [r.to_domain() for r in self.rooms]
lessons = [l.to_domain(timeslots, rooms) for l in self.lessons]
return Timetable(self.id, timeslots, rooms, lessons)
@classmethod
def from_domain(cls, timetable: Timetable) -> "TimetableDTO":
return cls(
id=timetable.id,
timeslots=[
TimeslotDTO(
day_of_week=ts.day_of_week,
start_time=ts.start_time.isoformat(),
end_time=ts.end_time.isoformat(),
)
for ts in timetable.timeslots
],
rooms=[RoomDTO(name=r.name) for r in timetable.rooms],
lessons=[
LessonDTO(
id=l.id,
subject=l.subject,
teacher=l.teacher,
student_group=l.student_group,
timeslot=TimeslotDTO(
day_of_week=l.timeslot.day_of_week,
start_time=l.timeslot.start_time.isoformat(),
end_time=l.timeslot.end_time.isoformat(),
) if l.timeslot else None,
room=RoomDTO(name=l.room.name) if l.room else None,
)
for l in timetable.lessons
],
score=str(timetable.score) if timetable.score else None,
)
# Global solver manager
solver_manager: SolverManager | None = None
solutions: dict[str, Timetable] = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize solver manager on startup."""
global solver_manager
solver_config = SolverConfig(
solution_class=Timetable,
entity_class_list=[Lesson],
score_director_factory_config=ScoreDirectorFactoryConfig(
constraint_provider_function=define_constraints
),
termination_config=TerminationConfig(
spent_limit=Duration(seconds=30)
),
)
solver_factory = SolverFactory.create(solver_config)
solver_manager = SolverManager.create(solver_factory)
yield
# Cleanup on shutdown
if solver_manager:
solver_manager.close()
app = FastAPI(
title="School Timetabling API",
description="Optimize school timetables using SolverForge",
lifespan=lifespan,
)
@app.post("/timetables", response_model=str)
async def submit_problem(timetable_dto: TimetableDTO) -> str:
"""Submit a timetabling problem for solving."""
job_id = str(uuid.uuid4())
problem = timetable_dto.to_domain()
def on_best_solution(solution: Timetable):
solutions[job_id] = solution
solver_manager.solve_and_listen(
job_id,
lambda _: problem,
on_best_solution,
)
return job_id
@app.get("/timetables/{job_id}", response_model=TimetableDTO)
async def get_solution(job_id: str) -> TimetableDTO:
"""Get the current best solution for a job."""
if job_id not in solutions:
raise HTTPException(status_code=404, detail="Job not found")
return TimetableDTO.from_domain(solutions[job_id])
@app.delete("/timetables/{job_id}")
async def stop_solving(job_id: str):
"""Stop solving a problem early."""
solver_manager.terminate_early(job_id)
return {"status": "terminated"}
@app.get("/demo-data", response_model=TimetableDTO)
async def get_demo_data() -> TimetableDTO:
"""Get demo problem data for testing."""
timeslots = [
Timeslot("MONDAY", time(8, 30), time(9, 30)),
Timeslot("MONDAY", time(9, 30), time(10, 30)),
Timeslot("TUESDAY", time(8, 30), time(9, 30)),
Timeslot("TUESDAY", time(9, 30), time(10, 30)),
]
rooms = [Room("Room A"), Room("Room B")]
lessons = [
Lesson("1", "Math", "A. Turing", "9th grade"),
Lesson("2", "Physics", "M. Curie", "9th grade"),
Lesson("3", "History", "I. Jones", "9th grade"),
Lesson("4", "Math", "A. Turing", "10th grade"),
]
return TimetableDTO.from_domain(Timetable("demo", timeslots, rooms, lessons))
Step 3: Run the API
uvicorn hello_world.rest_api:app --reload
The API is now running at http://localhost:8000.
Step 4: Test the API
Get Demo Data
curl http://localhost:8000/demo-data
Submit a Problem
# Get demo data and submit it for solving
curl http://localhost:8000/demo-data | curl -X POST \
-H "Content-Type: application/json" \
-d @- \
http://localhost:8000/timetables
This returns a job ID like "a1b2c3d4-...".
Check the Solution
curl http://localhost:8000/timetables/{job_id}
Stop Solving Early
curl -X DELETE http://localhost:8000/timetables/{job_id}
API Documentation
FastAPI automatically generates interactive API docs:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Architecture Notes
SolverManager
SolverManager handles concurrent solving jobs:
- Each job runs in its own thread
- Multiple problems can be solved simultaneously
- Solutions are updated as the solver improves them
Pydantic Models
We use separate Pydantic DTOs for:
- API request/response validation
- JSON serialization
- Decoupling API schema from domain model
Production Considerations
For production deployments:
- Persistence: Store solutions in a database
- Scaling: Use a message queue for distributed solving
- Monitoring: Add logging and metrics
- Security: Add authentication and rate limiting
Next Steps
- SolverManager - Learn more about async solving
- Integration - Database and deployment patterns
- Quickstarts - More complete examples
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.