Skip to content

Blackboard

What: SUSTech's LMS — assignment deadlines, file uploads, course materials, announcements.

Use for: Checking upcoming due dates before exams, submitting lab reports as PDF, downloading course slides.

Auth: BBAuth — CAS-authenticated session via sustech_survival.sso. See SSO for credential setup.


Authentication

from sustech_survival.sso import BBAuth

auth = BBAuth()               # singleton-per-class
ok, reason = auth.ensure()    # check + auto-refresh if expired
if not ok:
    raise RuntimeError(reason)

# Use the authenticated session
auth.session.get("https://bb.sustech.edu.cn/...")

Or with the decorator:

from sustech_survival.sso import require_auth, BBAuth

@require_auth(BBAuth)
def fetch_deadlines(auth=None):
    ...
# CLI — auth is automatic (ensure()); diagnose only when something fails
sustech sso check             # verify credentials against CAS
sustech sso creds set         # write / update the credentials file

Sessions are kept in memory only. Auto-refresh on stale response (HTTP 401).


CLI

sustech bb courses            # list enrolled courses (REST, fast)
sustech bb courses --query MSE  # filter by keyword
sustech bb search --course MSE306 --has-attachments  # find attachments
sustech bb types              # list content types per course
sustech bb page 629844 -c 8534 -v   # items on a BB content page (URL ids stripped)
sustech bb download <content_id> …  # fetch course-material files
sustech bb course <course> assignment <aid> [attempt] [--download]  # submissions
sustech bb submit <content_id> <file> --course <id> --name "<sid>-<name>-…" --yes

# BB content-page URL → command:
#   …/listContent.jsp?course_id=_8534_1&content_id=_629844_1 → sustech bb page 629844 -c 8534

Cross-course pending work and submitted grades

sustech bb pending --json                  # current term, all due dates
sustech bb grades --json                   # own submitted assessment attempts
sustech bb pending --semester '2026-2027-1' --json
sustech bb grades --course 101 --course 102 --json
sustech bb pending --course 101 \
  --due-from '2026-10-07T00:00:00+08:00' \
  --due-until '2026-10-14T23:59:59+08:00' --json

Without an explicit scope, these commands read live enrollments and select the unique BB term whose dated duration contains the query time. --semester (alias --term) accepts a BB term ID, name substring or academic code. If the site has overlapping/missing term dates, specify a semester or course IDs; the query does not guess a term. Repeated --course IDs alone select those enrolled courses without term inference. Adding --semester intersects the two scopes and reports requested courses outside the term or enrollments. There is no dependency on a private baseline, local course cache or archive.

pending includes overdue and undated work, and distinguishes unsubmitted assessments from drafts. Optional inclusive due bounds require ISO timestamps with an offset; applying either bound excludes undated tasks and reports that window. It reads gradebook columns with grading.type == Attempts, content metadata, the user's grade records (including exemptions) and all own attempts. For compatibility with SUSTech BB, attempts GETs omit the optional userId query filter, which can return 403 for student sessions. Every returned page is still checked for attempt ownership and filtered locally to the current user. Actual permission failures remain errors; there is no automatic retry. Ordinary assignments and test/assignment links are included with distinct kind labels. A test link is not silently presented as a written assignment.

Submitted/awaiting-grading tasks are absent from pending. Column exemptions appear in excluded. Group tasks without a personal submission, unsupported content, explicit notification-only/do-not-submit-here titles and failed reads appear in unknown/errors, rather than being declared unsubmitted. Notification detection recognizes explicit Chinese and English title markers; it cannot infer every external-platform workflow. Personal deadline extensions, late-submission access and external/group completion are not independently verified by this report.

grades includes submitted attempt-based assessments, including test-type items. Each own attempt retains its status, score, creation time and exemption flag. Zero is valid; missing points remain unknown. possible comes from the column's actual possible points. The separate grade field is the column value reported by BB, labelled source: gradebook_user_grade; the client never computes a highest/latest/average final grade from attempts. Known scores remain visible if content metadata cannot be read. InProgressAgain without a known submitted attempt stays unconfirmed.

Both commands default to readable text; --json writes only a versioned JSON object to stdout. Authentication diagnostics use stderr. Key fields:

