Skip to content
smcclabPublic

About

A command-line interface to ANU Programs & Courses, Timetabling, Calendar and Policies: programs, majors/minors/specialisations, courses, class summaries, timetable, calendar, policies, procedures and legislation, as Markdown, JSON or CSV

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

anu-pandc

tests Python 3.11+ License: MIT

A command-line interface to ANU Programs & Courses and the other ANU sources a curriculum question runs into: the Policy Library, University legislation, the class timetable, the exam timetable and the university calendar.

Unofficial. A tool from the Sound, Music and Creative Computing Lab (SMCClab) in the ANU School of Computing, for our own curriculum work. It is not an official ANU system and is not endorsed or supported by ANU. It reads the public ANU website converting data into a convenient format and ships no data of its own.

Point it at any program, major/minor/specialisation or course code and a year. It prints the page as clean Markdown (or JSON), or saves a whole year of a school's curriculum as a tree of small files you can grep, diff and commit.

It also fetches per-class summary pages (convener, dates, real assessment schedule), lists every course under a subject prefix from the catalogue search API, and extracts planned offerings for future years before P&C publishes them.

Beyond P&C it reads five more sources, because a question about coursework rarely stays inside one of them. What a course must do comes from legislation; how the University applies that comes from the Policy Library; when it happens comes from the university calendar; where the class meets comes from the timetable; and when and where its exam sits comes from the exam timetable, once the Examinations Office has published it.

If you are an agent or chat without a shell

Stop here. Everything below this section is a command-line tool you cannot run. Fetch these two files and follow them instead — they are written for you, and they have the URL shapes, the parsing traps and the etiquette:

Which one you need depends on the question, and getting that wrong is the commonest way to answer confidently and wrongly:

The question The source The file
What is this course, what does it require, who convenes it? P&C the first file
When and where does the class actually meet? MyTimetable the second
What week is it? Census, breaks, exams, results? University calendar the second
What is on in one particular week? the timetable, narrowed to that week the second
When and where is this course's exam? the exam timetable, when published the second
What must happen, and who may decide it? Legislation, then the Policy Library the second

A question with a date in it ("this week", "is it census yet", "what's on in September") is a calendar question before it is anything else — resolve the date against the calendar first, then go to the timetable or the class summary. Course pages are year-scoped, so resolve the year explicitly before you fetch anything. And a calendar range that says a class is not running when the timetable schedules it means you paired the range wrongly: "Return from teaching break" is the first day back.

Install

Python 3.11 or newer. It is on PyPI; with uv:

uv tool install anu-pandc
anu-pandc --help

Or run it without installing anything, which is the easy way to hand it to a script or a coding agent:

uvx anu-pandc get COMP1730 --year 2026

Or with pip: pip install anu-pandc. To depend on it from another project, add anu-pandc to your dependencies.

For the latest unreleased changes, install from this repository instead: uvx --from git+https://github.com/smcclab/anu-pandc.git anu-pandc ....

Status

Best effort. It reads pages ANU changes without notice, so a P&C redesign will break it until the parsers are updated. It is polite by default: one session, a self-identifying User-Agent, and a quarter-second gap between one response and the next request. Turn that up with --rate when reading a lot at once, and please don't turn it down to zero against the live site.

Running it elsewhere (sandboxes, CI, agents)

The tool needs outbound HTTPS to install it — pypi.org and files.pythonhosted.org, or github.com when installing from the repository — and to the host behind whichever source you are reading. Sandboxes with an egress allow-list commonly permit the first and refuse the rest, which surfaces as a 403 on the first fetch.

Host Read for
programsandcourses.anu.edu.au programs, courses, class summaries
policies.anu.edu.au the Policy Library
mytimetable.anu.edu.au the class timetable
exams.anu.edu.au the exam timetable
www.anu.edu.au the university calendar, the legislation index
api.prod.legislation.gov.au Federal Register metadata
www.legislation.gov.au Federal Register document text

Only P&C is needed for the P&C commands; the others are only touched by the command that reads them.

A 403 now says which end refused you, and names the host it was refused for. If the response carries the site's own Azure headers, ANU said no; if it carries none of them, something between you and ANU did — add that host to the allow-list. P&C is a plain Azure App Service with no bot-detection layer in front of it, and it answers curl, python-requests and browser User-Agents alike, so a blanket 403 is almost always the network in between.

