ntu-cool-cli 1.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ntu_cool_cli-1.3.1/LICENSE +21 -0
- ntu_cool_cli-1.3.1/PKG-INFO +505 -0
- ntu_cool_cli-1.3.1/README.md +474 -0
- ntu_cool_cli-1.3.1/pyproject.toml +49 -0
- ntu_cool_cli-1.3.1/scripts/ntu_cool_cli.egg-info/PKG-INFO +505 -0
- ntu_cool_cli-1.3.1/scripts/ntu_cool_cli.egg-info/SOURCES.txt +11 -0
- ntu_cool_cli-1.3.1/scripts/ntu_cool_cli.egg-info/dependency_links.txt +1 -0
- ntu_cool_cli-1.3.1/scripts/ntu_cool_cli.egg-info/entry_points.txt +2 -0
- ntu_cool_cli-1.3.1/scripts/ntu_cool_cli.egg-info/requires.txt +1 -0
- ntu_cool_cli-1.3.1/scripts/ntu_cool_cli.egg-info/top_level.txt +1 -0
- ntu_cool_cli-1.3.1/scripts/ntu_fetch.py +1091 -0
- ntu_cool_cli-1.3.1/setup.cfg +4 -0
- ntu_cool_cli-1.3.1/tests/test_ntu_fetch.py +1065 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gary Xie and ntu-cool-cli contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ntu-cool-cli
|
|
3
|
+
Version: 1.3.1
|
|
4
|
+
Summary: Read-only CLI for NTU Cool: course announcements, course files, handouts, and submission status.
|
|
5
|
+
Author-email: Gary Xie <gary.1020.xie@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Gary1020-byte/ntu-cool-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/Gary1020-byte/ntu-cool-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/Gary1020-byte/ntu-cool-cli/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Gary1020-byte/ntu-cool-cli/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: ntu,cool,ceiba,cli,announcements,course,files,homework,handout,student,scraper,read-only
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Natural Language :: Chinese (Traditional)
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Education
|
|
25
|
+
Classifier: Topic :: Utilities
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: requests>=2.28
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# ntu-cool-cli
|
|
33
|
+
|
|
34
|
+
A CLI and an agent skill for read-only access to NTU Cool: course
|
|
35
|
+
announcements, course files, CA handouts, and submission status.
|
|
36
|
+
|
|
37
|
+
Use it from a shell, or install it as a skill and let an agent drive it.
|
|
38
|
+
|
|
39
|
+
> 中文版: [README.zh-TW.md](README.zh-TW.md)
|
|
40
|
+
|
|
41
|
+
One CLI, four stores. Cool keeps a course's material in more than one place, and
|
|
42
|
+
the obvious one is often empty:
|
|
43
|
+
|
|
44
|
+
| what you want | where it actually is |
|
|
45
|
+
|---|---|
|
|
46
|
+
| what a course announced | `discussion_topics` — announcements and their attachments |
|
|
47
|
+
| course files: handouts, worksheets, solutions, syllabus | the Files tree (`folders`) |
|
|
48
|
+
| a CA handout, when the Files tree looks empty | the **assignment description** (`assignments`) |
|
|
49
|
+
| lectures grouped by week | **modules** (`modules` → `modules/{id}/items`) |
|
|
50
|
+
| whether your submission was accepted | `/submissions/self` |
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# no credentials — is the platform reachable?
|
|
54
|
+
python3 scripts/ntu_fetch.py --probe
|
|
55
|
+
|
|
56
|
+
# what courses am I taking this term?
|
|
57
|
+
python3 scripts/ntu_fetch.py --list-files --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Read-only.** It logs in to read and never writes, posts, or comments on
|
|
61
|
+
cool.ntu.edu.tw.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Why not a service
|
|
66
|
+
|
|
67
|
+
A daemon that polls on a schedule and pushes to Telegram needs a persistent
|
|
68
|
+
store, an ORM, and a migration surface — to answer a one-endpoint question.
|
|
69
|
+
On Python 3.14 that stack fails three ways before it reaches the endpoint:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
ImportError: cannot import name 'Markup' from 'jinja2' # Flask 1.0.2
|
|
73
|
+
AttributeError: module 'sqlite3.dbapi2' has no attribute 'version_info' # SQLAlchemy 1.3.8
|
|
74
|
+
TypeError: Mapper.__init__() got an unexpected keyword argument 'order_by'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Upgrading Flask cascades into the SQLAlchemy break, which cascades into a
|
|
78
|
+
mapper-API removal. The total patch surface is one file and four lines — which
|
|
79
|
+
tells you the service is the problem, not the pins.
|
|
80
|
+
|
|
81
|
+
This script drops Flask, SQLAlchemy, and `beautifulsoup4` entirely. **The only
|
|
82
|
+
third-party import is `requests`.** HTML parsing uses `html.parser` from the
|
|
83
|
+
stdlib, because the two things needed from BeautifulSoup — "find a form, read
|
|
84
|
+
its inputs" and "flatten visible text" — are twenty lines of `HTMLParser`.
|
|
85
|
+
|
|
86
|
+
Fetching is also the honest scope: a script that reads and exits has nothing to
|
|
87
|
+
keep in sync. Storage, dedupe, and delivery belong to whatever runs it.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Install
|
|
92
|
+
|
|
93
|
+
As a command — this gives you `ntu-cool-cli` on your PATH.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pipx install ntu-cool-cli # or: uv tool install ntu-cool-cli
|
|
97
|
+
ntu-cool-cli --probe
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or with plain pip, into your user site:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python3 -m pip install --user ntu-cool-cli
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**From Git instead** — if you want the unreleased tip, or to pin a commit:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
uv tool install 'git+https://github.com/Gary1020-byte/ntu-cool-cli.git'
|
|
110
|
+
pipx install 'git+https://github.com/Gary1020-byte/ntu-cool-cli.git'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**From a local checkout:**
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/Gary1020-byte/ntu-cool-cli.git
|
|
117
|
+
cd ntu-cool-cli
|
|
118
|
+
pipx install . # or: uv tool install . / python3 -m pip install --user .
|
|
119
|
+
ntu-cool-cli --probe
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The module is `ntu_fetch`, the command is `ntu-cool-cli`, and both do exactly the
|
|
123
|
+
same thing — `ntu-cool-cli --help` and `python3 scripts/ntu_fetch.py --help` are
|
|
124
|
+
identical. Use whichever suits; the script needs no install at all.
|
|
125
|
+
|
|
126
|
+
Requires Python 3.9+ and `requests`. Verify:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
ntu-cool-cli --probe
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Tests
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python3 -m unittest discover -s tests -v
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
52 offline tests, no network and no credentials. They cover the JSON prefix
|
|
139
|
+
handling, HTML form extraction without BeautifulSoup, the credential-file
|
|
140
|
+
permission guard, and — the one that matters — that a trap form parked in
|
|
141
|
+
`<head>` can never become a POST target.
|
|
142
|
+
|
|
143
|
+
Two classes guard the bug that was silently losing files: one drives
|
|
144
|
+
`_paged_get` across a multi-page Link chain, one proves the walk enters through
|
|
145
|
+
the root and still works when `/folders/root` answers 403.
|
|
146
|
+
|
|
147
|
+
### Note
|
|
148
|
+
|
|
149
|
+
The console script is a thin wrapper: `ntu_fetch:main`, with `main()` returning
|
|
150
|
+
the exit code. Importing the module has no side effects, so the tests can import
|
|
151
|
+
it without logging in or reaching the network.
|
|
152
|
+
|
|
153
|
+
> **Test with the interpreter you will actually use.** A virtualenv's Python and the
|
|
154
|
+
> system Python have separate `site-packages`. A bug report that says "the import
|
|
155
|
+
> fails" is often a report about the wrong interpreter.
|
|
156
|
+
> See `references/testing-and-interpreters.md`.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Credentials
|
|
161
|
+
|
|
162
|
+
Two platforms need your NTU account. **This tool never stores, requests, or prints a
|
|
163
|
+
password.**
|
|
164
|
+
|
|
165
|
+
Either export them for one shell:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
export NTU_USER=bXXXXXXXX
|
|
169
|
+
export NTU_PASS='...'
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
…or, better, keep them in a file the script will only read when it is mode `600`:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
umask 077
|
|
176
|
+
echo '{"ntu_user":"bXXXXXXXX","ntu_pass":"..."}' > ~/.ntu-creds.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A group- or world-readable credential file is **refused** with the `chmod` command to
|
|
180
|
+
fix it. Note that environment variables exported in one terminal are not visible to
|
|
181
|
+
another — a credential file is what makes this script usable unattended (cron, etc.).
|
|
182
|
+
|
|
183
|
+
### Where the password can and cannot leak
|
|
184
|
+
|
|
185
|
+
The password is submitted to ADFS as a form field. Four things it could have
|
|
186
|
+
done, and what it actually does:
|
|
187
|
+
|
|
188
|
+
| leak surface | what the tool does |
|
|
189
|
+
|---|---|
|
|
190
|
+
| printed in an error message | it doesn't — the login failure names the hop and the HTTP status, never the submitted values |
|
|
191
|
+
| dumped by `requests` debug logging | it doesn't — `enable_http_debugging()` and `debuglevel` are absent, and a test asserts they stay absent |
|
|
192
|
+
| left in the output as a traceback | it doesn't — the traceback would carry the *call*, not the value; the value only ever exists in the POST body |
|
|
193
|
+
| sitting in a world-readable file | it refuses to read one |
|
|
194
|
+
|
|
195
|
+
**The real risks are on your side of the boundary:**
|
|
196
|
+
|
|
197
|
+
- **The credential file's path.** `--cred-file ~/.ntu-creds.json` goes into
|
|
198
|
+
your shell history, and the path is visible in the process list. That is
|
|
199
|
+
why the environment-variable path is the better one when you are typing
|
|
200
|
+
the command by hand — and the file path is the better one when a cron job
|
|
201
|
+
needs it unattended.
|
|
202
|
+
- **The signed download URLs.** A file's `url` from the API authenticates.
|
|
203
|
+
The tool never logs one, and it never writes one into a file it produces.
|
|
204
|
+
Don't paste one into an issue.
|
|
205
|
+
- **The session cookies.** They live in the `requests.Session` for the length
|
|
206
|
+
of the run and are gone when the process exits. Nothing is written to disk.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## The three traps this encodes
|
|
211
|
+
|
|
212
|
+
### 1. `/api/v1/courses` is not your current course list
|
|
213
|
+
|
|
214
|
+
It returns courses the account has **ever** been enrolled in.
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
API said: 10 courses, newest announcement 2026-10-13, oldest 2025-09-05
|
|
218
|
+
→ omitted 3 current-term courses, one of them your main math course
|
|
219
|
+
→ included 3 dead courses as {"access_restricted_by_date": true}
|
|
220
|
+
|
|
221
|
+
Home page says: 7 courses, term "<term>" ← correct
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`fetch_courses()` parses the roster out of the home page's embedded JSON. The
|
|
225
|
+
historical list is still reachable via `fetch_all_courses_historical()` when you want
|
|
226
|
+
last term's exam schedule — never as the current roster.
|
|
227
|
+
|
|
228
|
+
### 2. Course material is not in the announcement body
|
|
229
|
+
|
|
230
|
+
Measured on one term:
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
20 announcement bodies 2,041 characters 0 file links
|
|
234
|
+
course Files tree 91 files
|
|
235
|
+
announcement attachments 5 files <- a third set
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
An announcement whose entire body is the word `attached` is **not** a missing-
|
|
239
|
+
attachment case to report — it is the normal shape, and the files are fully
|
|
240
|
+
retrievable. Reporting "this course has no attachments" after reading announcements
|
|
241
|
+
alone reads as complete and misses every worksheet.
|
|
242
|
+
|
|
243
|
+
### 3. Enter through the root, and follow `Link` — three separate traps
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
GET /api/v1/courses/{id}/folders/root → the root folder <- enter here
|
|
247
|
+
folder["files_url"] → files in that folder (paginated)
|
|
248
|
+
folder["folders_url"] → child folders; recurse (paginated)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**(a) The URLs come off the objects.** Six plausible spellings return 404 or 400 —
|
|
252
|
+
including `/api/v1/courses/{id}/files`, which looks obviously right and is dead.
|
|
253
|
+
|
|
254
|
+
**(b) `/folders` is a flat list, not a tree.** It enumerates folders at every depth.
|
|
255
|
+
Iterating it *and* recursing `folders_url` walks each non-root folder twice — three
|
|
256
|
+
times when its parent is listed too. A naive walk reported **110 files where 64 were
|
|
257
|
+
retrievable**. Enter through the root instead.
|
|
258
|
+
|
|
259
|
+
**(c) Every list endpoint is paginated at 10 by default.** This is the trap that
|
|
260
|
+
actually loses data: a folder holding 29 files returns 10, with no error and no
|
|
261
|
+
empty-looking tail. Measured across one term — **27 of 91 files silently missing**.
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
projected from Link header: <...>; rel="next", <...>; rel="last"
|
|
265
|
+
bare request: page=1&per_page=10
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Follow `Link: rel="next"`. Do not "fix" it with a bigger `per_page` — the API caps
|
|
269
|
+
that parameter, and the Link header is the only pagination contract it honours.
|
|
270
|
+
|
|
271
|
+
`fetch_course_files()` enters through `/folders/root` (falling back to the
|
|
272
|
+
parentless entries of `/folders`, because `/folders/root` answers **403** on at
|
|
273
|
+
least one non-credit course), paginates both directions, and dedupes by file `id`
|
|
274
|
+
as a last resort. There are tests that fail if any of the three is removed.
|
|
275
|
+
|
|
276
|
+
Announcement attachments are a **third** set, separate from both: one term had 5 of
|
|
277
|
+
them and 91 in the Files tree. `--list-files` and `--download-files` collect both.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Usage
|
|
282
|
+
|
|
283
|
+
### Announcements
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# current term roster
|
|
287
|
+
python3 scripts/ntu_fetch.py --list-files --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
288
|
+
|
|
289
|
+
# one course's announcements to stdout
|
|
290
|
+
python3 scripts/ntu_fetch.py --course <COURSE> --since 2026-09-01 --stdout
|
|
291
|
+
|
|
292
|
+
# write markdown into a notes directory
|
|
293
|
+
python3 scripts/ntu_fetch.py --course <COURSE> --since 2026-09-01 --out ./captures/
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Course files
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# list everything in the Files tree
|
|
300
|
+
python3 scripts/ntu_fetch.py --list-files --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
301
|
+
|
|
302
|
+
# download, skipping anything over 20 MB
|
|
303
|
+
python3 scripts/ntu_fetch.py --download-files --course <COURSE> \
|
|
304
|
+
--cred-file ~/.ntu-creds.json --files-out ./files/ --max-mb 20
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Downloads land at `<files-out>/<course_id>/<file_id>/<filename>`, so two files with
|
|
308
|
+
the same display name never collide.
|
|
309
|
+
|
|
310
|
+
### Assignments, submissions, pages, modules
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
# the homework roster: name, due_at, lock_at, points
|
|
314
|
+
python3 scripts/ntu_fetch.py --list-assignments --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
315
|
+
|
|
316
|
+
# your own submission status per assignment (read-only)
|
|
317
|
+
python3 scripts/ntu_fetch.py --list-submissions --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
318
|
+
|
|
319
|
+
# course pages: syllabus, calendar, late-submission policy
|
|
320
|
+
python3 scripts/ntu_fetch.py --list-pages --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
321
|
+
|
|
322
|
+
# modules — lectures grouped by week
|
|
323
|
+
python3 scripts/ntu_fetch.py --list-modules --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
324
|
+
|
|
325
|
+
# handout links that live in assignment descriptions, not the Files tree
|
|
326
|
+
python3 scripts/ntu_fetch.py --list-handouts --course <COURSE> --cred-file ~/.ntu-creds.json
|
|
327
|
+
|
|
328
|
+
# one course page's content, by slug (see --list-pages for the slugs)
|
|
329
|
+
python3 scripts/ntu_fetch.py --show-page ke-cheng-da-gang --course <COURSE> \
|
|
330
|
+
--cred-file ~/.ntu-creds.json
|
|
331
|
+
|
|
332
|
+
# download the handouts themselves
|
|
333
|
+
python3 scripts/ntu_fetch.py --download-handout --course <COURSE> \
|
|
334
|
+
--cred-file ~/.ntu-creds.json --files-out ./handouts/
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
`lock_at` is the real deadline; `due_at` is the nominal one, and the gap between
|
|
338
|
+
them is the buffer the course page promises. A `lock_at` in the past means the
|
|
339
|
+
upload window is closed.
|
|
340
|
+
|
|
341
|
+
`--show-page` needs exactly one course — it prints the page's title, its URL,
|
|
342
|
+
and the body as plain text. `--download-handout` follows the two-hop path
|
|
343
|
+
(the endpoint answers with JSON describing the file; the signed URL is inside
|
|
344
|
+
it) and writes the file under its real display name.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Installing this as an agent skill
|
|
349
|
+
|
|
350
|
+
Copy the block below into your agent (Claude Code, Hermes, a custom assistant —
|
|
351
|
+
anything that can run a command and read a file).
|
|
352
|
+
|
|
353
|
+
**The agent must ask which capabilities are needed, then install only those.**
|
|
354
|
+
Each capability maps to a specific flag or a specific section of `SKILL.md`, so
|
|
355
|
+
a partial install is the normal case, not a compromise.
|
|
356
|
+
|
|
357
|
+
````text
|
|
358
|
+
Install the NTU Cool skill: https://github.com/Gary1020-byte/ntu-cool-cli
|
|
359
|
+
|
|
360
|
+
Before installing anything, ask me which capabilities I need. Do NOT install
|
|
361
|
+
all of them by default. The capabilities are:
|
|
362
|
+
|
|
363
|
+
1. Announcements — course announcements, deadlines, TA notices
|
|
364
|
+
flag: --course / --all-courses
|
|
365
|
+
2. Course files — the Files tree: handouts, worksheets, solutions,
|
|
366
|
+
syllabus PDFs
|
|
367
|
+
flags: --list-files, --download-files
|
|
368
|
+
3. Announcement attachments — files bound to one announcement (a separate,
|
|
369
|
+
smaller set from the Files tree)
|
|
370
|
+
included in --list-files / --download-files
|
|
371
|
+
4. Homework roster — /assignments: what is assigned, due_at, lock_at,
|
|
372
|
+
points, and the handout link
|
|
373
|
+
flag: --list-assignments
|
|
374
|
+
5. Submission status — my own submission per assignment: unsubmitted /
|
|
375
|
+
submitted / graded, score, late policy
|
|
376
|
+
flag: --list-submissions
|
|
377
|
+
6. Course pages — syllabus, calendar, late-submission policy,
|
|
378
|
+
submission instructions
|
|
379
|
+
flag: --list-pages
|
|
380
|
+
7. Modules — lectures grouped by week
|
|
381
|
+
flag: --list-modules
|
|
382
|
+
8. CA handouts — handout links that live in assignment descriptions
|
|
383
|
+
rather than the Files tree (the Files tree is often
|
|
384
|
+
nearly empty for CA courses)
|
|
385
|
+
flags: --list-handouts, --download-handout
|
|
386
|
+
9. Page content — the body of one course page: syllabus, calendar,
|
|
387
|
+
late-submission policy, submission instructions
|
|
388
|
+
flag: --show-page <slug>
|
|
389
|
+
|
|
390
|
+
Show me this list, ask which ones I want, and wait for my answer.
|
|
391
|
+
|
|
392
|
+
Then:
|
|
393
|
+
- clone the repo
|
|
394
|
+
- install the `ntu-cool-cli` package:
|
|
395
|
+
`pipx install ntu-cool-cli` (or `uv tool install ntu-cool-cli`, or
|
|
396
|
+
`python3 -m pip install --user ntu-cool-cli`). It is on PyPI. If the
|
|
397
|
+
user wants the unreleased tip instead, install from the Git URL:
|
|
398
|
+
`uv tool install 'git+https://github.com/Gary1020-byte/ntu-cool-cli.git'`
|
|
399
|
+
- read SKILL.md, and keep only the sections matching the capabilities I chose
|
|
400
|
+
- tell me how to supply credentials. Prefer the environment variables
|
|
401
|
+
NTU_USER and NTU_PASS — nothing lands on disk and nothing stays in the
|
|
402
|
+
shell history. The alternative is a JSON file with ntu_user / ntu_pass
|
|
403
|
+
keys, passed with --cred-file; that file must be mode 600, because the
|
|
404
|
+
tool refuses to read a world-readable one. Never write credentials into
|
|
405
|
+
the repo, and never echo them back to me.
|
|
406
|
+
- run `ntu-cool-cli --probe` first. It needs no credentials and tells you
|
|
407
|
+
whether the platform is reachable. Do not debug credentials until the
|
|
408
|
+
probe is clean.
|
|
409
|
+
|
|
410
|
+
This tool is READ-ONLY. It logs in to read and never writes, posts, or
|
|
411
|
+
comments on cool.ntu.edu.tw. It cannot upload a submission — if I need to
|
|
412
|
+
hand something in, tell me what is due and where the file is, and let me do
|
|
413
|
+
the upload in the browser. Never attempt a write against the platform.
|
|
414
|
+
````
|
|
415
|
+
|
|
416
|
+
### Why the agent should ask first
|
|
417
|
+
|
|
418
|
+
Cool keeps a course's material in four different places, and which one holds
|
|
419
|
+
what is not guessable from the outside — the Files tree is nearly empty for
|
|
420
|
+
some CA courses, and a handout that is missing from it is usually in the
|
|
421
|
+
assignment description. Installing all nine capabilities means the agent
|
|
422
|
+
reads a skill file that describes stores it will never query, and every
|
|
423
|
+
capability it does not need is a way for it to answer a question with the
|
|
424
|
+
wrong source.
|
|
425
|
+
|
|
426
|
+
The list also doubles as a map: an agent that knows "the Files tree is empty
|
|
427
|
+
for this course" knows to ask for `--list-handouts` instead of reporting
|
|
428
|
+
"no attachments".
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## What the output is worth trusting
|
|
433
|
+
|
|
434
|
+
| claim | status |
|
|
435
|
+
|---|---|
|
|
436
|
+
| NTU Cool endpoints still exist | verified — 401 + structured body, not 404 |
|
|
437
|
+
| the SAML login sequence | verified against the live site |
|
|
438
|
+
| `/api/v1/courses` as current roster | **false** — see trap 1 |
|
|
439
|
+
| the Files-tree endpoint shape | verified, 91 files enumerated across a term |
|
|
440
|
+
| downloads are really PDFs | magic-checked per file |
|
|
441
|
+
| nothing is silently dropped | verified — pagination followed via `Link` |
|
|
442
|
+
| Ceiba reachability | **varies by network** — see `references/ceiba-unreachable.md` |
|
|
443
|
+
| Ceiba scraping | not implemented; the positional table parser is unverified |
|
|
444
|
+
|
|
445
|
+
**A download reporting `OK` has passed both checks.** Cool answers an expired signed
|
|
446
|
+
URL with HTTP 200 carrying the login page, so status code alone proves nothing —
|
|
447
|
+
the script sniffs for `<!DOCTYPE html` and verifies `%PDF-` against the declared
|
|
448
|
+
content type.
|
|
449
|
+
|
|
450
|
+
Signed file URLs are credential-bearing. The script never logs them; don't paste them
|
|
451
|
+
anywhere.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Many course PDFs have no text layer
|
|
456
|
+
|
|
457
|
+
Of 10 files in one course, **9 had a zero-length text layer** — they are rasterised
|
|
458
|
+
scans or vector drawings. That is not data loss. Render at ~160 dpi and read the
|
|
459
|
+
image:
|
|
460
|
+
|
|
461
|
+
```python
|
|
462
|
+
import pymupdf
|
|
463
|
+
d = pymupdf.open("worksheet.pdf")
|
|
464
|
+
d[0].get_pixmap(dpi=160).save("page1.png") # then read it
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Never file a PDF as unusable on the strength of one extraction path.
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Ceiba
|
|
472
|
+
|
|
473
|
+
`ceiba.ntu.edu.tw` is not implemented. On some networks it is unreachable (DNS
|
|
474
|
+
resolves, three sibling NTU hosts answer in <150 ms, ceiba times out on connect).
|
|
475
|
+
That is a network-access question, not a code one — see
|
|
476
|
+
`references/ceiba-unreachable.md` for the control test that distinguishes them.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## Files
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
SKILL.md the rulebook: verification status, traps, workflow
|
|
484
|
+
scripts/ntu_fetch.py the whole tool
|
|
485
|
+
pyproject.toml makes `ntu-cool-cli` installable
|
|
486
|
+
tests/test_ntu_fetch.py 52 offline tests, no network
|
|
487
|
+
ACKNOWLEDGEMENTS.md attribution and licensing notes
|
|
488
|
+
references/api-findings.md what the platform actually returns
|
|
489
|
+
references/ceiba-unreachable.md reachability control test
|
|
490
|
+
references/testing-and-interpreters.md the venv-vs-system-python trap
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`SKILL.md` is written for an AI agent reading it before acting. It is readable by
|
|
494
|
+
humans too, but its shape is "what is verified, what is not, what will bite you".
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Ethics and scope
|
|
499
|
+
|
|
500
|
+
Read-only. Every request this script makes is a `GET` except the two login form
|
|
501
|
+
submissions required to obtain a session — there is no upload, no write, no delete,
|
|
502
|
+
and no course modification of any kind. It obeys the same rule as the account
|
|
503
|
+
holder: fetch what you can already see by logging in, and nothing more.
|
|
504
|
+
|
|
505
|
+
Use it against your own account.
|