Metadata-Version: 2.5
Name: anu-pandc
Version: 0.5.1
Summary: A command-line interface to ANU curriculum data: Programs & Courses, the Policy Library, University legislation, the class and exam timetables and the university calendar, as Markdown, JSON or CSV.
Project-URL: Homepage, https://github.com/smcclab/anu-pandc
Project-URL: Repository, https://github.com/smcclab/anu-pandc
Project-URL: Issues, https://github.com/smcclab/anu-pandc/issues
Author-email: Charles Martin <cpm@charlesmartin.au>
License-Expression: MIT
License-File: LICENSE
Keywords: anu,cli,curriculum,higher-education,legislation,policy,programs-and-courses,timetable
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
Classifier: Topic :: Text Processing :: Markup :: HTML
Requires-Python: >=3.11
Requires-Dist: beautifulsoup4>=4.12
Requires-Dist: click>=8.1
Requires-Dist: requests>=2.31
Requires-Dist: rich>=13.0
Description-Content-Type: text/markdown

# anu-pandc

[![tests](https://github.com/smcclab/anu-pandc/actions/workflows/test.yml/badge.svg)](https://github.com/smcclab/anu-pandc/actions/workflows/test.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)

A command-line interface to [ANU Programs &
Courses](https://programsandcourses.anu.edu.au) 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)](https://smcclab.github.io) 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:

- **Programs & Courses** — courses, programs, majors/minors, class summaries,
  who is convening something, and how to work out which year and teaching
  period a question is actually about:
  <https://raw.githubusercontent.com/smcclab/anu-pandc/main/docs/reading-pandc-directly.md>
- **The other sources** — the Policy Library, University legislation, the
  class timetable, the exam timetable and the university calendar:
  <https://raw.githubusercontent.com/smcclab/anu-pandc/main/docs/reading-anu-sources-directly.md>

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](https://pypi.org/project/anu-pandc/);
with [uv](https://docs.astral.sh/uv/):

```bash
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:

```bash
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:

```bash
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](https://github.com/smcclab/anu-pandc/blob/main/docs/reading-pandc-directly.md) and
[docs/reading-anu-sources-directly.md](https://github.com/smcclab/anu-pandc/blob/main/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

```bash
# 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

```python
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

```bash
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](CONTRIBUTING.md) for how to fix a parser when ANU changes
a page, and [CHANGELOG.md](CHANGELOG.md) for what has changed between versions.

## Further reading

- [docs/reading-pandc-directly.md](https://github.com/smcclab/anu-pandc/blob/main/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](https://raw.githubusercontent.com/smcclab/anu-pandc/main/docs/reading-pandc-directly.md).
- [docs/reading-anu-sources-directly.md](https://github.com/smcclab/anu-pandc/blob/main/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](https://raw.githubusercontent.com/smcclab/anu-pandc/main/docs/reading-anu-sources-directly.md).
- [docs/anu-information-landscape.md](https://github.com/smcclab/anu-pandc/blob/main/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](https://github.com/smcclab/anu-pandc/blob/main/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](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](https://programsandcourses.anu.edu.au).