To check by hand from the environment in question:

curl -sS -D- -o /dev/null https://programsandcourses.anu.edu.au/2026/course/COMP1730

A 200 with Request-Context and ARRAffinity headers means the path is clear. A 403 carrying an x-deny-reason header (for example host_not_allowed) is your own gateway refusing the host, and no header, proxy or DNS change will get around it — the host has to be allow-listed. Claude Code's cloud sandboxes refuse it this way today, while GitHub is permitted, so installing from the repository works and fetching does not.

ANU_PANDC_USER_AGENT overrides the identifying User-Agent if you need to say who you are differently; standard HTTPS_PROXY / NO_PROXY variables are honoured, since the tool is built on requests.

Everything that reads a saved tree — offerings --from, conveners --from, and the parsers in anu_pandc.parse — works with no network at all, so an agent that cannot reach ANU can still work from data someone else committed.

If the tool is blocked but the agent has a web-fetch or browser tool that is not, it can read these sites directly over plain HTTPS: docs/reading-pandc-directly.md and docs/reading-anu-sources-directly.md give it the URL shapes and the recipes. That is a last resort for sandboxes — anywhere the CLI runs, use the CLI.

Quick start

# Look at one thing. The code's shape says what it is.
anu-pandc get COMP1730 --year 2026            # a course
anu-pandc get BCOMP --year 2026               # a program
anu-pandc get ARTIF-SPEC --year 2026          # a specialisation (also *-MAJ, *-MIN)
anu-pandc get 7706XMCOMP --year 2026 --json   # structured output instead of Markdown

# Save a program and everything under it: its majors/minors/specialisations,
# then every course any of them names.
anu-pandc get BCOMP HCOMP --year 2026 --save ./data --recursive

# Every COMP course in the 2026 catalogue, including ones no program names.
anu-pandc catalogue COMP --year 2026 --save ./data

# Now fetch any courses the catalogue found that --recursive didn't.
anu-pandc courses --year 2026 --save ./data

# Class summaries (convener, census date, assessment with due dates).
anu-pandc classes COMP1100 --year 2026
anu-pandc classes --year 2026 --save ./data --prefix COMP   # every saved COMP course

# Which sessions each course runs in, for this year and the future years
# already loaded on the course pages.
anu-pandc offerings --year 2027 --from ./data --prefix COMP --save ./data

# Who convened what, from the saved class pages.
anu-pandc conveners --from ./data --prefix COMP -o conveners.csv

# The policy and the legislation behind an assessment question.
anu-pandc policy search "assessment extension"
anu-pandc policy get "Student assessment (coursework)"
anu-pandc legislation get "Coursework Awards Rule"

# When things happen, and when the class actually meets.
anu-pandc calendar --year 2026 --ranges
anu-pandc timetable COMP3300 --year 2026
anu-pandc timetable COMP1100 --year 2026 --week 2026-09-21   # just that week
anu-pandc exams --list                        # which exam timetables are open now
anu-pandc exams COMP1100 COMP2300             # exam date, time and rooms

Markdown is rendered nicely when printing to a terminal; pipe it or pass --plain to get the raw text. Progress goes to stderr so stdout is safe to redirect.

Commands

Command What it does
get CODE... --year Y Fetch programs, subplans or courses. --save DIR writes files; --recursive walks program → subplans → courses. --kind overrides the auto-detected type.
courses --year Y --save DIR Bulk-fetch every code in DIR/Y/course-codes.txt (or the codes given). --prefix COMP to restrict.
classes [CODE...] --year Y Fetch class summary pages. All periods by default; --period "First Semester" to narrow. With no code and --save, uses the saved code list.
catalogue PREFIX --year Y Every course under a subject prefix via the search API. -f md/csv/json. Saving also adds the teaching codes to course-codes.txt (--include-research to add HDR shells too).
offerings --year Y Planned sittings per course, split by offering year. Reads saved pages with --from DIR or fetches the codes given. -f csv/md/json.
conveners --from DIR Convener and lecturer per saved class. --aliases FILE to merge name variants.
url CODE... --year Y Just print the P&C URLs.

And, for the sources beyond P&C:

