steveyehtw commited on
Commit
ebb4d1d
·
1 Parent(s): 2ad50f4

Show actionable Hugging Face persistence errors

Browse files
Files changed (2) hide show
  1. classroom_service.py +17 -5
  2. tests/test_classroom.py +15 -0
classroom_service.py CHANGED
@@ -1,6 +1,6 @@
1
  """Browser-side multi-file Python classroom for the Telearn demo Space."""
2
  from __future__ import annotations
3
- import asyncio, hmac, io, json, os, re, sqlite3, threading, time, zipfile
4
  from pathlib import Path, PurePosixPath
5
  from typing import Any
6
  from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
@@ -9,9 +9,9 @@ from pydantic import BaseModel
9
 
10
  ROOT=Path(__file__).resolve().parent; STATIC=ROOT/"classroom_static"; DATA=ROOT/"classroom_data"; TEMPLATES=DATA/"templates"; PUBLISHED=ROOT/"projects"
11
  RUNTIME=Path(os.getenv("CLASSROOM_DATA_DIR","/tmp/telearn_classroom")); DB=RUNTIME/"classroom.sqlite3"
12
- PASSWORD=os.getenv("CLASSROOM_TEACHER_PASSWORD",""); REPO=os.getenv("CLASSROOM_DEMO_REPO",os.getenv("SPACE_ID","Telearn/Project_Demo")); DEMO_CLASS=os.getenv("CLASSROOM_DEMO_CLASS","2026Fall進階專題"); TOKEN=os.getenv("HF_TOKEN","")
13
  ALLOWED={".py",".json",".txt",".csv",".md",".toml",".yaml",".yml"}; ID_RE=re.compile(r"[^0-9A-Za-z_\-一-鿿]")
14
- router=APIRouter(); lock=threading.Lock(); students={}; teachers={}; live={}
15
 
16
  class TeacherRequest(BaseModel): class_code:str; week_id:str; password:str
17
  class CourseCreate(BaseModel): class_code:str; course_name:str=""; password:str
@@ -86,6 +86,17 @@ def published_progress(display_name):
86
  def check_password(p):
87
  if not PASSWORD:raise HTTPException(503,"請先在 Space Secrets 設定 CLASSROOM_TEACHER_PASSWORD")
88
  if not hmac.compare_digest(p,PASSWORD):raise HTTPException(403,"教師密碼不正確")
 
 
 
 
 
 
 
 
 
 
 
89
  def finals(c,w):return [x for x in snapshot(c,w) if x.get("final_files")]
90
  def make_zip(c,w):
91
  b=io.BytesIO()
@@ -118,8 +129,9 @@ def create_class(req:CourseCreate):
118
  from huggingface_hub import CommitOperationAdd,HfApi
119
  payload=json.dumps([{"code":c,"name":n} for c,n in rows],ensure_ascii=False,indent=2)+"\n"
120
  HfApi(token=TOKEN).create_commit(repo_id=REPO,repo_type="space",operations=[CommitOperationAdd(path_in_repo="classroom_data/classes.json",path_or_fileobj=payload.encode())],commit_message=f"Add classroom course {code}")
121
- except Exception:
122
- warning="課程已可使用,但尚未永久寫入 Space;請檢查 HF_TOKEN 的寫入權限。"
 
123
  else:warning="課程已可使用,但尚未永久寫入 Space;請設定 HF_TOKEN。"
124
  return {"ok":True,"class_code":code,"course_name":name,"already_exists":bool(existing),"warning":warning}
125
  @router.delete("/api/classroom/students")
 
1
  """Browser-side multi-file Python classroom for the Telearn demo Space."""
2
  from __future__ import annotations
3
+ import asyncio, hmac, io, json, logging, os, re, sqlite3, threading, time, zipfile
4
  from pathlib import Path, PurePosixPath
5
  from typing import Any
6
  from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
 
9
 
10
  ROOT=Path(__file__).resolve().parent; STATIC=ROOT/"classroom_static"; DATA=ROOT/"classroom_data"; TEMPLATES=DATA/"templates"; PUBLISHED=ROOT/"projects"
11
  RUNTIME=Path(os.getenv("CLASSROOM_DATA_DIR","/tmp/telearn_classroom")); DB=RUNTIME/"classroom.sqlite3"
12
+ PASSWORD=os.getenv("CLASSROOM_TEACHER_PASSWORD",""); REPO=os.getenv("CLASSROOM_DEMO_REPO",os.getenv("SPACE_ID","Telearn/Project_Demo")).strip(); DEMO_CLASS=os.getenv("CLASSROOM_DEMO_CLASS","2026Fall進階專題"); TOKEN=os.getenv("HF_TOKEN","").strip()
13
  ALLOWED={".py",".json",".txt",".csv",".md",".toml",".yaml",".yml"}; ID_RE=re.compile(r"[^0-9A-Za-z_\-一-鿿]")
