Async Python client for NTNU course data. Wraps the three public, no-auth data sources for course information at the Norwegian University of Science and Technology behind one typed interface — built as a stepping stone toward an MCP server exposing course data to LLM agents.
| Namespace | Data | Backing source |
|---|---|---|
client.courses |
catalog search, per-course schedules & weekly timetables | ntnu.no course-page JSON (Liferay) |
client.grades |
grade distributions per course/year/semester | HK-dir DBH statistics API |
client.semesters |
term ids (26h/26v), teaching weeks, exam periods |
NTNU's TP timetable system |
See docs/api-research.md for the full survey of NTNU data sources (including the dead and auth-gated ones) and the exact upstream request shapes.
import asyncio
from ntnu_api import NTNUClient
async def main() -> None:
async with NTNUClient() as client:
# Server-side course search (one 500-course page)
page = await client.courses.search(2026, "objektorientert")
for hit in page.courses[:3]:
print(hit.course_code, hit.course_name)
# Full catalog iteration
async for result_page in client.courses.search_all(2026):
...
# Dated teaching activities and the summarized weekly grid
activities = await client.courses.schedules("TDT4100", 2026)
grid = await client.courses.timetable("TDT4100", 2026)
# Grade distribution (accepts bare or DBH-versioned codes)
for row in await client.grades.distribution("TDT4100", years=[2023]):
print(row.year, row.semester_name, row.grade, row.total)
# Terms as known by NTNU's timetable system
current = await client.semesters.current()
asyncio.run(main())A small ntnu command wraps every client method for quick terminal poking:
uv run ntnu search 2026 maskinlæring # catalog search (--all, --page, --sort)
uv run ntnu schedules TDT4100 2026 # dated activities, Oslo-local times
uv run ntnu timetable TDT4100 2026 # weekly grid
uv run ntnu grades TDT4100 --years 2023 # grade distribution table
uv run ntnu versions TDT4100 # DBH course-code versions
uv run ntnu semesters --current # terms known by TP
uv run ntnu search 2026 ai --json | jq ... # every subcommand takes --jsonAll models are pydantic v2; all errors derive from ntnu_api.NTNUAPIError
(with NotFoundError, RateLimitError, APITimeoutError, ... subclasses).
Transient failures (429, 502–504, connection errors) are retried with
exponential backoff, honoring Retry-After.
A word of caution: the ntnu.no endpoints are NTNU's own internal AJAX surface, not a documented API — treat results accordingly and be polite (the client ships a descriptive User-Agent by default).
The mcp extra ships an MCP server exposing the client as six tools
(search_courses, get_course_schedule, get_weekly_timetable,
get_grade_distribution, get_course_versions, get_semesters) with
LLM-shaped responses: English names with Norwegian fallback, Oslo-local
times, grade rows collapsed into per-term distribution tables with
percentages, and an in-process TTL cache for the catalog and semester data.
uv sync --extra mcp # or: pip install 'ntnu-api[mcp]'
claude mcp add ntnu -- uv run ntnu-mcp # register with Claude Code (stdio)
uv run ntnu-mcp --transport streamable-http --port 8000 # hostable HTTP service
npx @modelcontextprotocol/inspector uv run ntnu-mcp # interactive testing UIFor Claude Desktop, add to claude_desktop_config.json:
{"mcpServers": {"ntnu": {"command": "uv", "args": ["run", "--directory", "/path/to/NTNU_python_api", "ntnu-mcp"]}}}- uv — dependency & virtualenv management
- httpx + pydantic v2 — transport and models
- ruff — linting + formatting
- ty — static type checking
- pytest +
pytest-asyncio+ respx — testing - mise — pins Python/uv and exposes task shortcuts
- Dev Container — reproducible environment
Inside the Dev Container (or after uv sync on the host):
| Task | Command |
|---|---|
mise test |
uv run pytest |
mise lint |
uv run ruff check . && uv run ruff format --check . |
mise fmt |
uv run ruff format . && uv run ruff check --fix . |
mise typecheck |
uv run ty check |
mise check |
lint + typecheck + test |
The default test run is fully mocked (respx) against real captured payloads
in tests/fixtures/. To also verify against the live endpoints:
NTNU_API_LIVE=1 uv run pytest tests/test_live.py- MCP server exposing the client as tools (
ntnu-mcp) - Expose the remaining catalog search facets (campus, level, faculty/institute)
- Course detail scraping (description, credits, prerequisites — HTML-only upstream)
- TP
emner.phpsemester catalog (English names, campus ids — see docs/api-research.md) - TP iCal export as a secondary timetable source once semesters publish