Skip to content

Latest commit

 

History

History
257 lines (199 loc) · 12.6 KB

File metadata and controls

257 lines (199 loc) · 12.6 KB

AGENTS.md — tju-python

Guidance for AI coding agents working in this repository.

Overview

tju (PyPI: tju, v0.1.2) is a Python 3.11+ library that programmatically logs into Tianjin University's academic web systems and returns structured student data. It ships two optional extras:

  • tju[tui]tju CLI command: a full-screen interactive Textual terminal app
  • tju[mcp]tju-mcp CLI command: a local stdio MCP server for AI agent integration

Core systems accessed:

  • SSO / auth: https://sso.tju.edu.cn/cas/login — CAS login with CAPTCHA (solved via ddddocr OCR) and DES-encrypted password (via embedded JS in src/tju/encrypt.py).
  • Data backend: http://classes.tju.edu.cn/eams/ — the EAMS academic administration portal.

Credentials are passed as constructor args or via env vars TJU_USER / TJU_PASS. Live calls require being on the TJU campus network or VPN; Session.login() raises LoginError("VPN not connected") otherwise. The test suite is fully offline and never makes network requests.

Architecture

The core data flow is a strict four-layer pipeline:

Session  →  Client (mixins)  →  parser  →  models
Layer Key files Responsibility
Session src/tju/session.py httpx client, CAS login, captcha OCR, DES-encrypted creds, auto session-renewal
Client src/tju/client/__init__.py, src/tju/client/api/*.py Per-feature mixins composed into one Client class; create_client() convenience factory
Parser src/tju/parser/*.py Pure-regex / BeautifulSoup functions: raw HTML → plain Python dicts. No network I/O.
Models src/tju/models/*.py marshmallow-dataclass typed dataclasses; load/dump via marshmallow schemas

Client mixins (src/tju/client/api/):

Mixin Methods
CourseMixin query_courses(), query_course_info(), query_syllabus()
ProfileMixin profile (property)
ScheduleMixin schedule(semester, ...)
ExamMixin exam(semester)
ScoreMixin score(), exp_score(semester)
ClassroomMixin free_classrooms(date_begin, ...)

Supporting infrastructure: src/tju/consts.py (all URL paths, SEMESTER code→id map, CHINESE_WEEKDAY), src/tju/exceptions.py (SessionError, LoginError, HtmlParseError, DataError, StuTypeError), src/tju/fields.py + src/tju/schema.py (custom marshmallow fields for Chinese-keyed HTML data).

Additional packages

src/tju/config.py — shared credential/config module used by both TUI and MCP without cross-dependency. Stores username in ~/.config/tju/config.toml (mode 0o600) and password in the OS keyring (never on disk). Respects the TJU_CONFIG_DIR env var for test isolation.

src/tju/tui/ — Textual full-screen terminal app (tju command, pip install 'tju[tui]'). Entry point: src/tju/tui/__init__.py:main(). Key files: app.py (TjuApp), app.tcss (stylesheet), screens/login.py (LoginScreen), screens/main.py (MainScreen), render.py (pure widget builders). src/tju/tui/config.py is a thin re-export shim from tju.config.

src/tju/mcp/ — FastMCP server (tju-mcp command, pip install 'tju[mcp]'). Entry point: src/tju/mcp/__init__.py:main(). Key files: server.py (tool definitions), redact.py (PII masking). No tool accepts or returns a password — the server is the credential boundary. Password is always read from the OS keyring; the agent never touches it.

Setup

Core library + dev tools (offline tests):

uv sync          # installs runtime + dev deps into .venv
uv run pytest    # run the offline test suite

With TUI extra:

uv sync --extra tui
tju              # launch the terminal app

With MCP extra:

uv sync --extra mcp
tju-mcp setup    # one-time: store credentials in the OS keyring
tju-mcp          # start the stdio MCP server

Do not hand-edit uv.lock — it is generated by uv lock. Update deps via uv add <pkg> / uv remove <pkg> (dev deps: uv add --dev <pkg>).

Key runtime dependencies: httpx, ddddocr (+ onnxruntime, opencv-python-headless), pyexecjs, fake-useragent, marshmallow, marshmallow-dataclass, markdownify, beautifulsoup4. TUI extra adds: textual, keyring, platformdirs, tomli-w. MCP extra adds: mcp, keyring, platformdirs, tomli-w. Dev dependencies (in [dependency-groups] dev): pytest, pytest-asyncio, anyio[trio].

Usage (as a library)

from tju import Session
from tju.client import Client, create_client

# Option A — explicit
session = Session()  # reads TJU_USER / TJU_PASS from env (or pass username=, password=)
client = Client(session=session)

# Option B — factory
client = create_client()

# Use the client (all require campus network / VPN)
print(client.profile)                            # ProfileMixin
print(client.schedule(semester="24251"))          # ScheduleMixin
print(client.query_courses(semester="24251"))     # CourseMixin — page 1 of course library
print(client.query_course_info(lession_id="387248"))
print(client.query_syllabus(lession_id="387248"))
print(client.exam(semester="24251"))              # ExamMixin
print(client.score())                             # ScoreMixin
print(client.exp_score(semester="20211"))
print(client.free_classrooms(date_begin="2025-10-08"))  # ClassroomMixin

# Identity properties on Client
client.stu_id      # student ID string
client.stu_name    # name string
client.stu_type    # StuType.UNDERGRADUATE | StuType.GRADUATE
client.has_minor   # bool

Runnable examples: examples/fetch_schedule.py, examples/fetch_all_courses.py. Output goes to examples/output/ (gitignored).

Verifying Correctness

CI runs on every push via GitHub Actions (.github/workflows/ci.yml): pytest on Python 3.11 / 3.12 / 3.13. The docs site is rebuilt and deployed on every push to main (.github/workflows/docs.yml). PyPI releases are published via OIDC trusted publishing on v* tags (.github/workflows/publish.yml).

uv run pytest        # or just: pytest (if .venv is active)

70 tests across seven files — all offline, no network required:

File Count What it tests
tests/test_parser.py 18 Parser functions: raw HTML → dict/markdown
tests/test_schema.py 10 Model load/dump: parsed dict → typed dataclass → re-serialised JSON
tests/test_encrypt.py 2 DES strEnc encryption in encrypt.py
tests/test_tui_config.py 11 tju.config: credential store, keyring isolation, config TOML
tests/test_tui_app.py 6 Textual TUI: boot, login screen, sidebar, quit binding (async)
tests/test_mcp_redact.py 12 PII masking: mask_profile, should_reveal_pii, _partial_mask
tests/test_mcp_server.py 11 MCP tools: shape, PII masking, no-password invariant, friendly error on missing creds

Fixture contract

Fixtures live in tests/resources/:

website/*.html      ← captured real EAMS HTML (input to parsers)
parsed/*.json       ← expected parser output (input to schema tests)
parsed/*.md         ← expected syllabus markdown output
serialized/*.json   ← expected marshmallow-serialised model output

Privacy rule: committed fixtures must use sanitised placeholder data — no real student IDs, names, grades, or contact information. Real personal data lives only in gitignored .scratch/ and examples/output/.

When adding or fixing a parser: add a new website/<feature>_<variant>.html + parsed/<feature>_<variant>.json fixture pair, then add a test_parse_<variant>() function in tests/test_parser.py.

When changing a model: update the corresponding serialized/*.json fixtures and add/update a test in tests/test_schema.py.

When touching the MCP server: test_mcp_server.py enforces two security invariants that must always pass:

  • test_no_tool_accepts_password_parameter — iterates every registered tool's input schema
  • test_no_tool_output_contains_password — calls every tool and asserts the password string is absent from output

Conventions

  • src-layout: all importable code is under src/tju/. Install editable (pip install -e .) before running tests, or use uv run pytest (uv activates the venv automatically).
  • Type hints: all modules use from __future__ import annotations; Python 3.11+ so bare X | Y union syntax is fine in code.
  • Chinese-keyed data: EAMS HTML uses Chinese column headers. Use ChineseBool / ChineseHasBool from src/tju/fields.py and the mfield() helper from src/tju/schema.py when writing new models.
  • Parsers are pure functions: they take raw HTML strings and return plain dicts. No network calls, no side effects. Keep it that way — the offline test contract depends on it.
  • Chinese text markers drive branching logic. Examples:
    • "研究" (research) in a page title → graduate student path
    • "辅修" (minor) → minor schedule endpoint
    • CHINESE_WEEKDAY map (src/tju/consts.py) → convert Chinese weekday names to integers
  • Exceptions: always raise from src/tju/exceptions.py. Don't raise bare ValueError/RuntimeError for domain errors.
  • Build backend: hatchling. Don't add a setup.py or setup.cfg.
  • MCP security: never add a tool that accepts a username or password parameter, and never include credential values in tool output. PII reveal is controlled by a server-side config flag, not a tool arg.

Implemented Features

Feature Client API Parser Status
Login + auto-renew Session.login() inline in session.py
Logout Session.logout()
Student profile client.profile parse_profile
Personal timetable client.schedule() parse_schedule
Public course library client.query_courses() parse_course
Course info client.query_course_info() parse_course_info
Course syllabus client.query_syllabus() parse_syllabus
Exam schedule client.exam() parse_exam, parse_exam_batch_id
Scores (UG + GS) client.score() parse_score
Experiment scores client.exp_score() parse_score_exp
Free classroom search client.free_classrooms() parse_free_classroom
TUI (tju command) ✅ (tju[tui] extra)
MCP server (tju-mcp) ✅ (tju[mcp] extra)

Known Issues / Pending Work

Issue Location Notes
Study-plan features src/tju/consts.py (PLAN_URL_PATH, PLAN_COMPL_URL_PATH) Blocked: 403 「对不起,您没有权限」 for graduate accounts; UG-only. Not implemented.
Course selection src/tju/consts.py (classify paths) Blocked: 403 for graduate accounts; write operations out of scope. Not implemented.
No linting / type-checking No ruff/flake8/black/mypy/pre-commit configuration yet.

Gotchas

  • Course parser is fragile: src/tju/parser/course.py is the most actively-changed file. It uses multi-line regex over real EAMS HTML. When real-world pages change structure, this is where things break first. Always add a regression fixture when fixing it.
  • ddddocr is heavy: the OCR dependency pulls in onnxruntime and opencv-python-headless. In environments without these (or without a compatible glibc), import tju will fail. Tests don't import tju directly — they import from tju.parser, tju.models, and tju.encrypt — so the test suite stays importable even without compatible OCR binaries.
  • Semester codes: EAMS uses 5-digit numeric codes (e.g. "24251" = 2024–2025 first term). See SEMESTER in src/tju/consts.py for the full mapping. utils.get_current_semester() derives the current one from the system date.
  • Graduate vs. undergraduate paths: several client methods branch on client.stu_type. When adding features, check whether EAMS uses a different endpoint or response format for graduate students (StuType.GRADUATE).
  • Textual thread safety: Textual widget methods (add_column, add_row, etc.) must be called on the main event-loop thread. In src/tju/tui/screens/main.py, workers return raw data and pass it to main-thread callbacks via call_from_thread; never call widget methods directly from a @work(thread=True) worker.
  • MCP tool tests use build_server(get_client=...): inject a mock client via the factory's get_client kwarg to avoid network calls. Never hardcode real credentials in fixtures.

Documentation

Full API reference and usage guides: https://python.tjuse.com/

To build the docs locally:

uv sync --group docs
uv run mkdocs serve