3939JW_SCHEDULE_TABLE_URL = "https://jw.ustc.edu.cn/ws/schedule-table/datum"
4040MIN_CATALOG_LESSON_SEMESTER_ID = 221
4141MIN_CATALOG_EXAM_SEMESTER_ID = 381
42+ JW_SCHEDULE_CHUNK_SIZE = 100
43+ JW_SCHEDULE_EXPECTED_CHUNK_COUNT_KEY_PREFIX = "jw_schedule_expected_chunk_count_"
4244
4345
44- def _course_chunks (courses : list [Course ], chunk_size : int = 100 ) -> list [list [Course ]]:
46+ def _course_chunks (
47+ courses : list [Course ], chunk_size : int = JW_SCHEDULE_CHUNK_SIZE
48+ ) -> list [list [Course ]]:
4549 return [courses [i : i + chunk_size ] for i in range (0 , len (courses ), chunk_size )]
4650
4751
@@ -128,24 +132,109 @@ def _has_cached_catalog_exams(store: SQLiteModelStore, semester_id: str) -> bool
128132 )
129133
130134
131- def _has_cached_jw_schedule (store : SQLiteModelStore , semester_id : str ) -> bool :
135+ def _jw_schedule_expected_chunk_count_key (semester_id : str ) -> str :
136+ return f"{ JW_SCHEDULE_EXPECTED_CHUNK_COUNT_KEY_PREFIX } { semester_id } "
137+
138+
139+ def _catalog_lesson_chunk_count (
140+ store : SQLiteModelStore , semester_id : str
141+ ) -> int | None :
142+ table_exists = store .conn .execute (
143+ """
144+ SELECT 1 FROM sqlite_master
145+ WHERE type = 'table'
146+ AND name = 'catalog_teach_lesson_list_for_teach'
147+ """
148+ ).fetchone ()
149+ if table_exists is None :
150+ return None
151+
152+ fetch = store .conn .execute (
153+ """
154+ SELECT id FROM upstream_fetches
155+ WHERE source = 'catalog_teach_lesson_list_for_teach'
156+ AND ok = 1
157+ AND context = ?
158+ ORDER BY id DESC
159+ LIMIT 1
160+ """ ,
161+ (f"semester_id={ semester_id } " ,),
162+ ).fetchone ()
163+ if fetch is None :
164+ return None
165+
166+ lesson_count = store .conn .execute (
167+ """
168+ SELECT COUNT(*) FROM catalog_teach_lesson_list_for_teach
169+ WHERE fetch_id = ?
170+ """ ,
171+ (fetch [0 ],),
172+ ).fetchone ()[0 ]
132173 return (
133- store .conn .execute (
134- """
135- SELECT 1 FROM upstream_fetches
136- WHERE source = ?
137- AND ok = 1
138- AND (context = ? OR context LIKE ?)
139- LIMIT 1
140- """ ,
141- (
142- "jw_ws_schedule_table_datum" ,
143- f"semester_id={ semester_id } " ,
144- f"%&semester_id={ semester_id } " ,
145- ),
146- ).fetchone ()
147- is not None
174+ int (lesson_count ) + JW_SCHEDULE_CHUNK_SIZE - 1
175+ ) // JW_SCHEDULE_CHUNK_SIZE
176+
177+
178+ def _expected_jw_schedule_chunk_count (
179+ store : SQLiteModelStore , semester_id : str
180+ ) -> int | None :
181+ row = store .conn .execute (
182+ "SELECT value FROM metadata WHERE key = ?" ,
183+ (_jw_schedule_expected_chunk_count_key (semester_id ),),
184+ ).fetchone ()
185+ recorded_count = None
186+ if row is not None :
187+ try :
188+ recorded_count = int (row [0 ])
189+ except ValueError :
190+ return None
191+ if recorded_count < 0 :
192+ return None
193+
194+ catalog_count = _catalog_lesson_chunk_count (store , semester_id )
195+ if catalog_count is None :
196+ return recorded_count
197+ if recorded_count is not None and recorded_count != catalog_count :
198+ return None
199+ return catalog_count
200+
201+
202+ def _fetch_context_values (context : str | None ) -> dict [str , str ]:
203+ return {
204+ key : value
205+ for item in (context or "" ).split ("&" )
206+ if "=" in item
207+ for key , value in [item .split ("=" , 1 )]
208+ }
209+
210+
211+ def _has_cached_jw_schedule (store : SQLiteModelStore , semester_id : str ) -> bool :
212+ expected_count = _expected_jw_schedule_chunk_count (store , semester_id )
213+ if expected_count is None :
214+ return False
215+
216+ successful_chunks : set [int ] = set ()
217+ fetches = store .conn .execute (
218+ """
219+ SELECT ok, context FROM upstream_fetches
220+ WHERE source = 'jw_ws_schedule_table_datum'
221+ """
148222 )
223+ for ok , context in fetches :
224+ values = _fetch_context_values (context )
225+ if values .get ("semester_id" ) != semester_id :
226+ continue
227+ if not ok :
228+ return False
229+ try :
230+ chunk_index = int (values ["chunk_index" ])
231+ except (KeyError , ValueError ):
232+ return False
233+ if chunk_index in successful_chunks :
234+ return False
235+ successful_chunks .add (chunk_index )
236+
237+ return successful_chunks == set (range (expected_count ))
149238
150239
151240def _has_cached_source_semester (
@@ -330,13 +419,12 @@ async def _store_jw_schedule_chunks(
330419 courses : list [Course ],
331420) -> None :
332421 chunks = _course_chunks (courses )
333- if not chunks :
334- guesses .add_teacher_section_guesses (
335- semester_id = semester_id ,
336- catalog_lessons = catalog_response ,
337- jw_schedules = None ,
338- )
339- return
422+ store .put_metadata (
423+ {
424+ "jw_schedule_chunk_size" : JW_SCHEDULE_CHUNK_SIZE ,
425+ _jw_schedule_expected_chunk_count_key (semester_id ): len (chunks ),
426+ }
427+ )
340428
341429 schedule_responses : list [JwWsScheduleTableDatumResponse ] = []
342430 for chunk_index , chunk in enumerate (chunks ):
@@ -354,13 +442,13 @@ async def _store_jw_schedule_chunks(
354442 ok = False ,
355443 error = str (e ),
356444 )
357- logger .info (
358- "Skipping remaining JW schedule table chunks for semester %s after "
359- "non-JSON response at chunk %s" ,
445+ logger .error (
446+ "Aborting JW schedule table fetch for semester %s after non-JSON "
447+ "response at chunk %s" ,
360448 semester_id ,
361449 chunk_index ,
362450 )
363- break
451+ raise
364452
365453 response = JwWsScheduleTableDatumResponse .model_validate (payload )
366454 schedule_responses .append (response )
@@ -377,6 +465,10 @@ async def _store_jw_schedule_chunks(
377465 context = {"semester_id" : semester_id , "chunk_index" : chunk_index },
378466 )
379467
468+ if not _has_cached_jw_schedule (store , semester_id ):
469+ raise RuntimeError (
470+ f"Incomplete JW schedule table chunks for semester { semester_id } "
471+ )
380472 guesses .add_teacher_section_guesses (
381473 semester_id = semester_id ,
382474 catalog_lessons = catalog_response ,
0 commit comments