14
+ router=APIRouter(); lock=threading.Lock(); students={}; teachers={}; live={}; logger=logging.getLogger(__name__)
15
 
16
  class TeacherRequest(BaseModel): class_code:str; week_id:str; password:str
17
  class CourseCreate(BaseModel): class_code:str; course_name:str=""; password:str
 
86
  def check_password(p):
87
  if not PASSWORD:raise HTTPException(503,"請先在 Space Secrets 設定 CLASSROOM_TEACHER_PASSWORD")
88
  if not hmac.compare_digest(p,PASSWORD):raise HTTPException(403,"教師密碼不正確")
89
+ def hf_failure_message(exc):
90
+ """Return an actionable message without ever exposing a token."""
91
+ status=getattr(getattr(exc,"response",None),"status_code",None)
92
+ detail=getattr(exc,"server_message",None) or str(exc) or type(exc).__name__
93
+ detail=re.sub(r"hf_[A-Za-z0-9]+","hf_***",re.sub(r"\s+"," ",detail)).strip()[:220]
94
+ if status==401:reason="HF_TOKEN 未被 Space 正確載入、已失效或貼入值不正確"
95
+ elif status==403:reason=f"HF_TOKEN 已被載入,但對 {REPO} 的 Space 寫入驗證被拒絕"
96
+ elif status==404:reason=f"找不到目標 Space {REPO},請檢查 CLASSROOM_DEMO_REPO 或 SPACE_ID"
97
+ else:reason=f"連線到 Hugging Face 時發生 {type(exc).__name__}"
98
+ suffix=f"HTTP {status}:{detail}" if status else detail
99
+ return f"課程已可使用,但尚未永久寫入 Space。{reason}。詳情:{suffix}"
100
  def finals(c,w):return [x for x in snapshot(c,w) if x.get("final_files")]
101
  def make_zip(c,w):
102
  b=io.BytesIO()
 
129
  from huggingface_hub import CommitOperationAdd,HfApi
130
  payload=json.dumps([{"code":c,"name":n} for c,n in rows],ensure_ascii=False,indent=2)+"\n"
131
  HfApi(token=TOKEN).create_commit(repo_id=REPO,repo_type="space",operations=[CommitOperationAdd(path_in_repo="classroom_data/classes.json",path_or_fileobj=payload.encode())],commit_message=f"Add classroom course {code}")
132
+ except Exception as exc:
133
+ logger.exception("Could not persist classroom course catalog to %s",REPO)
134
+ warning=hf_failure_message(exc)
135
  else:warning="課程已可使用,但尚未永久寫入 Space;請設定 HF_TOKEN。"
136
  return {"ok":True,"class_code":code,"course_name":name,"already_exists":bool(existing),"warning":warning}
137
  @router.delete("/api/classroom/students")
tests/test_classroom.py CHANGED
@@ -5,6 +5,7 @@ import tempfile
5
  import unittest
6
  import zipfile
7
  from pathlib import Path
 
8
 
9
  import classroom_service as classroom
10
 
@@ -115,6 +116,20 @@ class ClassroomServiceTests(unittest.TestCase):
115
  course = next(item for item in classroom.catalog()["courses"] if item["class_code"] == "L6B")
116
  self.assertEqual(course["course_name"], "週六進階班")
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  def test_published_final_can_be_loaded_after_runtime_restart(self):
119
  old_published = classroom.PUBLISHED
120
  try:
 
5
  import unittest
6
  import zipfile
7
  from pathlib import Path
8
+ from types import SimpleNamespace
9
 
10
  import classroom_service as classroom
11
 
 
116
  course = next(item for item in classroom.catalog()["courses"] if item["class_code"] == "L6B")
117
  self.assertEqual(course["course_name"], "週六進階班")
118
 
119
+ def test_hf_failure_message_distinguishes_authentication_and_permission(self):
120
+ unauthorized = RuntimeError("token hf_should_never_leak was rejected")
121
+ unauthorized.response = SimpleNamespace(status_code=401)
122
+ forbidden = RuntimeError("Forbidden")
123
+ forbidden.response = SimpleNamespace(status_code=403)
124
+
125
+ unauthorized_message = classroom.hf_failure_message(unauthorized)
126
+ forbidden_message = classroom.hf_failure_message(forbidden)
127
+
128
+ self.assertIn("HF_TOKEN 未被 Space 正確載入", unauthorized_message)
129
+ self.assertNotIn("hf_should_never_leak", unauthorized_message)
130
+ self.assertIn(classroom.REPO, forbidden_message)
131
+ self.assertIn("寫入驗證被拒絕", forbidden_message)
132
+
133
  def test_published_final_can_be_loaded_after_runtime_restart(self):
134
  old_published = classroom.PUBLISHED
135
  try: