ntu-cool-cli 1.3.3__tar.gz → 1.3.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ntu-cool-cli
3
- Version: 1.3.3
3
+ Version: 1.3.4
4
4
  Summary: Read-only CLI for NTU Cool: course announcements, course files, handouts, and submission status.
5
5
  Author-email: Gary Xie <gary.1020.xie@gmail.com>
6
6
  Maintainer-email: Gary Xie <gary.1020.xie@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ntu-cool-cli"
7
- version = "1.3.3"
7
+ version = "1.3.4"
8
8
  description = "Read-only CLI for NTU Cool: course announcements, course files, handouts, and submission status."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ntu-cool-cli
3
- Version: 1.3.3
3
+ Version: 1.3.4
4
4
  Summary: Read-only CLI for NTU Cool: course announcements, course files, handouts, and submission status.
5
5
  Author-email: Gary Xie <gary.1020.xie@gmail.com>
6
6
  Maintainer-email: Gary Xie <gary.1020.xie@gmail.com>
@@ -1002,7 +1002,7 @@ def main() -> int:
1002
1002
  skipped += 1
1003
1003
  continue
1004
1004
  # display_name is the human filename; `filename` is the
1005
- # storage name and looks like "1789962952_790__lecture_ch1.pdf".
1005
+ # storage name and looks like "<TIMESTAMP>_<ID>__<ORIGINAL-NAME>".
1006
1006
  raw_name = str(f.get("name") or f.get("filename") or f.get("id"))
1007
1007
  safe = os.path.basename(raw_name).replace("/", "_")
1008
1008
  safe = re.sub(r"[^\w.\-㐀-鿿]+", "_", safe).strip("_") or str(f["id"])
@@ -408,15 +408,22 @@ class TestNoPersonalCourseRoster(unittest.TestCase):
408
408
  # 4. the course TITLES and the room numbers, which are the identifying
409
409
  # part: a roster of four real course names with their exam dates
410
410
  # says who this account belongs to as clearly as a course id does
411
+ # 5. the real FILE NAMES -- a course's own worksheet, solution key and
412
+ # optional-practice PDFs. These are the actual coursework files, so
413
+ # a filename is more identifying than a course title: it names the
414
+ # assignment the student sat. A filename rule is what was missing
415
+ # when a reviewer pointed at three real homework filenames still
416
+ # sitting in the published skill.
411
417
  #
412
- # Rules 1-3 are hard identifiers and need no context. Rule 4 needs
418
+ # Rules 1-3 and 5 are hard identifiers and need no context. Rule 4 needs
413
419
  # context, because the same words appear in ordinary prose ("the Files
414
420
  # tree structure", "a maths course"), so a title only counts when it
415
421
  # sits near a course-context marker.
416
422
  #
417
- # Nothing here lists a real course name, student id or password: a
418
- # denylist of the leaked values would itself be the leak. Every rule is
419
- # a shape, so it also catches values added after this file was written.
423
+ # Nothing here lists a real course name, student id, password or
424
+ # filename: a denylist of the leaked values would itself be the leak.
425
+ # Every rule is a shape, so it also catches values added after this
426
+ # file was written.
420
427
  STUDENT_ID_RE = re.compile(r"(?<![A-Za-z0-9])[BR]\d{8}(?![0-9])")
421
428
  COURSE_CODE_RE = re.compile(r"(?<!\d)[56]\d{2}\s?\d{5}(?!\d)")
422
429
  PASSWORD_RE = re.compile(r"authenticate\(\s*[\"'][^\"']+[\"']\s*\)")
@@ -430,6 +437,55 @@ class TestNoPersonalCourseRoster(unittest.TestCase):
430
437
  r"|Electronics|Structures)\b"
431
438
  r"|\b[A-Z][a-z]+ [A-Z][a-z]+ Structures\b")
432
439
  ROOM_RE = re.compile(r"(?<![0-9])\d{2,3}\s?(?:教室|實驗室)")
440
+
441
+ # --- filenames ---
442
+ #
443
+ # A filename is the most identifying string in the whole tree: it names
444
+ # the exact worksheet, solution key or midterm paper the student sat.
445
+ # Redacting the course title while leaving its homework filenames in
446
+ # place leaves the account just as findable.
447
+ #
448
+ # So a filename counts when it carries a piece that identifies:
449
+ # - a Chinese discipline noun (力學 / 數學 / 電路 / 結構 / 物理 / 化學)
450
+ # - a course prefix (a two-to-four letter course tag, then CA / HW /
451
+ # CH / note / mid / exam)
452
+ # - an academic-term prefix (a three-or-four-digit term, then _ or -)
453
+ # - a student id
454
+ # - an English course name
455
+ #
456
+ # A generic filename does not count: a Chinese name that names no
457
+ # discipline, or a plain English noun, say nothing about whose they are.
458
+ # The rule must not be so broad that it rejects every .pdf in the tree --
459
+ # a guard that cries wolf is a guard that gets disabled.
460
+ FILENAME_RE = re.compile(
461
+ r"[^\s`|()<>\[\]{}\"'`,;。,、::/\\]+?"
462
+ r"\.(?:pdf|zip|pptx?|docx?|xlsx?)")
463
+ FILENAME_CJK_RE = re.compile(r"力學|數學|電路|結構|物理|化學|電子|工力")
464
+ FILENAME_PREFIX_RE = re.compile(
465
+ r"^[A-Za-z]{2,4}_(?:CA|HW|CH|note|mid|exam)", re.IGNORECASE)
466
+ FILENAME_TERM_RE = re.compile(r"^\d{3,4}[-_]")
467
+ FILENAME_EN_RE = re.compile(
468
+ r"Mechanics|Circuits|Circuit_Design|Data_Structures|Electronics",
469
+ re.IGNORECASE)
470
+
471
+ def _filename_reason(self, token):
472
+ """Why this filename identifies someone, or None if it is generic."""
473
+ base = token.rsplit(".", 1)[0]
474
+ if "*" in base:
475
+ base = base.split("*")[0]
476
+ if not base:
477
+ return None
478
+ if self.FILENAME_CJK_RE.search(base):
479
+ return "a discipline noun"
480
+ if self.FILENAME_PREFIX_RE.match(base):
481
+ return "a course prefix"
482
+ if self.FILENAME_TERM_RE.match(base):
483
+ return "an academic-term prefix"
484
+ if self.STUDENT_ID_RE.search(base):
485
+ return "a student id"
486
+ if self.FILENAME_EN_RE.search(base):
487
+ return "an English course name"
488
+ return None
433
489
  # Words that mean "this line is about a course", not "this line is prose".
434
490
  CONTEXT_RE = re.compile(
435
491
  r"期中考|期末考|模擬考|小考|課程|課堂|助教|講義|作業|學期|科目"
@@ -517,6 +573,24 @@ class TestNoPersonalCourseRoster(unittest.TestCase):
517
573
  f"{label} {hit.group(0)!r} in {path}:{i + 1} "
518
574
  f"near a course-context marker:\n{line.strip()[:160]}")
519
575
 
576
+ def test_no_identifying_filenames(self):
577
+ """A filename that names a course's actual coursework identifies the
578
+ student more directly than a course title does.
579
+
580
+ Generic filenames are allowed: 期中座位表.pdf, worksheet.pdf and
581
+ seating.pdf do not say whose they are."""
582
+ for path in self._repo_files():
583
+ src = path.read_text(encoding="utf-8", errors="replace")
584
+ for i, line in enumerate(src.splitlines(), 1):
585
+ for match in self.FILENAME_RE.finditer(line):
586
+ token = match.group(0)
587
+ reason = self._filename_reason(token)
588
+ if reason is None:
589
+ continue
590
+ self.fail(
591
+ f"filename {token!r} in {path}:{i} carries {reason}:\n"
592
+ f"{line.strip()[:160]}")
593
+
520
594
  def test_no_room_numbers_in_course_context(self):
521
595
  """A room number is only identifying in a course context; the same
522
596
  digits appear in a port number or a code elsewhere."""
@@ -775,7 +849,7 @@ class TestFetchAssignmentHandouts(unittest.TestCase):
775
849
  '<p>see <a class="instructure_file_link" '
776
850
  'href="https://cool.ntu.edu.tw/courses/9/files/10140367/download" '
777
851
  'data-api-endpoint="https://cool.ntu.edu.tw/api/v1/courses/9/files/10140367" '
778
- 'data-api-returntype="File">DS_CA3.zip</a></p>'
852
+ 'data-api-returntype="File"><PREFIX>_CA3.zip</a></p>'
779
853
  )
780
854
 
781
855
  def test_finds_the_endpoint_in_the_description(self):
File without changes
File without changes
File without changes