Guidance for AI coding agents working in this repository.
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]—tjuCLI command: a full-screen interactive Textual terminal apptju[mcp]—tju-mcpCLI 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 viaddddocrOCR) and DES-encrypted password (via embedded JS insrc/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.
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).
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.
Core library + dev tools (offline tests):
uv sync # installs runtime + dev deps into .venv
uv run pytest # run the offline test suiteWith TUI extra:
uv sync --extra tui
tju # launch the terminal appWith MCP extra:
uv sync --extra mcp
tju-mcp setup # one-time: store credentials in the OS keyring
tju-mcp # start the stdio MCP serverDo not hand-edit
uv.lock— it is generated byuv lock. Update deps viauv 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].
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 # boolRunnable examples: examples/fetch_schedule.py, examples/fetch_all_courses.py.
Output goes to examples/output/ (gitignored).
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 |
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 schematest_no_tool_output_contains_password— calls every tool and asserts the password string is absent from output
- src-layout: all importable code is under
src/tju/. Install editable (pip install -e .) before running tests, or useuv run pytest(uv activates the venv automatically). - Type hints: all modules use
from __future__ import annotations; Python 3.11+ so bareX | Yunion syntax is fine in code. - Chinese-keyed data: EAMS HTML uses Chinese column headers. Use
ChineseBool/ChineseHasBoolfromsrc/tju/fields.pyand themfield()helper fromsrc/tju/schema.pywhen 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 endpointCHINESE_WEEKDAYmap (src/tju/consts.py) → convert Chinese weekday names to integers
- Exceptions: always raise from
src/tju/exceptions.py. Don't raise bareValueError/RuntimeErrorfor domain errors. - Build backend:
hatchling. Don't add asetup.pyorsetup.cfg. - MCP security: never add a tool that accepts a
usernameorpasswordparameter, and never include credential values in tool output. PII reveal is controlled by a server-side config flag, not a tool arg.
| 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) |
| 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. |
- Course parser is fragile:
src/tju/parser/course.pyis 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. ddddocris heavy: the OCR dependency pulls inonnxruntimeandopencv-python-headless. In environments without these (or without a compatible glibc),import tjuwill fail. Tests don't importtjudirectly — they import fromtju.parser,tju.models, andtju.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). SeeSEMESTERinsrc/tju/consts.pyfor 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. Insrc/tju/tui/screens/main.py, workers return raw data and pass it to main-thread callbacks viacall_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'sget_clientkwarg to avoid network calls. Never hardcode real credentials in fixtures.
Full API reference and usage guides: https://python.tjuse.com/
To build the docs locally:
uv sync --group docs
uv run mkdocs serve