Field Meaning
schema_version, mode, checked_at, timezone Schema version 1, view and query time in Asia/Shanghai
scope Live source, semester/terms, selected courses, due bounds and whether undated work is included
courses_checked, attempt_columns_checked Courses with successful column reads and considered assessment columns
items Confirmed pending assessments or own submitted assessment attempts
excluded Exempt columns excluded from the requested view
unknown Scope/task states that cannot be established, with stable reason codes
errors Read stage, safe diagnostic, HTTP status when available and endpoint without user identifiers/query values
complete True only when unknown and errors are empty

Exit status is 0 for a complete report, 1 for partial/failed reads and 2 for invalid CLI arguments. A status-1 JSON report can contain useful known items: consume them with the reported gaps. Never treat an incomplete empty list as evidence that no work remains. No course names, raw authentication responses or report snapshots are written to disk by these commands.

from sustech_survival.bb.assessments import query

pending = query('pending', semester='2026-2027-1')
grades = query('grades', course_ids=['101', '102'])
# An existing package-authorized requests.Session may be passed as session=.
# All reads within one query share that session; query() itself never POSTs.
if not pending['complete']:
    print(pending['unknown'], pending['errors'])

Content discovery and availability

query.walk_contents() and query.discover_pages() exclude items explicitly marked availability.available = "No", including their descendants. They do not request those items' details during broad discovery. Yes, PartiallyVisible, or missing availability metadata are still read normally; these states do not guarantee permission. Direct requests for a specific item retain normal server-side permission checks.

Each fresh walk reads parent listings again, so newly available folders are included without relying on modified timestamps or a permanent deny list. discover_pages() reports the excluded count on stderr, including cache hits. Its usual one-hour cache still applies; use refresh=True when current availability is required. Unavailable content is excluded from totals; a successful read does not establish that the entire course is accessible.

The walker reuses one session, reads each collection's pagination, rejects pagination loops and links to other hosts, and reads the root collection only once. Failed or malformed reads propagate; discovery does not cache a partial result as an empty or successful listing. Cached listings created before the availability filter are not reused.

from sustech_survival.bb.query import walk_contents, discover_pages

unavailable = []
rows = list(walk_contents("1234", unavailable=unavailable))
# unavailable contains course/content IDs, titles, and availability_no reasons.
pages = discover_pages("1234", refresh=True)

See Blackboard's REST API best practices and content availability rules.


Deadlines

from sustech_survival.bb import ddl

ddl.run()                    # next 7 days, all courses
ddl.run(days=14)             # next 14 days
ddl.run(course_id='_8053_1') # single course

How it works: REST API for assignment items + portal page for course IDs. Due dates are parsed from item titles (Week N) or body text (每周六晚12点).

Due date parsing rules:

Pattern Interpretation
第12周 Spring 2026: Saturday of week 12, 23:59
Week 12 Same
每周六晚12点 Recurring Saturday 23:59
12月31日 23:59 Specific date
(no date) Status unknown

Active semester courses: Course IDs are internal BB IDs — use sustech bb courses to list yours.


File Submission

The submitter is pure REST (no browser). bb.submit (formerly the Playwright submitter) now hosts the REST path:

from sustech_survival.bb.submit import submit_assignment_rest

submit_assignment_rest(
    course_id='_8053_1',
    content_id='490876',
    file_path='/tmp/report.pdf',
)

The legacy-signature wrapper (accepts a list of paths; only the first file is submitted via REST) and the submit_file(content_id, file_path) helper remain available:

from sustech_survival.bb.submit import submit_assignment, submit_file

submit_assignment(
    course_id='_8053_1',
    content_id='490876',
    file_paths=['/tmp/report.pdf'],
)
ok, msg = submit_file('490876', '/tmp/report.pdf')  # auto-resolves course

⚠️ Always verify submission by checking attempt count increased after "success".


Download

from sustech_survival.bb.download import download_content

download_content(content_id='_12345_1', out_dir='./downloads')

Content file downloads are pure REST. Note: the gradebook REST API does not expose the URLs of files submitted to an assignment (the old Playwright scraper was removed), so download_submission reports attempts without downloading files.


See also

  • SSO — credential setup and auth infrastructure

Deadline reads inspect complete personal enrollments, with no fixed BB term ID; the date window scopes upcoming work. Attempt reads follow pagination and filter the current user. Failed reads or unknown attempt ownership raise errors. A file comment supplied to apply_submission() is sent in the same multipart submission, creating one attempt. CSV/display exports are separate from submission.