e3cli 0.3.1__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.
- e3cli/__init__.py +3 -0
- e3cli/__main__.py +5 -0
- e3cli/ai/__init__.py +1 -0
- e3cli/api/__init__.py +0 -0
- e3cli/api/assignments.py +38 -0
- e3cli/api/client.py +77 -0
- e3cli/api/courses.py +27 -0
- e3cli/api/files.py +25 -0
- e3cli/api/site.py +10 -0
- e3cli/auth.py +33 -0
- e3cli/cli.py +61 -0
- e3cli/commands/__init__.py +0 -0
- e3cli/commands/_common.py +29 -0
- e3cli/commands/assignments.py +91 -0
- e3cli/commands/courses.py +39 -0
- e3cli/commands/download.py +105 -0
- e3cli/commands/login.py +68 -0
- e3cli/commands/logout.py +19 -0
- e3cli/commands/schedule.py +42 -0
- e3cli/commands/setup.py +142 -0
- e3cli/commands/submit.py +77 -0
- e3cli/commands/sync.py +115 -0
- e3cli/config.py +98 -0
- e3cli/credential.py +116 -0
- e3cli/i18n.py +233 -0
- e3cli/scheduler/__init__.py +0 -0
- e3cli/scheduler/cron.py +54 -0
- e3cli/storage/__init__.py +0 -0
- e3cli/storage/db.py +124 -0
- e3cli/storage/models.py +32 -0
- e3cli/storage/tracking.py +11 -0
- e3cli-0.3.1.dist-info/METADATA +370 -0
- e3cli-0.3.1.dist-info/RECORD +36 -0
- e3cli-0.3.1.dist-info/WHEEL +4 -0
- e3cli-0.3.1.dist-info/entry_points.txt +2 -0
- e3cli-0.3.1.dist-info/licenses/LICENSE +21 -0
e3cli/i18n.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
雙語支援 — 根據系統語言自動切換中文/英文。
|
|
3
|
+
|
|
4
|
+
使用者可透過環境變數 E3CLI_LANG=en 或 E3CLI_LANG=zh 手動指定。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import locale
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
_STRINGS: dict[str, dict[str, str]] = {
|
|
13
|
+
# === CLI help ===
|
|
14
|
+
"cli.help": {
|
|
15
|
+
"zh": "NYCU E3 Moodle 自動化工具",
|
|
16
|
+
"en": "NYCU E3 Moodle automation tool",
|
|
17
|
+
},
|
|
18
|
+
"cli.login": {"zh": "登入取得 token", "en": "Login and get token"},
|
|
19
|
+
"cli.logout": {"zh": "清除認證資料", "en": "Clear credentials"},
|
|
20
|
+
"cli.courses": {"zh": "列出修課清單", "en": "List enrolled courses"},
|
|
21
|
+
"cli.assignments": {"zh": "列出作業與截止日期", "en": "List assignments and deadlines"},
|
|
22
|
+
"cli.download": {"zh": "下載課程教材", "en": "Download course materials"},
|
|
23
|
+
"cli.submit": {"zh": "提交作業", "en": "Submit assignment"},
|
|
24
|
+
"cli.sync": {"zh": "全量同步", "en": "Full sync"},
|
|
25
|
+
"cli.schedule": {"zh": "管理定時同步排程", "en": "Manage sync schedule"},
|
|
26
|
+
"cli.setup": {"zh": "重新執行初始設定引導", "en": "Re-run setup wizard"},
|
|
27
|
+
"cli.version": {"zh": "顯示版本", "en": "Show version"},
|
|
28
|
+
|
|
29
|
+
# === Login ===
|
|
30
|
+
"login.prompt_user": {"zh": "帳號", "en": "Username"},
|
|
31
|
+
"login.prompt_pass": {"zh": "密碼: ", "en": "Password: "},
|
|
32
|
+
"login.connecting": {"zh": "正在連線 {url} ...", "en": "Connecting to {url} ..."},
|
|
33
|
+
"login.success": {"zh": "✓ 登入成功!Token 已儲存。", "en": "✓ Login successful! Token saved."},
|
|
34
|
+
"login.success_saved": {
|
|
35
|
+
"zh": "✓ 登入成功!Token 已儲存,帳密已加密保存。",
|
|
36
|
+
"en": "✓ Login successful! Token saved, credentials encrypted.",
|
|
37
|
+
},
|
|
38
|
+
"login.hint_save": {
|
|
39
|
+
"zh": "提示: 加上 --save 可記住帳密,下次用 --refresh 自動重新登入。",
|
|
40
|
+
"en": "Tip: use --save to remember credentials, --refresh to auto-login next time.",
|
|
41
|
+
},
|
|
42
|
+
"login.no_saved": {
|
|
43
|
+
"zh": "找不到已儲存的帳密,請先用 e3cli login --save 登入。",
|
|
44
|
+
"en": "No saved credentials found. Run e3cli login --save first.",
|
|
45
|
+
},
|
|
46
|
+
"login.use_saved": {
|
|
47
|
+
"zh": "使用已儲存的帳號 ({user}) 登入?",
|
|
48
|
+
"en": "Login with saved account ({user})?",
|
|
49
|
+
},
|
|
50
|
+
"login.refreshing": {
|
|
51
|
+
"zh": "使用已儲存的帳密重新取得 token ({user}) ...",
|
|
52
|
+
"en": "Refreshing token with saved credentials ({user}) ...",
|
|
53
|
+
},
|
|
54
|
+
"login.opt_username": {"zh": "學號/帳號", "en": "Student ID / Username"},
|
|
55
|
+
"login.opt_save": {"zh": "加密儲存帳密,下次自動登入", "en": "Save credentials (encrypted) for auto-login"},
|
|
56
|
+
"login.opt_refresh": {"zh": "使用已儲存的帳密重新取得 token", "en": "Refresh token using saved credentials"},
|
|
57
|
+
|
|
58
|
+
# === Logout ===
|
|
59
|
+
"logout.done": {"zh": "✓ 所有認證資料已安全清除。", "en": "✓ All credentials securely erased."},
|
|
60
|
+
|
|
61
|
+
# === Courses ===
|
|
62
|
+
"courses.title": {"zh": "修課清單", "en": "Enrolled Courses"},
|
|
63
|
+
"courses.col_id": {"zh": "ID", "en": "ID"},
|
|
64
|
+
"courses.col_code": {"zh": "課程代碼", "en": "Code"},
|
|
65
|
+
"courses.col_name": {"zh": "課程名稱", "en": "Course Name"},
|
|
66
|
+
"courses.empty": {"zh": "找不到任何課程。", "en": "No courses found."},
|
|
67
|
+
|
|
68
|
+
# === Assignments ===
|
|
69
|
+
"assign.title": {"zh": "作業列表", "en": "Assignments"},
|
|
70
|
+
"assign.col_course": {"zh": "課程", "en": "Course"},
|
|
71
|
+
"assign.col_name": {"zh": "作業名稱", "en": "Assignment"},
|
|
72
|
+
"assign.col_due": {"zh": "截止日期", "en": "Due Date"},
|
|
73
|
+
"assign.col_status": {"zh": "狀態", "en": "Status"},
|
|
74
|
+
"assign.no_deadline": {"zh": "無截止日", "en": "No deadline"},
|
|
75
|
+
"assign.expired": {"zh": "已過期", "en": "Expired"},
|
|
76
|
+
"assign.days_left": {"zh": "{n}天後", "en": "in {n} days"},
|
|
77
|
+
"assign.empty": {"zh": "沒有符合條件的作業。", "en": "No matching assignments."},
|
|
78
|
+
"assign.opt_due_soon": {"zh": "只顯示 N 天內到期的作業", "en": "Show assignments due within N days"},
|
|
79
|
+
|
|
80
|
+
# === Download ===
|
|
81
|
+
"dl.need_flag": {"zh": "請指定 --course 或 --all", "en": "Specify --course or --all"},
|
|
82
|
+
"dl.no_match": {"zh": "找不到符合 '{q}' 的課程", "en": "No course matching '{q}'"},
|
|
83
|
+
"dl.no_new": {"zh": "沒有新檔案", "en": "No new files"},
|
|
84
|
+
"dl.progress": {"zh": "下載中...", "en": "Downloading..."},
|
|
85
|
+
"dl.done": {
|
|
86
|
+
"zh": "✓ 完成!下載 {new} 個新檔案,略過 {skip} 個已存在的檔案。",
|
|
87
|
+
"en": "✓ Done! Downloaded {new} new files, skipped {skip} existing.",
|
|
88
|
+
},
|
|
89
|
+
"dl.opt_course": {"zh": "只下載特定課程 (課程代碼或名稱的子字串)", "en": "Download specific course (substring match)"},
|
|
90
|
+
"dl.opt_all": {"zh": "下載所有課程的教材", "en": "Download all course materials"},
|
|
91
|
+
|
|
92
|
+
# === Submit ===
|
|
93
|
+
"submit.not_found": {"zh": "檔案不存在: {f}", "en": "File not found: {f}"},
|
|
94
|
+
"submit.checking": {"zh": "檢查作業 #{id} 狀態...", "en": "Checking assignment #{id} status..."},
|
|
95
|
+
"submit.check_fail": {"zh": "✗ 無法取得作業資訊: {e}", "en": "✗ Cannot get assignment info: {e}"},
|
|
96
|
+
"submit.past_due": {
|
|
97
|
+
"zh": "✗ 作業已於 {dt} 截止。使用 --force 可強制提交。",
|
|
98
|
+
"en": "✗ Assignment was due {dt}. Use --force to submit anyway.",
|
|
99
|
+
},
|
|
100
|
+
"submit.uploading": {"zh": "上傳 {n} 個檔案...", "en": "Uploading {n} file(s)..."},
|
|
101
|
+
"submit.submitting": {"zh": "提交作業中...", "en": "Submitting..."},
|
|
102
|
+
"submit.ok": {"zh": "✓ 作業提交成功!", "en": "✓ Assignment submitted!"},
|
|
103
|
+
"submit.draft": {
|
|
104
|
+
"zh": "⚠ 作業已儲存為草稿,可能需要到 Moodle 手動確認提交。",
|
|
105
|
+
"en": "⚠ Saved as draft. You may need to confirm on Moodle.",
|
|
106
|
+
},
|
|
107
|
+
"submit.opt_text": {"zh": "線上文字內容 (可選)", "en": "Online text content (optional)"},
|
|
108
|
+
"submit.opt_force": {"zh": "強制提交(即使已過截止日)", "en": "Force submit (even past deadline)"},
|
|
109
|
+
|
|
110
|
+
# === Sync ===
|
|
111
|
+
"sync.syncing": {"zh": "同步中 — {name}", "en": "Syncing — {name}"},
|
|
112
|
+
"sync.new_assign": {"zh": "★ 新作業: [{course}] {name}", "en": "★ New: [{course}] {name}"},
|
|
113
|
+
"sync.assign_fail": {"zh": "取得作業資訊失敗: {e}", "en": "Failed to get assignments: {e}"},
|
|
114
|
+
"sync.done": {
|
|
115
|
+
"zh": "✓ 同步完成 — {files} 個新檔案, {assigns} 個新作業",
|
|
116
|
+
"en": "✓ Sync done — {files} new files, {assigns} new assignments",
|
|
117
|
+
},
|
|
118
|
+
"sync.opt_quiet": {"zh": "安靜模式(適用排程)", "en": "Quiet mode (for cron)"},
|
|
119
|
+
|
|
120
|
+
# === Schedule ===
|
|
121
|
+
"sched.enabled": {"zh": "✓ 已啟用定時同步,每 {m} 分鐘執行一次。", "en": "✓ Auto-sync enabled, every {m} minutes."},
|
|
122
|
+
"sched.disabled": {"zh": "✓ 已停用定時同步。", "en": "✓ Auto-sync disabled."},
|
|
123
|
+
"sched.status_on": {"zh": "✓ 排程已啟用", "en": "✓ Schedule active"},
|
|
124
|
+
"sched.status_off": {
|
|
125
|
+
"zh": "排程未啟用。使用 e3cli schedule enable 來啟用。",
|
|
126
|
+
"en": "Schedule not active. Run e3cli schedule enable to activate.",
|
|
127
|
+
},
|
|
128
|
+
"sched.opt_interval": {"zh": "同步間隔 (分鐘)", "en": "Sync interval (minutes)"},
|
|
129
|
+
|
|
130
|
+
# === Setup wizard ===
|
|
131
|
+
"setup.welcome": {"zh": "首次使用,讓我們快速完成設定!", "en": "First time? Let's set things up!"},
|
|
132
|
+
"setup.step_url": {"zh": "Moodle 網址", "en": "Moodle URL"},
|
|
133
|
+
"setup.step_url_hint": {
|
|
134
|
+
"zh": "預設為 NYCU E3 平台,直接按 Enter 使用預設值",
|
|
135
|
+
"en": "Default is NYCU E3. Press Enter to use default.",
|
|
136
|
+
},
|
|
137
|
+
"setup.step_dir": {"zh": "教材下載目錄", "en": "Download directory"},
|
|
138
|
+
"setup.step_dir_hint": {"zh": "課程教材會下載到這個資料夾", "en": "Course materials will be saved here"},
|
|
139
|
+
"setup.step_save": {"zh": "儲存設定", "en": "Save config"},
|
|
140
|
+
"setup.config_saved": {"zh": "✓ 設定已儲存至 {path}", "en": "✓ Config saved to {path}"},
|
|
141
|
+
"setup.step_login": {"zh": "登入帳號", "en": "Login"},
|
|
142
|
+
"setup.want_login": {"zh": "現在要登入嗎?", "en": "Login now?"},
|
|
143
|
+
"setup.prompt_id": {"zh": "帳號 (學號)", "en": "Username (Student ID)"},
|
|
144
|
+
"setup.want_save_creds": {"zh": "記住帳密?(加密儲存,下次自動登入)", "en": "Remember credentials? (encrypted, auto-login)"},
|
|
145
|
+
"setup.login_fail_hint": {
|
|
146
|
+
"zh": "沒關係,之後可以用 e3cli login 重新登入。",
|
|
147
|
+
"en": "No worries, run e3cli login later.",
|
|
148
|
+
},
|
|
149
|
+
"setup.done_title": {"zh": "設定完成!", "en": "Setup complete!"},
|
|
150
|
+
"setup.done_body": {
|
|
151
|
+
"zh": (
|
|
152
|
+
"接下來你可以:\n"
|
|
153
|
+
" [cyan]e3cli courses[/cyan] 列出修課清單\n"
|
|
154
|
+
" [cyan]e3cli assignments[/cyan] 查看作業與截止日期\n"
|
|
155
|
+
" [cyan]e3cli download --all[/cyan] 下載所有教材\n"
|
|
156
|
+
" [cyan]e3cli sync[/cyan] 一鍵同步所有內容\n"
|
|
157
|
+
" [cyan]e3cli --help[/cyan] 查看所有指令"
|
|
158
|
+
),
|
|
159
|
+
"en": (
|
|
160
|
+
"You can now:\n"
|
|
161
|
+
" [cyan]e3cli courses[/cyan] List enrolled courses\n"
|
|
162
|
+
" [cyan]e3cli assignments[/cyan] View assignments & deadlines\n"
|
|
163
|
+
" [cyan]e3cli download --all[/cyan] Download all materials\n"
|
|
164
|
+
" [cyan]e3cli sync[/cyan] Sync everything\n"
|
|
165
|
+
" [cyan]e3cli --help[/cyan] Show all commands"
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
# === Common ===
|
|
170
|
+
"common.not_logged_in": {
|
|
171
|
+
"zh": "尚未登入,請先執行 e3cli login",
|
|
172
|
+
"en": "Not logged in. Run e3cli login first.",
|
|
173
|
+
},
|
|
174
|
+
"common.no_courses": {"zh": "沒有課程。", "en": "No courses."},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _detect_lang() -> str:
|
|
179
|
+
"""偵測語言,優先順序:E3CLI_LANG 環境變數 > config.toml > 系統語言。"""
|
|
180
|
+
# 1. 環境變數最優先
|
|
181
|
+
env_lang = os.environ.get("E3CLI_LANG", "").lower()
|
|
182
|
+
if env_lang in ("en", "zh"):
|
|
183
|
+
return env_lang
|
|
184
|
+
|
|
185
|
+
# 2. 讀取 config.toml 裡的 lang 設定
|
|
186
|
+
try:
|
|
187
|
+
from e3cli.config import CONFIG_FILE
|
|
188
|
+
if CONFIG_FILE.exists():
|
|
189
|
+
import tomllib
|
|
190
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
191
|
+
data = tomllib.load(f)
|
|
192
|
+
cfg_lang = data.get("general", {}).get("lang", "").lower()
|
|
193
|
+
if cfg_lang in ("en", "zh"):
|
|
194
|
+
return cfg_lang
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
# 3. 系統語言
|
|
199
|
+
try:
|
|
200
|
+
sys_lang = locale.getdefaultlocale()[0] or ""
|
|
201
|
+
except Exception:
|
|
202
|
+
sys_lang = ""
|
|
203
|
+
|
|
204
|
+
if sys_lang.startswith("zh"):
|
|
205
|
+
return "zh"
|
|
206
|
+
return "en"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
_current_lang: str | None = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_lang() -> str:
|
|
213
|
+
global _current_lang
|
|
214
|
+
if _current_lang is None:
|
|
215
|
+
_current_lang = _detect_lang()
|
|
216
|
+
return _current_lang
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_lang(lang: str) -> None:
|
|
220
|
+
global _current_lang
|
|
221
|
+
_current_lang = lang
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def t(key: str, **kwargs) -> str:
|
|
225
|
+
"""取得翻譯字串,支援 format 變數。"""
|
|
226
|
+
entry = _STRINGS.get(key)
|
|
227
|
+
if entry is None:
|
|
228
|
+
return key
|
|
229
|
+
lang = get_lang()
|
|
230
|
+
text = entry.get(lang, entry.get("en", key))
|
|
231
|
+
if kwargs:
|
|
232
|
+
text = text.format(**kwargs)
|
|
233
|
+
return text
|
|
File without changes
|
e3cli/scheduler/cron.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Crontab 排程管理。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
CRON_MARKER = "# e3cli-sync"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_e3cli_cmd() -> str:
|
|
13
|
+
"""取得 e3cli 的完整路徑。"""
|
|
14
|
+
path = shutil.which("e3cli")
|
|
15
|
+
if path:
|
|
16
|
+
return path
|
|
17
|
+
return f"{sys.executable} -m e3cli"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_current_crontab() -> list[str]:
|
|
21
|
+
result = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
|
|
22
|
+
if result.returncode != 0:
|
|
23
|
+
return []
|
|
24
|
+
return [line for line in result.stdout.strip().split("\n") if line]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def install(interval_minutes: int = 60) -> None:
|
|
28
|
+
"""安裝 cron job 定時執行 e3cli sync。"""
|
|
29
|
+
lines = [entry for entry in _get_current_crontab() if CRON_MARKER not in entry]
|
|
30
|
+
cmd = _get_e3cli_cmd()
|
|
31
|
+
lines.append(f"*/{interval_minutes} * * * * {cmd} sync --quiet {CRON_MARKER}")
|
|
32
|
+
subprocess.run(["crontab", "-"], input="\n".join(lines) + "\n", text=True, check=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def uninstall() -> None:
|
|
36
|
+
"""移除 e3cli 的 cron job。"""
|
|
37
|
+
lines = [entry for entry in _get_current_crontab() if CRON_MARKER not in entry]
|
|
38
|
+
if lines:
|
|
39
|
+
subprocess.run(["crontab", "-"], input="\n".join(lines) + "\n", text=True, check=True)
|
|
40
|
+
else:
|
|
41
|
+
subprocess.run(["crontab", "-r"], capture_output=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_installed() -> bool:
|
|
45
|
+
"""檢查 cron job 是否已安裝。"""
|
|
46
|
+
return any(CRON_MARKER in entry for entry in _get_current_crontab())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_schedule_line() -> str | None:
|
|
50
|
+
"""取得目前的排程設定行。"""
|
|
51
|
+
for entry in _get_current_crontab():
|
|
52
|
+
if CRON_MARKER in entry:
|
|
53
|
+
return entry
|
|
54
|
+
return None
|
|
File without changes
|
e3cli/storage/db.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""SQLite 資料庫管理 — 追蹤已下載的檔案和作業狀態。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_SCHEMA = """
|
|
9
|
+
CREATE TABLE IF NOT EXISTS courses (
|
|
10
|
+
id INTEGER PRIMARY KEY,
|
|
11
|
+
shortname TEXT NOT NULL,
|
|
12
|
+
fullname TEXT NOT NULL,
|
|
13
|
+
last_synced INTEGER
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE IF NOT EXISTS downloaded_files (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
course_id INTEGER NOT NULL,
|
|
19
|
+
module_id INTEGER NOT NULL,
|
|
20
|
+
filename TEXT NOT NULL,
|
|
21
|
+
fileurl TEXT NOT NULL,
|
|
22
|
+
filesize INTEGER,
|
|
23
|
+
time_modified INTEGER NOT NULL,
|
|
24
|
+
local_path TEXT NOT NULL,
|
|
25
|
+
downloaded_at INTEGER NOT NULL,
|
|
26
|
+
UNIQUE(course_id, module_id, filename)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS assignments (
|
|
30
|
+
id INTEGER PRIMARY KEY,
|
|
31
|
+
course_id INTEGER NOT NULL,
|
|
32
|
+
course_name TEXT NOT NULL DEFAULT '',
|
|
33
|
+
name TEXT NOT NULL,
|
|
34
|
+
duedate INTEGER,
|
|
35
|
+
status TEXT DEFAULT 'new',
|
|
36
|
+
last_checked INTEGER,
|
|
37
|
+
notified INTEGER DEFAULT 0
|
|
38
|
+
);
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Database:
|
|
43
|
+
def __init__(self, db_path: Path):
|
|
44
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
self.conn = sqlite3.connect(str(db_path))
|
|
46
|
+
self.conn.row_factory = sqlite3.Row
|
|
47
|
+
self._init_schema()
|
|
48
|
+
|
|
49
|
+
def _init_schema(self) -> None:
|
|
50
|
+
self.conn.executescript(_SCHEMA)
|
|
51
|
+
self.conn.commit()
|
|
52
|
+
|
|
53
|
+
def close(self) -> None:
|
|
54
|
+
self.conn.close()
|
|
55
|
+
|
|
56
|
+
# --- 課程 ---
|
|
57
|
+
|
|
58
|
+
def upsert_course(self, course_id: int, shortname: str, fullname: str) -> None:
|
|
59
|
+
self.conn.execute(
|
|
60
|
+
"INSERT INTO courses (id, shortname, fullname) VALUES (?, ?, ?) "
|
|
61
|
+
"ON CONFLICT(id) DO UPDATE SET shortname=excluded.shortname, fullname=excluded.fullname",
|
|
62
|
+
(course_id, shortname, fullname),
|
|
63
|
+
)
|
|
64
|
+
self.conn.commit()
|
|
65
|
+
|
|
66
|
+
def get_courses(self) -> list[sqlite3.Row]:
|
|
67
|
+
return self.conn.execute("SELECT * FROM courses ORDER BY id").fetchall()
|
|
68
|
+
|
|
69
|
+
# --- 已下載檔案 ---
|
|
70
|
+
|
|
71
|
+
def is_downloaded(self, course_id: int, module_id: int, filename: str, time_modified: int) -> bool:
|
|
72
|
+
"""檢查檔案是否已下載且未更新。"""
|
|
73
|
+
row = self.conn.execute(
|
|
74
|
+
"SELECT time_modified FROM downloaded_files "
|
|
75
|
+
"WHERE course_id=? AND module_id=? AND filename=?",
|
|
76
|
+
(course_id, module_id, filename),
|
|
77
|
+
).fetchone()
|
|
78
|
+
return row is not None and row["time_modified"] >= time_modified
|
|
79
|
+
|
|
80
|
+
def record_download(
|
|
81
|
+
self, course_id: int, module_id: int, filename: str,
|
|
82
|
+
fileurl: str, filesize: int, time_modified: int,
|
|
83
|
+
local_path: str, downloaded_at: int,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.conn.execute(
|
|
86
|
+
"INSERT INTO downloaded_files "
|
|
87
|
+
"(course_id, module_id, filename, fileurl, filesize, time_modified, local_path, downloaded_at) "
|
|
88
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?) "
|
|
89
|
+
"ON CONFLICT(course_id, module_id, filename) DO UPDATE SET "
|
|
90
|
+
"fileurl=excluded.fileurl, filesize=excluded.filesize, "
|
|
91
|
+
"time_modified=excluded.time_modified, local_path=excluded.local_path, "
|
|
92
|
+
"downloaded_at=excluded.downloaded_at",
|
|
93
|
+
(course_id, module_id, filename, fileurl, filesize, time_modified, local_path, downloaded_at),
|
|
94
|
+
)
|
|
95
|
+
self.conn.commit()
|
|
96
|
+
|
|
97
|
+
# --- 作業 ---
|
|
98
|
+
|
|
99
|
+
def upsert_assignment(
|
|
100
|
+
self, assign_id: int, course_id: int, course_name: str,
|
|
101
|
+
name: str, duedate: int, last_checked: int,
|
|
102
|
+
) -> bool:
|
|
103
|
+
"""更新或新增作業,回傳是否為新作業。"""
|
|
104
|
+
existing = self.conn.execute("SELECT id FROM assignments WHERE id=?", (assign_id,)).fetchone()
|
|
105
|
+
self.conn.execute(
|
|
106
|
+
"INSERT INTO assignments (id, course_id, course_name, name, duedate, last_checked) "
|
|
107
|
+
"VALUES (?, ?, ?, ?, ?, ?) "
|
|
108
|
+
"ON CONFLICT(id) DO UPDATE SET "
|
|
109
|
+
"name=excluded.name, duedate=excluded.duedate, last_checked=excluded.last_checked",
|
|
110
|
+
(assign_id, course_id, course_name, name, duedate, last_checked),
|
|
111
|
+
)
|
|
112
|
+
self.conn.commit()
|
|
113
|
+
return existing is None
|
|
114
|
+
|
|
115
|
+
def get_assignments(self) -> list[sqlite3.Row]:
|
|
116
|
+
return self.conn.execute(
|
|
117
|
+
"SELECT * FROM assignments ORDER BY duedate ASC"
|
|
118
|
+
).fetchall()
|
|
119
|
+
|
|
120
|
+
def update_assignment_status(self, assign_id: int, status: str) -> None:
|
|
121
|
+
self.conn.execute(
|
|
122
|
+
"UPDATE assignments SET status=? WHERE id=?", (status, assign_id)
|
|
123
|
+
)
|
|
124
|
+
self.conn.commit()
|
e3cli/storage/models.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""資料模型。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Course:
|
|
10
|
+
id: int
|
|
11
|
+
shortname: str
|
|
12
|
+
fullname: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Assignment:
|
|
17
|
+
id: int
|
|
18
|
+
course_id: int
|
|
19
|
+
course_name: str
|
|
20
|
+
name: str
|
|
21
|
+
duedate: int # Unix timestamp, 0 = 無截止日
|
|
22
|
+
status: str = "new" # new, draft, submitted
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CourseFile:
|
|
27
|
+
course_id: int
|
|
28
|
+
module_id: int
|
|
29
|
+
filename: str
|
|
30
|
+
fileurl: str
|
|
31
|
+
filesize: int
|
|
32
|
+
time_modified: int
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""追蹤邏輯的高階封裝。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from e3cli.storage.db import Database
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_pending_assignments(db: Database) -> list:
|
|
9
|
+
"""取得尚未提交的作業。"""
|
|
10
|
+
rows = db.get_assignments()
|
|
11
|
+
return [r for r in rows if r["status"] not in ("submitted",) and r["duedate"] != 0]
|