Coverage for src/data_api/api/routers/calendars.py: 100%

42 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-11-13 19:28 +0000

1"""Campus calendar endpoints.""" 

2 

3from datetime import date 

4from typing import Optional 

5 

6from fastapi import APIRouter, HTTPException, Path, Query, Response 

7 

8from data_api.api.schemas.calendars import Calendar, CalendarEvent 

9from data_api.api.schemas.errors import ErrorResponse 

10from data_api.api.schemas.responses import COUNT_HEADERS, SNAPSHOT_RESPONSES 

11from data_api.runtime import get_runtime 

12 

13router = APIRouter(responses=SNAPSHOT_RESPONSES) 

14 

15CALENDAR_ID_PATH = Path( 

16 ..., description="行事曆 id,可由 /calendars 取得,例如 academic、library-main" 

17) 

18 

19 

20@router.get("", response_model=list[Calendar], operation_id="getAllCalendars") 

21async def get_all_calendars(response: Response): 

22 """ 

23 取得所有校園行事曆的資訊(不含事件)。 

24 包含校級與圖書館開館時間行事曆。 

25 資料來源:[清華校園資料](https://data.nthusa.tw/calendars.json)、 

26 [圖書館行事曆](https://data.nthusa.tw/libraries/calendars.json) 

27 """ 

28 commit_hash, calendars = await get_runtime().calendars.get_all_calendars() 

29 if commit_hash is not None: 

30 response.headers["X-Data-Commit-Hash"] = commit_hash 

31 return calendars 

32 

33 

34@router.get( 

35 "/{calendar_id}", 

36 response_model=Calendar, 

37 operation_id="getCalendar", 

38 responses={404: {"model": ErrorResponse, "description": "Calendar not found"}}, 

39) 

40async def get_calendar(response: Response, calendar_id: str = CALENDAR_ID_PATH): 

41 """取得指定行事曆的資訊(不含事件)。""" 

42 commit_hash, calendar = await get_runtime().calendars.get_calendar(calendar_id) 

43 if calendar is None: 

44 raise HTTPException(status_code=404, detail="找不到行事曆") 

45 if commit_hash is not None: 

46 response.headers["X-Data-Commit-Hash"] = commit_hash 

47 return calendar 

48 

49 

50@router.get( 

51 "/{calendar_id}/events", 

52 response_model=list[CalendarEvent], 

53 operation_id="searchCalendarEvents", 

54 responses={ 

55 200: {"headers": COUNT_HEADERS}, 

56 400: {"model": ErrorResponse, "description": "Start date must not follow end date"}, 

57 404: {"model": ErrorResponse, "description": "Calendar not found"}, 

58 }, 

59) 

60async def search_calendar_events( 

61 response: Response, 

62 calendar_id: str = CALENDAR_ID_PATH, 

63 start: Optional[date] = Query( 

64 None, description="只回傳在此日期(含)之後仍進行中的事件,格式 YYYY-MM-DD" 

65 ), 

66 end: Optional[date] = Query( 

67 None, description="只回傳在此日期(含)之前開始的事件,格式 YYYY-MM-DD" 

68 ), 

69 keyword: Optional[str] = Query( 

70 None, description="在標題與說明中搜尋的關鍵字,不區分大小寫,不使用正規表示式" 

71 ), 

72 limit: int = Query(100, ge=1, le=1000, description="最多回傳筆數"), 

73 offset: int = Query(0, ge=0, description="略過的筆數,用於分頁"), 

74): 

75 """ 

76 查詢與指定日期區間重疊的事件,依開始時間、事件 id 排序。 

77 

78 日期採用來源行事曆的當地日期,區間包含首尾兩日,可省略任一邊界。 

79 全天事件的結束日期不包含在內;恰好於午夜結束的事件不涵蓋隔日。 

80 所有篩選條件以 AND 結合,分頁前的符合筆數放在 `X-Total-Count` header。 

81 

82 例如:`/calendars/academic/events?start=2026-10-01&end=2026-10-31` 

83 """ 

84 if start and end and start > end: 

85 raise HTTPException(status_code=400, detail="start 不可晚於 end") 

86 commit_hash, events = await get_runtime().calendars.search_calendar_events( 

87 calendar_id, start=start, end=end, keyword=keyword 

88 ) 

89 if events is None: 

90 raise HTTPException(status_code=404, detail="找不到行事曆") 

91 if commit_hash is not None: 

92 response.headers["X-Data-Commit-Hash"] = commit_hash 

93 response.headers["X-Total-Count"] = str(len(events)) 

94 return events[offset : offset + limit] 

95 

96 

97@router.get( 

98 "/{calendar_id}/events/{event_id}", 

99 response_model=CalendarEvent, 

100 operation_id="getCalendarEvent", 

101 responses={404: {"model": ErrorResponse, "description": "Calendar event not found"}}, 

102) 

103async def get_calendar_event( 

104 response: Response, 

105 calendar_id: str = CALENDAR_ID_PATH, 

106 event_id: str = Path(..., description="事件 id"), 

107): 

108 """取得指定行事曆中的單一事件。""" 

109 commit_hash, event = await get_runtime().calendars.get_calendar_event(calendar_id, event_id) 

110 if event is None: 

111 raise HTTPException(status_code=404, detail="找不到事件") 

112 if commit_hash is not None: 

113 response.headers["X-Data-Commit-Hash"] = commit_hash 

114 return event