Command What it does
policy get NUMBER-OR-TITLE... One Policy Library document — body, governance metadata (effective date, responsible officer, approving body, the legislation it is made under) and related documents. A title is resolved through the library's search.
policy search TERM Keyword search. Says when a document type is truncated, and which policy list --type has the rest.
policy list Every document in the library, or --type Policy / --topic Students for a slice.
legislation list ANU's own index of the legislation that applies to the University, grouped into Acts, Statutes, Rules and Orders. --resolve adds each item's Register id and status.
legislation get ID-OR-NAME... An instrument's status, commencement, what authorises it, and its full text from the Federal Register.
legislation search TERM Titles on the Register whose name matches.
timetable TERM... --year Y Scheduled classes from MyTimetable, plus the contact hours one student carries. --week YYYY-MM-DD narrows to the Mon–Sun week around a date, which is what "what's on this week" needs — without it the table is a weekly pattern covering every teaching period of the year.
exams [TERM...] Exam dates, times, reading and writing time, and rooms from the exam timetable, for course codes or a subject prefix (COMP). Searches every exam event open right now; --event narrows to one (its db number or words from its name), --year to one year. With no TERM, or --list, prints the open events. Most of the year nothing is published, and it says so.
calendar --year Y The university calendar: census dates, teaching breaks, exam periods, results, public holidays. --ranges pairs the begins/ends events; --find WORD narrows.

Global options: --rate SECONDS (minimum gap between requests, default 0.25), -v to log every fetch, -q for errors only.

Output tree

--save DIR produces:

DIR/2026/
  programs/BCOMP.md            one file per program
  subplans/COMS-MAJ.md         majors, minors, specialisations
  courses/COMP1100.md          one file per course
  classes/COMP1100-FirstSemester-3695.md
  catalogue-COMP.md            all COMP courses that year (code, title, career, units, sessions)
  offerings-COMP.csv           planned sittings for the 2026 offering year
  timetable-COMP3300.md        scheduled classes and contact hours
  exams-COMP1100.md            exam date, time and rooms, when published
  calendar.md                  the university calendar for that year
  course-codes.txt             union of every course code seen so far
  scrape-log.md                append-only record of what was fetched when

Policy documents and legislation have no year — a policy has an effective date and an instrument has a commencement date, and only the current version is published — so they sit above the year directories:

DIR/
  policy/ANUP_004603.md        one Policy Library document
  policy/index.md              a listing of the library
  legislation/F2024L01752.md   one Act, Statute, Rule or Order
  legislation/index.md         ANU's index of University legislation
  scrape-log.md                the log for both of those

Pass -f json (or both -f md -f json) to get, courses and classes to write .json files with the same names. Existing files are skipped unless --force is given, so re-running a bulk command only fetches what is missing.

What gets parsed

