A FastAPI-based REST API for querying UC (Pontificia Universidad Católica de Chile) BuscaCursos course data. Courses are stored in a SQLite database with one table per semester and are automatically updated from the jj-sm/buscacursos-dl releases.
- ReDocs: visit the API Documentation here: uc.api.jjsm.science/redoc
- Swagger UI: visit the API Documentation here: uc.api.jjsm.science/docs#
For testing, use this free tier key:
X-API-Key: i2bUXB2DBnP01tXA3vQCYjYNNHhAHIkjX25Bj63zkVc, if you need to make more requests per second, contact me at api@jjsm.science. It won't have any cost, just need to control who access massively the API.
- BuscaCursos Endpoints: Search, list, retrieve, stream, and aggregate course data across semesters
- Tier-Based Authentication: 4 API tiers with different rate limits (Free/Pro/Premium/Enterprise)
- Rate Limiting: Sliding-window rate limiter with per-tier configuration
- Free: 10 requests/second
- Pro: 100 requests/second + NDJSON streaming
- Premium: 1,000 requests/second + semester statistics
- Enterprise: Unlimited
- Streaming Responses: Stream full semester datasets as NDJSON (Pro+)
- Auto-Update: Background task checks GitHub for new semester releases monthly (configurable)
- Admin Management: API key lifecycle, update-frequency control, manual update triggers
- Optional Observability: Toggle Grafana stack (Tempo traces, Prometheus metrics, Loki log correlation) via env flag
- API Key Authentication: Secure API key-based authentication via
X-API-Keyheader - Interactive Documentation: Auto-generated Swagger UI (
/docs) and ReDoc (/redoc)
- Python 3.8+
- SQLite 3.6+ (default) or PostgreSQL/MySQL for production
- Docker (optional, for containerized deployment)
-
Clone the repository:
git clone https://github.com/jj-sm/uc-buscacursos-api.git cd uc-buscacursos-api -
Install dependencies:
./scripts/setup.sh
-
Set up environment variables:
cp .env.example .env # Edit .env with your configuration -
Run the application:
./scripts/run.sh
The API will be available at http://localhost:8000
-
Using docker-compose (recommended):
./scripts/docker-up.sh
-
Manual Docker build:
./scripts/docker-build.sh universal-api:local docker run -p 8000:8000 universal-api:local
- docs/COURSES_API.md – Full endpoint reference for BuscaCursos
Run the server and visit:
- Swagger UI:
http://localhost:8000/docs - ReDoc:
http://localhost:8000/redoc
-
Core System:
app/main.py- FastAPI application setup and lifespan (starts background updater)app/auth_models.py- Tier-based API key modelsapp/deps.py- Authentication and rate limitingapp/course_db.py- Raw sqlite3 connection for dynamic semester tablesapp/course_updater.py- Periodic GitHub release checker / DB merger
-
routers/:
courses.py- All BuscaCursos endpoints (search, list, stream, stats, metadata)admin_courses.py- Update frequency, status, and manual trigger endpointsadmin.py- API key management
-
Set up environment:
cp .env.example .env # Set COURSES_DATABASE_URL, GITHUB_TOKEN (optional) -
Run the server:
./scripts/run.sh
-
On first start the background task will check GitHub for the latest semester and download it automatically.
# List available semesters
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/courses/semesters
# Search courses
curl -H "X-API-Key: your-api-key" \
"http://localhost:8000/courses/semester_2026_1/search?q=programacion&page_size=5"
# Get course by NRC
curl -H "X-API-Key: your-api-key" \
http://localhost:8000/courses/semester_2026_1/nrc/12345
# Stream entire semester (Pro+)
curl -H "X-API-Key: your-pro-api-key" \
http://localhost:8000/courses/semester_2026_1/stream
# Semester statistics (Premium+)
curl -H "X-API-Key: your-premium-api-key" \
http://localhost:8000/courses/semester_2026_1/stats# Check update status
curl -H "X-API-Key: admin-key" \
http://localhost:8000/admin/courses/update-status
# Trigger immediate update check
curl -X POST -H "X-API-Key: admin-key" \
http://localhost:8000/admin/courses/update-check
# Change update interval to 7 days
curl -X POST -H "X-API-Key: admin-key" \
"http://localhost:8000/admin/courses/update-frequency?interval_seconds=604800"Rate limits are enforced per API key and tier:
| Tier | Requests/Second | Features |
|---|---|---|
| Free | 10 | Search, list, retrieve |
| Pro | 100 | + NDJSON streaming |
| Premium | 1,000 | + Semester statistics |
| Enterprise | Unlimited | All features |
When you exceed rate limits, you'll receive a 429 response.
- Create your router file in
app/routers/my_router.py - Follow the patterns in
routers/template.py(examples provided) - Register in
app/main.py:from .routers import admin, template, courses, my_router app.include_router(my_router.router, prefix="/api/my", tags=["My Router"])
- Test at
http://localhost:8000/docs
All API endpoints require authentication via API key. Include your API key in the request header:
curl -H "X-API-Key: your-api-key" http://localhost:8000/courses/semestersSee docs/COURSES_API.md for the full endpoint reference.
| Endpoint | Tier | Description |
|---|---|---|
GET /courses/semesters |
Free | List available semesters |
GET /courses/{sem}/search |
Free | Search with filters + pagination |
GET /courses/{sem}/list |
Free | Paginated full list |
GET /courses/{sem}/course/{id} |
Free | Single course by composite ID |
GET /courses/{sem}/nrc/{nrc} |
Free | Single course by NRC |
GET /courses/{sem}/initials/{ini} |
Free | All sections by course initials |
GET /courses/{sem}/schools |
Free | Distinct schools |
GET /courses/{sem}/areas |
Free | Distinct areas |
GET /courses/{sem}/categories |
Free | Distinct categories |
GET /courses/{sem}/campuses |
Free | Distinct campuses |
GET /courses/{sem}/formats |
Free | Distinct formats |
GET /courses/{sem}/programs |
Free | Distinct programs |
GET /courses/{sem}/teachers |
Free | Distinct teachers |
GET /courses/{sem}/stream |
Pro+ | NDJSON streaming of all courses |
GET /courses/{sem}/stats |
Premium+ | Aggregated semester statistics |
| Endpoint | Description |
|---|---|
GET /admin/courses/update-status |
Current updater state |
POST /admin/courses/update-check |
Trigger immediate GitHub check |
POST /admin/courses/update-frequency |
Change check interval |
| Endpoint | Description |
|---|---|
GET /admin/ |
List admin endpoints |
POST /admin/keys |
Create API key |
GET /admin/keys/list |
List all API keys |
PATCH /admin/keys/{name} |
Deactivate API key |
GET /admin/authenticated |
Check authentication |
Environment variables for configuration (see .env.example):
# Courses database (buscacursos data)
COURSES_DATABASE_URL=sqlite:///./data/courses.sqlite
GITHUB_TOKEN= # optional, for release API
COURSES_UPDATE_INTERVAL_SECONDS=2592000 # 30 days
# Auth database
AUTH_DATABASE_URL=sqlite:///./auth_data/api_keys.db
# Server
API_URL=0.0.0.0
API_PORT=8000
# CORS
CORS_ORIGINS=*
# Observability (optional)
ENABLE_OBSERVABILITY=0
OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317
OTEL_SERVICE_NAME=uc-buscacursos-api
# Debug (set 1 to skip auth)
DEBUG=0- Set
ENABLE_OBSERVABILITY=1to turn on traces/metrics/log correlation. - Point
OTEL_EXPORTER_OTLP_ENDPOINTto your Tempo/OTel Collector (defaulthttp://localhost:4317). - Prometheus can scrape
/metrics(OpenMetrics format with exemplars); Loki can ingest stdout logs that now include OTEL trace/span fields viaLoggingInstrumentor. - Service name defaults to
uc-buscacursos-api(configure withOTEL_SERVICE_NAME).
- Install the Loki Docker logging plugin on the host:
docker plugin install grafana/loki-docker-driver:3.3.2-amd64 --alias loki --grant-all-permissions docker plugin enable loki - In Portainer, create a stack with Grafana, Prometheus, Tempo, and Loki (use the official images). Use a shared network so they can reach each other:
services: tempo: image: grafana/tempo:latest command: ["-config.file=/etc/tempo.yaml"] volumes: - ./etc/tempo.yaml:/etc/tempo.yaml prometheus: image: prom/prometheus:latest volumes: - ./etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml loki: image: grafana/loki:latest command: ["-config.file=/etc/loki/local-config.yaml"] volumes: - ./etc/loki/local-config.yaml:/etc/loki/local-config.yaml grafana: image: grafana/grafana:latest ports: ["3000:3000"] volumes: - ./etc/grafana/:/etc/grafana/provisioning/datasources - ./etc/dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml - ./etc/dashboards:/etc/grafana/dashboards
- Run the API container on the same host network, mounting data and pointing to the observability stack:
services: api: image: ghcr.io/jj-sm/universal-api:latest env_file: .env environment: ENABLE_OBSERVABILITY: "1" OTEL_EXPORTER_OTLP_ENDPOINT: "http://tempo:4317" OTEL_SERVICE_NAME: "uc-buscacursos-api" logging: driver: loki options: loki-url: "http://loki:3100/api/prom/push"
- In Grafana, add data sources:
- Prometheus:
http://prometheus:9090with exemplars pointing to Tempo. - Tempo:
http://tempo:3200with traces-to-logs usingservice.name. - Loki:
http://loki:3100with a derived field regex(trace_id)=(\\w+)-> Tempo.
- Prometheus:
- Scrape the API at
http://api:8000/metricsfrom Prometheus. Traces flow via OTLP to Tempo. Logs are shipped via the Loki driver and already include OTEL trace/span IDs fromLoggingInstrumentor.
uc-buscacursos-api/
├── app/
│ ├── main.py # FastAPI app setup + background updater
│ ├── auth_models.py # Tier-based API key models
│ ├── auth_db.py # Auth database connection
│ ├── course_db.py # Raw sqlite3 for semester tables
│ ├── course_updater.py # GitHub release checker / DB merger
│ ├── deps.py # Authentication & rate limiting
│ ├── docs/responses/
│ │ └── courses.py # OpenAPI response examples
│ └── routers/
│ ├── courses.py # All BuscaCursos endpoints
│ ├── admin_courses.py # Update management endpoints
│ └── admin.py # API key management endpoints
├── data/ # SQLite databases (gitignored)
├── docs/
│ └── COURSES_API.md # Full endpoint reference
├── test/
│ ├── test_courses.py # Courses endpoint tests
│ └── test_api_endpoints.py # General API tests
└── .env.example # Environment template
# Setup project
./scripts/setup.sh
# Run development server with auto-reload
./scripts/run-dev.shFollow the patterns in routers/template.py:
- Define Pydantic models for request/response validation
- Extract rate limit info from
get_api_key()dependency - Implement business logic
- Include appropriate error handling
- Register router in
main.py
See ROUTER_MIGRATION_GUIDE.md for detailed examples.
# Interactive docs
http://localhost:8000/docs
# ReDoc documentation
http://localhost:8000/redoc
# Health check
./scripts/verify.shModify tier limits by editing TIER_CONFIG in app/auth_models.py:
TIER_CONFIG = {
"free": {
"requests_per_second": 10,
"max_burst": 20,
"description": "Free tier",
"features": ["read-only access"]
},
"pro": {
"requests_per_second": 100,
"max_burst": 200,
"description": "Professional tier",
"features": ["read-write access", "advanced analytics"]
},
# ... more tiers
}- Quick Start: Read DEVELOPER_QUICKSTART.md
- Complete Setup: See API_TEMPLATE_GUIDE.md
- API Docs: Run the server and visit
http://localhost:8000/docs - Examples: Check
routers/template.pyandrouters/courses.py - Issues: Report on GitHub issues tracker
This project is licensed under the MIT License. See LICENSE file for details.
v1.0.0 - Universal API Template