Coverage for src/worblehat/queries/deadline_daemon.py: 94%

28 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-25 22:25 +0000

1from datetime import datetime, timedelta 

2 

3from sqlalchemy import func, select 

4from sqlalchemy.orm import Session 

5from sqlalchemy.sql.elements import SQLColumnExpression 

6 

7from worblehat.models import ( 

8 Borrowing, 

9 BorrowingEventType, 

10 BorrowingLog, 

11 DeadlineDaemonLastRunDatetime, 

12 QueuePosition, 

13) 

14 

15 

16def _sql_subtract_date( 

17 sql_session: Session, 

18 x: SQLColumnExpression[datetime], 

19 y: timedelta, 

20) -> SQLColumnExpression[datetime]: 

21 dialect_name = sql_session.get_bind().dialect.name 

22 if dialect_name == "sqlite": 

23 # SQLite does not support timedelta in queries 

24 return func.datetime(x, f"-{y.days} days") 

25 if dialect_name == "postgresql": 25 ↛ 27line 25 didn't jump to line 27 because the condition on line 25 was always true

26 return x - y 

27 raise NotImplementedError( 

28 f"Unsupported dialect: {dialect_name}", 

29 ) 

30 

31 

32def find_last_run(sql_session: Session) -> DeadlineDaemonLastRunDatetime | None: 

33 return sql_session.scalars( 

34 select(DeadlineDaemonLastRunDatetime), 

35 ).one_or_none() 

36 

37 

38def list_close_deadline_borrowings( 

39 sql_session: Session, 

40 day: int, 

41 last_run_datetime: datetime, 

42 current_run_datetime: datetime, 

43) -> list[Borrowing]: 

44 return list( 

45 sql_session.scalars( 

46 select(Borrowing).where( 

47 _sql_subtract_date( 

48 sql_session, 

49 Borrowing.due_time, 

50 timedelta(days=day), 

51 ).between( 

52 last_run_datetime, 

53 current_run_datetime, 

54 ), 

55 ), 

56 ).all(), 

57 ) 

58 

59 

60def list_undelivered_overdue_borrowings( 

61 sql_session: Session, 

62 current_run_datetime: datetime, 

63) -> list[Borrowing]: 

64 return list( 

65 sql_session.scalars( 

66 select(Borrowing).where( 

67 Borrowing.due_time < current_run_datetime, 

68 ), 

69 ).all(), 

70 ) 

71 

72 

73def list_newly_available_queue_items( 

74 sql_session: Session, 

75 last_run_datetime: datetime, 

76 current_run_datetime: datetime, 

77) -> list[QueuePosition]: 

78 items_returned_since_last_run = ( 

79 select(BorrowingLog.fk_bookcase_item_uid) 

80 .where( 

81 BorrowingLog.event_type == BorrowingEventType.RETURNED, 

82 BorrowingLog.timestamp.between(last_run_datetime, current_run_datetime), 

83 ) 

84 .distinct() 

85 ) 

86 

87 return list( 

88 sql_session.scalars( 

89 select(QueuePosition) 

90 .where( 

91 QueuePosition.notified_available_time.is_(None), 

92 QueuePosition.fk_bookcase_item_uid.in_(items_returned_since_last_run), 

93 ) 

94 .order_by(QueuePosition.entered_queue_time) 

95 .group_by(QueuePosition.fk_bookcase_item_uid), 

96 ).all(), 

97 ) 

98 

99 

100def list_expiring_queue_positions( 

101 sql_session: Session, 

102 last_run_datetime: datetime, 

103 current_run_datetime: datetime, 

104) -> list[QueuePosition]: 

105 return list( 

106 sql_session.scalars( 

107 select(QueuePosition).where( 

108 QueuePosition.notified_available_time.between( 

109 last_run_datetime, 

110 current_run_datetime, 

111 ), 

112 ), 

113 ).all(), 

114 ) 

115 

116 

117def list_overdue_queue_positions( 

118 sql_session: Session, 

119 queue_position_expiry_days: int, 

120 current_run_datetime: datetime, 

121) -> list[QueuePosition]: 

122 expiry_cutoff = current_run_datetime - timedelta(days=queue_position_expiry_days) 

123 return list( 

124 sql_session.scalars( 

125 select(QueuePosition).where( 

126 QueuePosition.notified_available_time < expiry_cutoff, 

127 ), 

128 ).all(), 

129 ) 

130 

131 

132def find_next_queue_position( 

133 sql_session: Session, 

134 item_uid: int, 

135) -> QueuePosition | None: 

136 return sql_session.scalars( 

137 select(QueuePosition) 

138 .where( 

139 QueuePosition.fk_bookcase_item_uid == item_uid, 

140 QueuePosition.notified_available_time.is_(None), 

141 ) 

142 .order_by(QueuePosition.entered_queue_time) 

143 .limit(1), 

144 ).one_or_none()