Course pages: title, units, level, prerequisites and incompatibilities (split heuristically, with the raw requisite text preserved), co-taught codes, description, learning outcomes, indicative assessment with weights, and the "Offered in" table (year, session, mode, class number, summary link, and the advertised topic title for special-topics shells such as COMP4011), and the Fees tab (student contribution band, EFTSL, and domestic and international fee-paying tuition for the page's year).

Program and subplan pages: title, total units, introduction, learning outcomes, requirement groups as tables of courses, and the list of majors/minors/specialisations offered.

Class summary pages: every section on the page. Convener and lecturer (with email, phone, consulting hours and research interests), mode, start/end/ census/last-enrol dates, description, learning outcomes, assessment summary table with due dates and LO mapping, per-task detail, assessment requirements, moderation, examinations, participation, submission, late-submission, returning, extension and resubmission rules, class schedule, required and recommended resources, staff and student feedback, other information, tutorial registration, and the university-wide academic integrity, policies, privacy, grade distribution and student support text.

Policy Library documents: the document body as Markdown — with its clause numbering intact, which matters because a policy cites its own clauses — plus document type and number, effective and next-review dates, responsible officer, approving body, contact area, the legislation it is made under, and the procedures and forms related to it.

Legislation: status and whether it is in force, making and commencement dates, compilation number, the instrument that authorises it, and the full text from the Federal Register's EPUB.

Timetable: one row per scheduled activity — group, stream, type, day, time, duration, room, session count from the explicit date list, and the co-taught course codes — plus contact hours for one student, counting one stream per activity group rather than all the alternatives.

Exam timetable: one row per exam — the co-taught courses sitting it, exam title, sitting type, date, start, end (start plus reading and writing time), reading and writing minutes, and every room it is spread across, folded together from the one-row-per-room table the site publishes.

University calendar: every event, and the begins/ends pairs reassembled into date ranges.

Notes on the other sources

  • A policy's clause numbers live in <ol start="..."> attributes, not in the text. Cross-references like "in accordance with Clause 73" only line up because the parser reads them.
  • Policy Library search shows five hits per document type and says how many there were; policy list --type has the rest.
  • The Federal Register's /latest/text URL is a JavaScript shell. The document text is in the EPUB, at a URL built from the version's start date, which is why fetching an instrument is two requests.
  • A legislation name matches every version ever made of it, most of them repealed. legislation get prefers the in-force principal version and says so when what it found is not in force.
  • MyTimetable runs one instance per parity of the year — currently 2025 and 2026 — and has no year parameter, so no other year can be read from it.
  • The timetable is the scheduled one. It changes, it does not say who taught, and it is not evidence that anything was delivered.
  • The exam timetable is only there while an event is open. Each exam event (end of semester, in-class and online, deferred) is released when the Examinations Office is ready and closes on a fixed date, so most of the year exams finds nothing, and that is the right answer. Its db numbers are recycled slots, not ids — db=14 was Semester 1 2024 — so the tool reads the index rather than guessing them.
  • Co-taught courses sit one exam under a combined code (COMP1110/COMP1140/COMP6710_Semester 2); searching any one of them finds it. The site's search is a prefix match, so COMP finds every COMP exam.
  • Calendar events are all single days; ranges are published as separate begins/ends events with wording that drifts between years. --ranges pairs them. Cite the calendar page, not the feed.

Notes on P&C behaviour

  • A course page carries class rows for every future year the schedule has been loaded for, not just its own year. That is why offerings produces one table per offering year and can show a year that has no catalogue yet.
  • Program pages for a future year are often published before its subplan or course pages exist.
  • Unknown codes return HTTP 200 with a "page doesn't exist" body. The tool detects that and reports an error rather than saving it.
  • Requests are rate-limited (0.25 s between one response and the next request) and sent with an identifying User-Agent. Please keep it that way.

Using it as a library

from anu_pandc.scrape import fetch_item
course = fetch_item("COMP1730", "2026")
course.data["learning_outcomes"]
course.markdown()

The parsers in anu_pandc.parse take a BeautifulSoup tree and return plain dicts, so they can be run over saved HTML too.

Development

git clone https://github.com/smcclab/anu-pandc.git
cd anu-pandc
uv sync
uv run pytest

Tests run against saved HTML fixtures in tests/fixtures/; no network needed. See CONTRIBUTING.md for how to fix a parser when ANU changes a page, and CHANGELOG.md for what has changed between versions.

Further reading

  • docs/reading-pandc-directly.md — the fallback for sandboxed agents that cannot reach ANU through this tool: how to read P&C with nothing but a web-fetch tool — the URL shapes for courses, programs, subplans and class summaries, how to find who is convening something, how to tell an error page from a real one, and how to work out which year and teaching period a question is actually about. Written for an agent or chat that cannot run this CLI. Point one at the raw file.
  • docs/reading-anu-sources-directly.md — the same fallback for the sources beyond P&C: which source answers which kind of question, the URL shapes for the Policy Library, the Federal Register, MyTimetable, the exam timetable and the university calendar, and the quirks that make each of them easy to read wrongly. Raw file.
  • docs/anu-information-landscape.md — a survey of where ANU publishes student-facing course information, how the seven sources fit together (and where they don't), and the practical problems with each: how each system handles time, pages that don't exist until released, and what would help most.
  • docs/pc-api.md — the undocumented JSON endpoints behind the P&C catalogue search, what they return, and the quirks worth knowing (the page size cap, which server-side filters silently do nothing, how far back SelectedYear goes).

Licence

MIT — see LICENSE. Written by Charles Martin for curriculum work in the ANU School of Computing. The course data it reads belongs to ANU and is published at Programs & Courses.

About

A command-line interface to ANU Programs & Courses, Timetabling, Calendar and Policies: programs, majors/minors/specialisations, courses, class summaries, timetable, calendar, policies, procedures and legislation, as Markdown, JSON or CSV

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages