A REST API server for retrieving public data from Canadian universities, including the University of Ottawa and Carleton University.
This package features a completely refactored, clean architecture with improved maintainability and extensibility.
- π« Multi-University Support: University of Ottawa and Carleton University
- π Complete Course Data: Catalogs, timetables, prerequisites, components
- π Academic Programs: 840+ programs with filtering, search, and bulk export
- β‘ Live Timetable Data: Real-time course availability and scheduling
- β Rate My Professor Integration: Professor ratings for both universities
- π FastAPI REST API: Complete HTTP API with interactive documentation
- π¦ Laravel Integration: Bulk program export for database seeding
- π§ Clean Architecture: Layered design with proper separation of concerns
- π Python Library: Programmatic access to services for building your own applications
- π Type Safety: Full type annotations with Pydantic models
The package has been completely refactored with a clean layered architecture:
uoapi/
βββ core/ # Domain models & interfaces
βββ universities/ # University-specific implementations
βββ services/ # Business logic layer
βββ server/ # REST API server
βββ utils/ # Shared utilities
- β Single Responsibility: Each module has one clear purpose
- β Consistent Models: Unified data structures across universities
- β Easy Extension: Add new universities by implementing simple interfaces
- β Better Testing: Clear boundaries enable comprehensive testing
- β Type Safety: Full type annotations throughout
# From PyPI (Recommended)
pip install schedulo-api
# From Source
pip install git+https://github.com/Rain6435/uoapi.git@dev
# Development Installation
git clone https://github.com/Rain6435/uoapi.git
cd uoapi
pip install -e .[tests]# Start the REST API server
schedulo-server --port 8000
# With custom options
schedulo-server --host 0.0.0.0 --port 8080 --workers 4
# Development mode with auto-reload
schedulo-server --reload --log-level debug
# Interactive docs available at:
# http://localhost:8000/docs
# http://localhost:8000/redoc# Start server programmatically
from uoapi.server.app import create_app
import uvicorn
app = create_app()
uvicorn.run(app, host="127.0.0.1", port=8000)from uoapi.core import University
from uoapi.services import DefaultCourseService, DefaultTimetableService
# Initialize services
course_service = DefaultCourseService()
timetable_service = DefaultTimetableService()
# Get all subjects for a university
subjects = course_service.get_subjects(University.CARLETON)
print(f"Found {len(subjects)} subjects")
# Get courses for a specific subject
courses = course_service.get_courses(University.CARLETON, "COMP")
print(f"Found {len(courses)} COMP courses")
# Search courses
search_result = course_service.search_courses(University.UOTTAWA, "programming")
print(f"Found {search_result.total_found} courses matching 'programming'")
# Get live timetable data
if University.CARLETON in timetable_service.get_supported_universities():
live_data = timetable_service.get_live_courses(
University.CARLETON,
term_code="202501",
subjects=["COMP"],
max_courses_per_subject=10
)
print(f"Found {live_data.courses_offered} offered courses")from uoapi.core import University
from uoapi.services import DefaultCourseService
service = DefaultCourseService()
# Get all supported universities
universities = service.get_all_universities()
# Get subjects
subjects = service.get_subjects(University.UOTTAWA)
for subject in subjects[:5]:
print(f"{subject.code}: {subject.name}")
# Get courses with filtering
courses = service.get_courses(
University.CARLETON,
subject_code="COMP",
query="database"
)
# Get specific course
course = service.get_course_by_code(University.UOTTAWA, "CSI3140")
print(f"{course.title}: {course.credits} credits")
# Get course statistics
stats = service.get_course_statistics(University.CARLETON)
print(f"Total courses: {stats['total_courses']}")from uoapi.services import DefaultTimetableService
service = DefaultTimetableService()
# Check which universities support live data
supported = service.get_supported_universities()
print(f"Live data supported by: {[u.value for u in supported]}")
# Get available terms
terms = service.get_available_terms(University.CARLETON)
for code, name in terms:
print(f"{code}: {name}")
# Get live course data
result = service.get_live_courses(
university=University.CARLETON,
term_code="202501",
subjects=["COMP", "MATH"],
max_courses_per_subject=20
)
print(f"Processing time: {result.processing_time:.2f}s")
print(f"Offering rate: {result.offering_rate:.1f}%")
for course in result.courses:
if course.is_offered:
print(f"\n{course.course_code}: {course.title}")
for section in course.sections:
print(f" {section.section}: {section.instructor} - {section.status}")
for mt in section.meeting_times:
print(f" {mt.days} {mt.start_time}-{mt.end_time}")from uoapi.services import DefaultRatingService
service = DefaultRatingService()
# Get individual instructor rating
rating = service.get_instructor_rating("John Smith", University.UOTTAWA)
if rating:
print(f"Rating: {rating['rating']}/5.0")
print(f"Difficulty: {rating['avg_difficulty']}/5.0")
# Get batch ratings
instructors = [("Jane", "Doe"), ("John", "Smith")]
ratings = service.get_batch_ratings(instructors, University.CARLETON)
# Enhance courses with ratings
enhanced_courses = service.inject_ratings_into_courses(courses, University.UOTTAWA)The Schedulo API provides a comprehensive FastAPI-based REST server with interactive documentation, structured responses, and powerful filtering capabilities.
# Start the server (Recommended)
schedulo-server --port 8000
# With additional options
schedulo-server --host 0.0.0.0 --port 8080 --workers 4
# Development mode
schedulo-server --reload --log-level debug
# Or programmatically
python -c "
from uoapi.server.app import create_app
import uvicorn
app = create_app()
uvicorn.run(app, host='127.0.0.1', port=8000)
"Interactive Documentation: http://localhost:8000/docs ReDoc Documentation: http://localhost:8000/redoc
# List all supported universities
curl http://localhost:8000/universities
# Get university-specific information
curl http://localhost:8000/universities/carleton/info
curl http://localhost:8000/universities/uottawa/info# Get subjects (preview - first 20)
curl http://localhost:8000/universities/carleton/subjects
curl http://localhost:8000/universities/uottawa/subjects
# Get all subjects (complete catalog)
curl http://localhost:8000/universities/carleton/subjects/catalog
curl http://localhost:8000/universities/uottawa/subjects/catalog# Get catalog courses by subject
curl "http://localhost:8000/universities/carleton/courses/catalog?subjects=COMP,MATH&limit=10"
curl "http://localhost:8000/universities/uottawa/courses/catalog?subjects=CSI,MAT&limit=5"
# Get all catalog courses (warning: large response)
curl "http://localhost:8000/universities/carleton/courses/catalog"
# Get single course (catalog data only)
curl http://localhost:8000/universities/carleton/courses/COMP1005
curl http://localhost:8000/universities/uottawa/courses/CSI3140# Get available terms for live data
curl http://localhost:8000/universities/carleton/terms
curl http://localhost:8000/universities/uottawa/terms
# Multiple courses with live sections
curl "http://localhost:8000/universities/carleton/courses/live?term=fall&year=2025&subjects=COMP,MATH&limit=20&include_ratings=true"
curl "http://localhost:8000/universities/uottawa/courses/live?term=winter&year=2025&subjects=CSI,CEG&limit=10"
# Filter by specific course codes
curl "http://localhost:8000/universities/carleton/courses/live?term=fall&year=2025&subjects=COMP&course_codes=COMP1005,COMP1405"
# Single course with structured sections
curl "http://localhost:8000/universities/carleton/courses/COMP1005/live?term=fall&year=2025&include_ratings=true"New Single Course Response Structure:
{
"university": "carleton",
"term_code": "202530",
"term_name": "Fall 2025",
"course": {
"course_code": "COMP1005",
"subject_code": "COMP",
"title": "Programming Concepts",
"credits": 0.5,
"is_offered": true,
"sections_found": 13
},
"sections": [
{
"section": "A",
"components": [
{
"name": "A",
"crn": "31108",
"status": "Open",
"credits": 0.5,
"schedule_type": "Lecture",
"instructor": "Ava McKenney",
"meeting_times": [
{
"start_date": "Sep 03, 2025",
"end_date": "Dec 05, 2025",
"days": "Wed Fri",
"start_time": "13:05",
"end_time": "14:25"
}
],
"notes": ["Also Register in: COMP 1005 A1 or A2 or A3"],
"rmp_rating": {
"instructor": "Ava McKenney",
"rating": 4.2,
"num_ratings": 15
}
},
{
"name": "A1",
"crn": "31109",
"status": "Open",
"schedule_type": "Tutorial",
"instructor": "Ava McKenney"
},
{
"name": "A2",
"crn": "31110",
"status": "Full, No Waitlist",
"schedule_type": "Tutorial"
}
]
}
]
}# Get Rate My Professor ratings
curl "http://localhost:8000/universities/carleton/professors/John/Smith"
curl "http://localhost:8000/universities/uottawa/professors/Lucia/Moura"# Get all programs for a university
curl "http://localhost:8000/universities/carleton/programs?limit=10"
curl "http://localhost:8000/universities/uottawa/programs?limit=10"
# Filter programs by criteria
curl "http://localhost:8000/universities/carleton/programs?faculty=engineering&limit=5"
curl "http://localhost:8000/universities/uottawa/programs?degree_type=bachelor&faculty=science"
# Search programs by name
curl "http://localhost:8000/universities/carleton/programs/search?q=computer&limit=5"
curl "http://localhost:8000/universities/uottawa/programs/search?q=engineering&limit=5"
# Get available filter options
curl "http://localhost:8000/universities/carleton/programs/filters"
curl "http://localhost:8000/universities/uottawa/programs/filters"
# π BULK EXPORT - All programs for Laravel/database import
curl "http://localhost:8000/universities/carleton/programs/export"
curl "http://localhost:8000/universities/uottawa/programs/export"Programs Data Coverage:
- π Carleton University: 129 programs across 5 faculties
- π University of Ottawa: 700+ programs across 9 faculties
- π Total: 840+ academic programs available
Bulk Export Features:
- π¦ One-shot export: Complete university + faculty + program data
- ποΈ Laravel-compatible: Ready for direct database import
- π Relational structure: Proper university β faculty β program hierarchy
- π Rich metadata: Export timestamps, counts, and import notes
- ποΈ Structured Responses: Properly grouped course sections and components
- π Academic Programs: Complete program catalog with search and filtering
- π¦ Bulk Export: Laravel-ready program data with relational structure
- β Professor Integration: Optional Rate My Professor ratings via
?include_ratings=true - π Smart Filtering: Filter by subjects, course codes, terms, faculties, disciplines
- π University-Specific: Handles different term formats and subject code lengths
- π Comprehensive Data: Course catalogs, live timetables, prerequisites, programs
- π High Performance: Direct single-course queries bypass bulk discovery
- π Interactive Docs: Auto-generated OpenAPI documentation
- π‘οΈ Type Safety: Full Pydantic validation and serialization
- π― RESTful Design: Clean, predictable endpoint structure
from uoapi.core import UniversityProvider, University, Subject, Course
from uoapi.universities import BaseUniversityProvider
class MyUniversityProvider(BaseUniversityProvider):
@property
def university(self) -> University:
return University.MYUNI # Add to enum first
@property
def name(self) -> str:
return "My University"
def get_subjects(self) -> List[Subject]:
# Implement subject scraping/loading
return []
def get_courses(self, subject_code: str = None) -> List[Course]:
# Implement course scraping/loading
return []
# Register with service
from uoapi.services import DefaultCourseService
service = DefaultCourseService()
service._providers[University.MYUNI] = MyUniversityProvider()from fastapi import APIRouter
from uoapi.server.app import create_app
# Create custom router
custom_router = APIRouter()
@custom_router.get("/custom/endpoint")
async def custom_endpoint():
return {"message": "Custom functionality"}
# Add to app
app = create_app()
app.include_router(custom_router, prefix="/api/v1", tags=["custom"])from uoapi.utils import get_config
config = get_config()
# Adjust cache settings
config.cache.ttl_seconds = 7200 # 2 hours
config.cache.uottawa_ttl = 14400 # 4 hours for UOttawa
# Adjust scraping settings
config.scraping.timeout_seconds = 60
config.scraping.concurrent_workers = 8
# API settings
config.api.port = 9000
config.api.debug = Truefrom uoapi.core import Course
# All universities use the same model
course = Course(
course_code="COMP1001",
subject_code="COMP",
course_number="1001",
title="Introduction to Computing",
description="Basic computing concepts...",
credits=3,
university=University.CARLETON,
components=["Lecture", "Laboratory"],
prerequisites="None",
sections=[...], # Live sections if available
is_offered=True
)from uoapi.core import SearchResult
result = SearchResult(
university=University.UOTTAWA,
query="programming",
subject_filter="CSI",
total_found=25,
courses=[...],
metadata={"search_method": "text_search"}
)from uoapi.core import DiscoveryResult
result = DiscoveryResult(
term_code="202501",
term_name="Winter 2025",
university=University.CARLETON,
subjects_queried=["COMP", "MATH"],
total_courses=150,
courses_offered=142,
offering_rate=94.7,
processing_time=25.3,
courses=[...]
)git clone https://github.com/Rain6435/uoapi.git
cd uoapi
pip install -r requirements.txt# Run all tests (187 tests)
pytest tests/ -v
# Run without coverage (faster)
pytest tests/ --no-cov -v
# Run specific test categories
pytest tests/core/ -v # Core models & exceptions (95 tests)
pytest tests/services/ -v # Business logic services (40 tests)
pytest tests/server/ -v # Helper functions (26 tests)
pytest tests/utils/ -v # Configuration (26 tests)
# Run with coverage report
pytest tests/ --cov=src/uoapi --cov-report=html --cov-report=term-missing
# Type checking
mypy src/
# Linting
flake8 src/ tests/
# Code formatting
black src/ tests/
# Security scan
bandit -r src/
# All checks at once
make check-all- 187 passing tests with zero warnings
- 95 core tests: Pydantic models (41) and exception hierarchy (54)
- 40 service tests: DefaultCourseService (30) and DefaultTimetableService (10)
- 26 helper tests: Helper function validation
- 26 config tests: Configuration management
- 100% mocked dependencies: No network calls, pure unit tests
- 70% minimum coverage enforced
The refactored codebase maintains high code quality with:
- 100% type coverage with mypy
- 187 passing unit tests with comprehensive coverage
- Consistent formatting with black
- Clean imports and modular design
- Zero warnings in test output
- Pydantic V2 type-safe models with field validators
# Old way
from uoapi.course.course_info import scrape_subjects, get_courses
subjects = scrape_subjects()
courses = list(get_courses(subjects[0]['link']))
# New way (recommended)
from uoapi.core import University
from uoapi.services import DefaultCourseService
service = DefaultCourseService()
subjects = service.get_subjects(University.UOTTAWA)
courses = service.get_courses(University.UOTTAWA, subjects[0].code)# Old imports still work
from uoapi.course import scrape_subjects, get_courses # β
Still works
from uoapi.carleton.discovery import CarletonDiscovery # β
Still works
from uoapi.server.app import create_app # β
Still works
# But new imports are cleaner
from uoapi.core import * # β
New unified models
from uoapi.services import * # β
Business logic
from uoapi.interfaces.api import create_app # β
Clean API- Import errors: Ensure Python 3.10+ and proper installation
- University not supported: Check
service.get_all_universities() - Term validation: Use
timetable_service.get_available_terms()first - Rate limiting: Reduce concurrent workers if getting blocked
- Live data not available: Both Carleton and UOttawa support live timetable data
import logging
logging.basicConfig(level=logging.DEBUG)
# Or start server with debug logging
schedulo-server --log-level debugfrom uoapi.utils import get_config, reload_config
# Check current config
config = get_config()
print(config.to_dict())
# Reload with different environment
reload_config("development")- π¨ CLI Removed: The package is now server-only. Use
schedulo-serverto start the REST API - π§ Server-First Architecture: All functionality now accessed via HTTP API or Python library
- π§ Enhanced Section Parsing: Complete retrieval of all course sections, lectures, tutorials, and labs
- β‘ Improved Data Accuracy: Fixed Banner system parsing to capture all available course sections
- π Better Performance: Optimized server with worker support
- π¨βπ« Professor Ratings: Rate My Professor integration via API endpoints
- π Complete REST API: All functionality available via HTTP endpoints
- π― Smart Subject Validation: University-specific subject code validation (4-letter for Carleton, 3-letter for UOttawa)
- ποΈ Clean Architecture: Proper layered design with separation of concerns
- π§ Service Layer: Business logic separated from data access
- π― Single Responsibility: Each module has one clear purpose
- π Dependency Inversion: High-level modules don't depend on low-level details
- β Type Safety: Complete type annotations with Pydantic
- π§ͺ Better Testing: Clear boundaries enable comprehensive testing
- π Better Documentation: Comprehensive examples and API docs
- π§ Easy Extension: Add new universities via simple interfaces
- π API-First: Build your own clients or integrate with any platform
- π¨ Consistent APIs: Same patterns across all universities
- β‘ Better Performance: Improved caching and parallel processing
- π Better Error Messages: Structured exceptions with helpful details
- π Richer Data: Enhanced models with metadata and validation
- π Universal Access: Use any HTTP client to consume the API
We welcome contributions! The architecture makes it easy to contribute:
- Add Universities: Implement
UniversityProviderinterface - Add Features: Extend service classes with new functionality
- Add API Endpoints: Create new REST API endpoints
- Fix Bugs: Clear modular structure makes debugging easier
# 1. Fork and clone
git clone https://github.com/your-username/uoapi.git
# 2. Create feature branch
git checkout -b feature/my-feature
# 3. Make changes and test
make test
make lint
# 4. Submit PR
git push origin feature/my-featureGNU LGPLv3.0 - See the COPYING and COPYING.LESSER files for details.
- Original uoapi by Andrew Nagarajah
- University of Ottawa and Carleton University for public data access
- Rate My Professor for their API
- The Python community for excellent libraries and tools
Ready to explore university course data via REST API?
pip install schedulo-api
schedulo-server --port 8000 # Start the server
# Visit http://localhost:8000/docs for interactive documentation