bb-sync 1.9.0__py3-none-any.whl
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.
- bb_sync/__init__.py +23 -0
- bb_sync/__main__.py +8 -0
- bb_sync/blackboard/__init__.py +5 -0
- bb_sync/blackboard/downloader.py +348 -0
- bb_sync/blackboard/login.py +190 -0
- bb_sync/blackboard/models.py +143 -0
- bb_sync/blackboard/scraper.py +695 -0
- bb_sync/browser/__init__.py +5 -0
- bb_sync/browser/steel.py +511 -0
- bb_sync/cli.py +302 -0
- bb_sync/commands/__init__.py +5 -0
- bb_sync/commands/auth.py +80 -0
- bb_sync/commands/config.py +239 -0
- bb_sync/commands/course.py +45 -0
- bb_sync/commands/doctor.py +65 -0
- bb_sync/commands/due.py +66 -0
- bb_sync/commands/run.py +74 -0
- bb_sync/context.py +69 -0
- bb_sync/core/__init__.py +5 -0
- bb_sync/core/config.py +205 -0
- bb_sync/core/config_store.py +190 -0
- bb_sync/core/doctor.py +230 -0
- bb_sync/core/due.py +192 -0
- bb_sync/core/errors.py +107 -0
- bb_sync/core/output.py +151 -0
- bb_sync/core/service.py +446 -0
- bb_sync/core/update.py +286 -0
- bb_sync/creds.py +91 -0
- bb_sync/paths.py +26 -0
- bb_sync-1.9.0.dist-info/METADATA +265 -0
- bb_sync-1.9.0.dist-info/RECORD +34 -0
- bb_sync-1.9.0.dist-info/WHEEL +4 -0
- bb_sync-1.9.0.dist-info/entry_points.txt +2 -0
- bb_sync-1.9.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Blackboard 领域模型与站点常量。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import date
|
|
8
|
+
|
|
9
|
+
BASE = "https://bb.cuhk.edu.cn"
|
|
10
|
+
|
|
11
|
+
#: ADFS 页面元素选择器
|
|
12
|
+
ADFS_USER = "input[name='userNameInput'], #userNameInput"
|
|
13
|
+
ADFS_PASS = "input[name='PasswordInput'], #passwordInput"
|
|
14
|
+
ADFS_NEXT = "#nextButton, #submitButton" # 第一页「下一步」
|
|
15
|
+
ADFS_SUBMIT = "#submitButton, #nextButton, span.submit" # 密码页「登录」
|
|
16
|
+
LOGIN_URL = f"{BASE}/webapps/login/"
|
|
17
|
+
|
|
18
|
+
_PORTAL_HOME = f"{BASE}/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_1_1"
|
|
19
|
+
_PORTAL_COURSES = f"{BASE}/webapps/portal/execute/tabs/tabAction?tab_tab_group_id=_25_1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Course:
|
|
24
|
+
"""一门课程。"""
|
|
25
|
+
|
|
26
|
+
bb_id: str # e.g. _12345_1
|
|
27
|
+
title: str # 原始标题
|
|
28
|
+
code: str = "" # 提取的课程代码,如 CSC5010
|
|
29
|
+
slug: str = "" # 本地文件夹名
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
m = re.search(r"\b([A-Z]{3,4}\d{4}[A-Z]?)\b", self.title.upper())
|
|
33
|
+
if m:
|
|
34
|
+
self.code = m.group(1)
|
|
35
|
+
|
|
36
|
+
def as_dict(self) -> dict[str, str]:
|
|
37
|
+
return {"id": self.bb_id, "code": self.code, "title": self.title, "folder": self.slug}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class FileItem:
|
|
42
|
+
"""内容区里的一个待下载条目。"""
|
|
43
|
+
|
|
44
|
+
name: str | None
|
|
45
|
+
url: str
|
|
46
|
+
category: str # lectures / assignments / tutorials / other
|
|
47
|
+
folder_hint: str = "" # 内容区路径提示
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class DueItem:
|
|
52
|
+
"""一项待办:课程主页 Due / To Do 任务,或从公告整理出的课堂测验 / 考试。"""
|
|
53
|
+
|
|
54
|
+
course_id: str
|
|
55
|
+
course_code: str
|
|
56
|
+
course_title: str
|
|
57
|
+
title: str
|
|
58
|
+
due_date: date | None
|
|
59
|
+
due_text: str = ""
|
|
60
|
+
kind: str = "task" # task = To Do 模块;quiz / exam = 公告里的课堂测验 / 考试
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def course_label(self) -> str:
|
|
64
|
+
"""优先显示课程代码,没有代码时退回原始标题。"""
|
|
65
|
+
return self.course_code or self.course_title or self.course_id
|
|
66
|
+
|
|
67
|
+
def as_dict(self) -> dict[str, str | None]:
|
|
68
|
+
return {
|
|
69
|
+
"course_id": self.course_id,
|
|
70
|
+
"course_code": self.course_code,
|
|
71
|
+
"course_title": self.course_title,
|
|
72
|
+
"title": self.title,
|
|
73
|
+
"due_date": self.due_date.isoformat() if self.due_date else None,
|
|
74
|
+
"due_text": self.due_text,
|
|
75
|
+
"kind": self.kind,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class SyncStats:
|
|
81
|
+
"""一次同步的统计结果。"""
|
|
82
|
+
|
|
83
|
+
downloaded: int = 0
|
|
84
|
+
exists: int = 0
|
|
85
|
+
would_download: int = 0
|
|
86
|
+
empty: int = 0
|
|
87
|
+
failed: int = 0
|
|
88
|
+
courses: list[str] = field(default_factory=list)
|
|
89
|
+
|
|
90
|
+
def bump(self, status: str) -> None:
|
|
91
|
+
key = status.replace("-", "_")
|
|
92
|
+
if hasattr(self, key):
|
|
93
|
+
setattr(self, key, getattr(self, key) + 1)
|
|
94
|
+
|
|
95
|
+
def as_dict(self) -> dict[str, object]:
|
|
96
|
+
return {
|
|
97
|
+
"downloaded": self.downloaded,
|
|
98
|
+
"exists": self.exists,
|
|
99
|
+
"would_download": self.would_download,
|
|
100
|
+
"empty": self.empty,
|
|
101
|
+
"failed": self.failed,
|
|
102
|
+
"courses": self.courses,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class DueStats:
|
|
108
|
+
"""一次 ``bb-sync due`` 的结果。"""
|
|
109
|
+
|
|
110
|
+
path: str = ""
|
|
111
|
+
items: list[DueItem] = field(default_factory=list)
|
|
112
|
+
failed: int = 0 # Due / To Do 模块抓取失败的课程数
|
|
113
|
+
announcement_failed: int = 0 # 公告抓取失败的课程数
|
|
114
|
+
courses: list[str] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def total(self) -> int:
|
|
118
|
+
return len(self.items)
|
|
119
|
+
|
|
120
|
+
def as_dict(self) -> dict[str, object]:
|
|
121
|
+
return {
|
|
122
|
+
"path": self.path,
|
|
123
|
+
"total": self.total,
|
|
124
|
+
"failed": self.failed,
|
|
125
|
+
"announcement_failed": self.announcement_failed,
|
|
126
|
+
"courses": self.courses,
|
|
127
|
+
"items": [item.as_dict() for item in self.items],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
__all__ = [
|
|
132
|
+
"ADFS_NEXT",
|
|
133
|
+
"ADFS_PASS",
|
|
134
|
+
"ADFS_SUBMIT",
|
|
135
|
+
"ADFS_USER",
|
|
136
|
+
"BASE",
|
|
137
|
+
"LOGIN_URL",
|
|
138
|
+
"Course",
|
|
139
|
+
"DueItem",
|
|
140
|
+
"DueStats",
|
|
141
|
+
"FileItem",
|
|
142
|
+
"SyncStats",
|
|
143
|
+
]
|