Monkeytype pylib/anki/sched.py

This commit is contained in:
Alan Du 2020-02-26 00:49:07 -05:00
parent b157ee7570
commit 02dd30f2a0

View file

@ -8,7 +8,7 @@ import random
import time import time
from heapq import * from heapq import *
from operator import itemgetter from operator import itemgetter
from typing import List, Optional, Set from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
import anki import anki
from anki import hooks from anki import hooks
@ -56,14 +56,14 @@ class Scheduler:
return card return card
return None return None
def reset(self): def reset(self) -> None:
self._updateCutoff() self._updateCutoff()
self._resetLrn() self._resetLrn()
self._resetRev() self._resetRev()
self._resetNew() self._resetNew()
self._haveQueues = True self._haveQueues = True
def answerCard(self, card, ease): def answerCard(self, card: Card, ease: int) -> None:
self.col.log() self.col.log()
assert 1 <= ease <= 4 assert 1 <= ease <= 4
self.col.markReview(card) self.col.markReview(card)
@ -71,7 +71,7 @@ class Scheduler:
self._burySiblings(card) self._burySiblings(card)
card.reps += 1 card.reps += 1
# former is for logging new cards, latter also covers filt. decks # former is for logging new cards, latter also covers filt. decks
card.wasNew = card.type == CARD_TYPE_NEW card.wasNew = card.type == CARD_TYPE_NEW # type: ignore
wasNewQ = card.queue == QUEUE_TYPE_NEW wasNewQ = card.queue == QUEUE_TYPE_NEW
if wasNewQ: if wasNewQ:
# came from the new queue, move to learning # came from the new queue, move to learning
@ -102,7 +102,7 @@ class Scheduler:
card.usn = self.col.usn() card.usn = self.col.usn()
card.flushSched() card.flushSched()
def counts(self, card=None): def counts(self, card: Optional[Card] = None) -> Tuple[int, int, int]:
counts = [self.newCount, self.lrnCount, self.revCount] counts = [self.newCount, self.lrnCount, self.revCount]
if card: if card:
idx = self.countIdx(card) idx = self.countIdx(card)
@ -110,9 +110,11 @@ class Scheduler:
counts[1] += card.left // 1000 counts[1] += card.left // 1000
else: else:
counts[idx] += 1 counts[idx] += 1
return tuple(counts)
def dueForecast(self, days=7): new, lrn, rev = counts
return (new, lrn, rev)
def dueForecast(self, days: int = 7) -> List[Any]:
"Return counts over next DAYS. Includes today." "Return counts over next DAYS. Includes today."
daysd = dict( daysd = dict(
self.col.db.all( self.col.db.all(
@ -135,12 +137,12 @@ order by due"""
ret = [x[1] for x in sorted(daysd.items())] ret = [x[1] for x in sorted(daysd.items())]
return ret return ret
def countIdx(self, card): def countIdx(self, card: Card) -> int:
if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN: if card.queue == QUEUE_TYPE_DAY_LEARN_RELEARN:
return 1 return 1
return card.queue return card.queue
def answerButtons(self, card): def answerButtons(self, card: Card) -> int:
if card.odue: if card.odue:
# normal review in dyn deck? # normal review in dyn deck?
if card.odid and card.queue == QUEUE_TYPE_REV: if card.odid and card.queue == QUEUE_TYPE_REV:
@ -154,7 +156,7 @@ order by due"""
else: else:
return 3 return 3
def unburyCards(self): def unburyCards(self) -> None:
"Unbury cards." "Unbury cards."
self.col.conf["lastUnburied"] = self.today self.col.conf["lastUnburied"] = self.today
self.col.log( self.col.log(
@ -166,7 +168,7 @@ order by due"""
f"update cards set queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED}" f"update cards set queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED}"
) )
def unburyCardsForDeck(self): def unburyCardsForDeck(self) -> None:
sids = ids2str(self.col.decks.active()) sids = ids2str(self.col.decks.active())
self.col.log( self.col.log(
self.col.db.list( self.col.db.list(
@ -184,14 +186,14 @@ order by due"""
# Rev/lrn/time daily stats # Rev/lrn/time daily stats
########################################################################## ##########################################################################
def _updateStats(self, card, type, cnt=1): def _updateStats(self, card: Card, type: str, cnt: int = 1) -> None:
key = type + "Today" key = type + "Today"
for g in [self.col.decks.get(card.did)] + self.col.decks.parents(card.did): for g in [self.col.decks.get(card.did)] + self.col.decks.parents(card.did):
# add # add
g[key][1] += cnt g[key][1] += cnt
self.col.decks.save(g) self.col.decks.save(g)
def extendLimits(self, new, rev): def extendLimits(self, new: int, rev: int) -> None:
cur = self.col.decks.current() cur = self.col.decks.current()
parents = self.col.decks.parents(cur["id"]) parents = self.col.decks.parents(cur["id"])
children = [ children = [
@ -204,7 +206,11 @@ order by due"""
g["revToday"][1] -= rev g["revToday"][1] -= rev
self.col.decks.save(g) self.col.decks.save(g)
def _walkingCount(self, limFn=None, cntFn=None): def _walkingCount(
self,
limFn: Optional[Callable[[Any], Optional[int]]] = None,
cntFn: Optional[Callable[[int, int], int]] = None,
) -> int:
tot = 0 tot = 0
pcounts: Dict[int, int] = {} pcounts: Dict[int, int] = {}
# for each of the active decks # for each of the active decks
@ -238,7 +244,7 @@ order by due"""
# Deck list # Deck list
########################################################################## ##########################################################################
def deckDueList(self): def deckDueList(self) -> List[List[Any]]:
"Returns [deckname, did, rev, lrn, new]" "Returns [deckname, did, rev, lrn, new]"
self._checkDay() self._checkDay()
self.col.decks.checkIntegrity() self.col.decks.checkIntegrity()
@ -274,10 +280,10 @@ order by due"""
lims[deck["name"]] = [nlim, rlim] lims[deck["name"]] = [nlim, rlim]
return data return data
def deckDueTree(self): def deckDueTree(self) -> Any:
return self._groupChildren(self.deckDueList()) return self._groupChildren(self.deckDueList())
def _groupChildren(self, grps): def _groupChildren(self, grps: List[List[Any]]) -> Any:
# first, split the group names into components # first, split the group names into components
for g in grps: for g in grps:
g[0] = g[0].split("::") g[0] = g[0].split("::")
@ -286,7 +292,7 @@ order by due"""
# then run main function # then run main function
return self._groupChildrenMain(grps) return self._groupChildrenMain(grps)
def _groupChildrenMain(self, grps): def _groupChildrenMain(self, grps: List[List[Any]]) -> Any:
tree = [] tree = []
# group and recurse # group and recurse
def key(grp): def key(grp):
@ -328,7 +334,7 @@ order by due"""
# Getting the next card # Getting the next card
########################################################################## ##########################################################################
def _getCard(self): def _getCard(self) -> Optional[Card]:
"Return the next due card id, or None." "Return the next due card id, or None."
# learning card due? # learning card due?
c = self._getLrnCard() c = self._getLrnCard()
@ -357,7 +363,7 @@ order by due"""
# New cards # New cards
########################################################################## ##########################################################################
def _resetNewCount(self): def _resetNewCount(self) -> None:
cntFn = lambda did, lim: self.col.db.scalar( cntFn = lambda did, lim: self.col.db.scalar(
f""" f"""
select count() from (select 1 from cards where select count() from (select 1 from cards where
@ -367,13 +373,13 @@ did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
) )
self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn) self.newCount = self._walkingCount(self._deckNewLimitSingle, cntFn)
def _resetNew(self): def _resetNew(self) -> None:
self._resetNewCount() self._resetNewCount()
self._newDids = self.col.decks.active()[:] self._newDids = self.col.decks.active()[:]
self._newQueue = [] self._newQueue: List[Any] = []
self._updateNewCardRatio() self._updateNewCardRatio()
def _fillNew(self): def _fillNew(self) -> Optional[bool]:
if self._newQueue: if self._newQueue:
return True return True
if not self.newCount: if not self.newCount:
@ -400,13 +406,15 @@ did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
# removed from the queue but not buried # removed from the queue but not buried
self._resetNew() self._resetNew()
return self._fillNew() return self._fillNew()
return None
def _getNewCard(self): def _getNewCard(self) -> Optional[Card]:
if self._fillNew(): if self._fillNew():
self.newCount -= 1 self.newCount -= 1
return self.col.getCard(self._newQueue.pop()) return self.col.getCard(self._newQueue.pop())
return None
def _updateNewCardRatio(self): def _updateNewCardRatio(self) -> None:
if self.col.conf["newSpread"] == NEW_CARDS_DISTRIBUTE: if self.col.conf["newSpread"] == NEW_CARDS_DISTRIBUTE:
if self.newCount: if self.newCount:
self.newCardModulus = (self.newCount + self.revCount) // self.newCount self.newCardModulus = (self.newCount + self.revCount) // self.newCount
@ -416,7 +424,7 @@ did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
return return
self.newCardModulus = 0 self.newCardModulus = 0
def _timeForNewCard(self): def _timeForNewCard(self) -> Optional[bool]:
"True if it's time to display a new card when distributing." "True if it's time to display a new card when distributing."
if not self.newCount: if not self.newCount:
return False return False
@ -425,9 +433,12 @@ did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
elif self.col.conf["newSpread"] == NEW_CARDS_FIRST: elif self.col.conf["newSpread"] == NEW_CARDS_FIRST:
return True return True
elif self.newCardModulus: elif self.newCardModulus:
return self.reps and self.reps % self.newCardModulus == 0 return self.reps != 0 and self.reps % self.newCardModulus == 0
return None
def _deckNewLimit(self, did, fn=None): def _deckNewLimit(
self, did: int, fn: Optional[Callable[[Dict[str, Any]], int]] = None
) -> int:
if not fn: if not fn:
fn = self._deckNewLimitSingle fn = self._deckNewLimitSingle
sel = self.col.decks.get(did) sel = self.col.decks.get(did)
@ -441,7 +452,7 @@ did = ? and queue = {QUEUE_TYPE_NEW} limit ?)""",
lim = min(rem, lim) lim = min(rem, lim)
return lim return lim
def _newForDeck(self, did, lim): def _newForDeck(self, did: int, lim: int) -> int:
"New count for a single deck." "New count for a single deck."
if not lim: if not lim:
return 0 return 0
@ -454,14 +465,14 @@ select count() from
lim, lim,
) )
def _deckNewLimitSingle(self, g): def _deckNewLimitSingle(self, g: Dict[str, Any]) -> int:
"Limit for deck without parent limits." "Limit for deck without parent limits."
if g["dyn"]: if g["dyn"]:
return self.reportLimit return self.reportLimit
c = self.col.decks.confForDid(g["id"]) c = self.col.decks.confForDid(g["id"])
return max(0, c["new"]["perDay"] - g["newToday"][1]) return max(0, c["new"]["perDay"] - g["newToday"][1])
def totalNewForCurrentDeck(self): def totalNewForCurrentDeck(self) -> int:
return self.col.db.scalar( return self.col.db.scalar(
f""" f"""
select count() from cards where id in ( select count() from cards where id in (
@ -473,7 +484,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_NEW} limit ?)"""
# Learning queues # Learning queues
########################################################################## ##########################################################################
def _resetLrnCount(self): def _resetLrnCount(self) -> None:
# sub-day # sub-day
self.lrnCount = ( self.lrnCount = (
self.col.db.scalar( self.col.db.scalar(
@ -494,14 +505,14 @@ and due <= ? limit %d"""
self.today, self.today,
) )
def _resetLrn(self): def _resetLrn(self) -> None:
self._resetLrnCount() self._resetLrnCount()
self._lrnQueue = [] self._lrnQueue: List[Any] = []
self._lrnDayQueue = [] self._lrnDayQueue: List[Any] = []
self._lrnDids = self.col.decks.active()[:] self._lrnDids = self.col.decks.active()[:]
# sub-day learning # sub-day learning
def _fillLrn(self): def _fillLrn(self) -> Union[bool, List[Any]]:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnQueue: if self._lrnQueue:
@ -518,7 +529,7 @@ limit %d"""
self._lrnQueue.sort() self._lrnQueue.sort()
return self._lrnQueue return self._lrnQueue
def _getLrnCard(self, collapse=False): def _getLrnCard(self, collapse: bool = False) -> Optional[Card]:
if self._fillLrn(): if self._fillLrn():
cutoff = time.time() cutoff = time.time()
if collapse: if collapse:
@ -528,9 +539,10 @@ limit %d"""
card = self.col.getCard(id) card = self.col.getCard(id)
self.lrnCount -= card.left // 1000 self.lrnCount -= card.left // 1000
return card return card
return None
# daily learning # daily learning
def _fillLrnDay(self): def _fillLrnDay(self) -> Optional[bool]:
if not self.lrnCount: if not self.lrnCount:
return False return False
if self._lrnDayQueue: if self._lrnDayQueue:
@ -557,16 +569,18 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
return True return True
# nothing left in the deck; move to next # nothing left in the deck; move to next
self._lrnDids.pop(0) self._lrnDids.pop(0)
return None
def _getLrnDayCard(self): def _getLrnDayCard(self) -> Optional[Card]:
if self._fillLrnDay(): if self._fillLrnDay():
self.lrnCount -= 1 self.lrnCount -= 1
return self.col.getCard(self._lrnDayQueue.pop()) return self.col.getCard(self._lrnDayQueue.pop())
return None
def _answerLrnCard(self, card, ease): def _answerLrnCard(self, card: Card, ease: int) -> None:
# ease 1=no, 2=yes, 3=remove # ease 1=no, 2=yes, 3=remove
conf = self._lrnConf(card) conf = self._lrnConf(card)
if card.odid and not card.wasNew: if card.odid and not card.wasNew: # type: ignore
type = REVLOG_CRAM type = REVLOG_CRAM
elif card.type == CARD_TYPE_REV: elif card.type == CARD_TYPE_REV:
type = REVLOG_RELRN type = REVLOG_RELRN
@ -625,7 +639,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
self._logLrn(card, ease, conf, leaving, type, lastLeft) self._logLrn(card, ease, conf, leaving, type, lastLeft)
def _delayForGrade(self, conf, left): def _delayForGrade(self, conf: Dict[str, Any], left: int) -> Union[int, float]:
left = left % 1000 left = left % 1000
try: try:
delay = conf["delays"][-left] delay = conf["delays"][-left]
@ -637,13 +651,13 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
delay = 1 delay = 1
return delay * 60 return delay * 60
def _lrnConf(self, card): def _lrnConf(self, card: Card) -> Dict[str, Any]:
if card.type == CARD_TYPE_REV: if card.type == CARD_TYPE_REV:
return self._lapseConf(card) return self._lapseConf(card)
else: else:
return self._newConf(card) return self._newConf(card)
def _rescheduleAsRev(self, card, conf, early): def _rescheduleAsRev(self, card: Card, conf: Dict[str, Any], early: bool) -> None:
lapse = card.type == CARD_TYPE_REV lapse = card.type == CARD_TYPE_REV
if lapse: if lapse:
if self._resched(card): if self._resched(card):
@ -666,7 +680,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
card.queue = card.type = CARD_TYPE_NEW card.queue = card.type = CARD_TYPE_NEW
card.due = self.col.nextID("pos") card.due = self.col.nextID("pos")
def _startingLeft(self, card): def _startingLeft(self, card: Card) -> int:
if card.type == CARD_TYPE_REV: if card.type == CARD_TYPE_REV:
conf = self._lapseConf(card) conf = self._lapseConf(card)
else: else:
@ -675,7 +689,9 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
tod = self._leftToday(conf["delays"], tot) tod = self._leftToday(conf["delays"], tot)
return tot + tod * 1000 return tot + tod * 1000
def _leftToday(self, delays, left, now=None): def _leftToday(
self, delays: List[int], left: int, now: Optional[int] = None
) -> int:
"The number of steps that can be completed by the day cutoff." "The number of steps that can be completed by the day cutoff."
if not now: if not now:
now = intTime() now = intTime()
@ -688,7 +704,9 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
ok = i ok = i
return ok + 1 return ok + 1
def _graduatingIvl(self, card, conf, early, adj=True): def _graduatingIvl(
self, card: Card, conf: Dict[str, Any], early: bool, adj: bool = True
) -> int:
if card.type == CARD_TYPE_REV: if card.type == CARD_TYPE_REV:
# lapsed card being relearnt # lapsed card being relearnt
if card.odid: if card.odid:
@ -706,13 +724,21 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
else: else:
return ideal return ideal
def _rescheduleNew(self, card, conf, early): def _rescheduleNew(self, card: Card, conf: Dict[str, Any], early: bool) -> None:
"Reschedule a new card that's graduated for the first time." "Reschedule a new card that's graduated for the first time."
card.ivl = self._graduatingIvl(card, conf, early) card.ivl = self._graduatingIvl(card, conf, early)
card.due = self.today + card.ivl card.due = self.today + card.ivl
card.factor = conf["initialFactor"] card.factor = conf["initialFactor"]
def _logLrn(self, card, ease, conf, leaving, type, lastLeft): def _logLrn(
self,
card: Card,
ease: int,
conf: Dict[str, Any],
leaving: bool,
type: int,
lastLeft: int,
) -> None:
lastIvl = -(self._delayForGrade(conf, lastLeft)) lastIvl = -(self._delayForGrade(conf, lastLeft))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
@ -737,7 +763,7 @@ did = ? and queue = {QUEUE_TYPE_DAY_LEARN_RELEARN} and due <= ? limit ?""",
time.sleep(0.01) time.sleep(0.01)
log() log()
def removeLrn(self, ids=None): def removeLrn(self, ids: Optional[List[int]] = None) -> None:
"Remove cards from the learning queues." "Remove cards from the learning queues."
if ids: if ids:
extra = " and id in " + ids2str(ids) extra = " and id in " + ids2str(ids)
@ -763,7 +789,7 @@ where queue in ({QUEUE_TYPE_LRN},{QUEUE_TYPE_DAY_LEARN_RELEARN}) and type = {CAR
) )
) )
def _lrnForDeck(self, did): def _lrnForDeck(self, did: int) -> int:
cnt = ( cnt = (
self.col.db.scalar( self.col.db.scalar(
f""" f"""
@ -788,16 +814,16 @@ and due <= ? limit ?)""",
# Reviews # Reviews
########################################################################## ##########################################################################
def _deckRevLimit(self, did): def _deckRevLimit(self, did: int) -> int:
return self._deckNewLimit(did, self._deckRevLimitSingle) return self._deckNewLimit(did, self._deckRevLimitSingle)
def _deckRevLimitSingle(self, d): def _deckRevLimitSingle(self, d: Dict[str, Any]) -> int:
if d["dyn"]: if d["dyn"]:
return self.reportLimit return self.reportLimit
c = self.col.decks.confForDid(d["id"]) c = self.col.decks.confForDid(d["id"])
return max(0, c["rev"]["perDay"] - d["revToday"][1]) return max(0, c["rev"]["perDay"] - d["revToday"][1])
def _revForDeck(self, did, lim): def _revForDeck(self, did: int, lim: int) -> int:
lim = min(lim, self.reportLimit) lim = min(lim, self.reportLimit)
return self.col.db.scalar( return self.col.db.scalar(
f""" f"""
@ -809,7 +835,7 @@ and due <= ? limit ?)""",
lim, lim,
) )
def _resetRevCount(self): def _resetRevCount(self) -> None:
def cntFn(did, lim): def cntFn(did, lim):
return self.col.db.scalar( return self.col.db.scalar(
f""" f"""
@ -822,12 +848,12 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit %d)"""
self.revCount = self._walkingCount(self._deckRevLimitSingle, cntFn) self.revCount = self._walkingCount(self._deckRevLimitSingle, cntFn)
def _resetRev(self): def _resetRev(self) -> None:
self._resetRevCount() self._resetRevCount()
self._revQueue = [] self._revQueue: List[Any] = []
self._revDids = self.col.decks.active()[:] self._revDids = self.col.decks.active()[:]
def _fillRev(self): def _fillRev(self) -> Optional[bool]:
if self._revQueue: if self._revQueue:
return True return True
if not self.revCount: if not self.revCount:
@ -868,12 +894,15 @@ did = ? and queue = {QUEUE_TYPE_REV} and due <= ? limit ?""",
self._resetRev() self._resetRev()
return self._fillRev() return self._fillRev()
def _getRevCard(self): return None
def _getRevCard(self) -> Optional[Card]:
if self._fillRev(): if self._fillRev():
self.revCount -= 1 self.revCount -= 1
return self.col.getCard(self._revQueue.pop()) return self.col.getCard(self._revQueue.pop())
return None
def totalRevForCurrentDeck(self): def totalRevForCurrentDeck(self) -> int:
return self.col.db.scalar( return self.col.db.scalar(
f""" f"""
select count() from cards where id in ( select count() from cards where id in (
@ -886,15 +915,15 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Answering a review card # Answering a review card
########################################################################## ##########################################################################
def _answerRevCard(self, card, ease): def _answerRevCard(self, card: Card, ease: int) -> None:
delay = 0 delay: float = 0
if ease == BUTTON_ONE: if ease == BUTTON_ONE:
delay = self._rescheduleLapse(card) delay = self._rescheduleLapse(card)
else: else:
self._rescheduleRev(card, ease) self._rescheduleRev(card, ease)
self._logRev(card, ease, delay) self._logRev(card, ease, delay)
def _rescheduleLapse(self, card): def _rescheduleLapse(self, card: Card) -> Union[int, float]:
conf = self._lapseConf(card) conf = self._lapseConf(card)
card.lastIvl = card.ivl card.lastIvl = card.ivl
if self._resched(card): if self._resched(card):
@ -906,7 +935,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
if card.odid: if card.odid:
card.odue = card.due card.odue = card.due
# if suspended as a leech, nothing to do # if suspended as a leech, nothing to do
delay = 0 delay: float = 0
if self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED: if self._checkLeech(card, conf) and card.queue == QUEUE_TYPE_SUSPENDED:
return delay return delay
# if no relearning steps, nothing to do # if no relearning steps, nothing to do
@ -930,10 +959,10 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN card.queue = QUEUE_TYPE_DAY_LEARN_RELEARN
return delay return delay
def _nextLapseIvl(self, card, conf): def _nextLapseIvl(self, card: Card, conf: Dict[str, Any]) -> int:
return max(conf["minInt"], int(card.ivl * conf["mult"])) return max(conf["minInt"], int(card.ivl * conf["mult"]))
def _rescheduleRev(self, card, ease): def _rescheduleRev(self, card: Card, ease: int) -> None:
# update interval # update interval
card.lastIvl = card.ivl card.lastIvl = card.ivl
if self._resched(card): if self._resched(card):
@ -948,7 +977,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
card.odid = 0 card.odid = 0
card.odue = 0 card.odue = 0
def _logRev(self, card, ease, delay): def _logRev(self, card: Card, ease: int, delay: Union[int, float]) -> None:
def log(): def log():
self.col.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
@ -973,7 +1002,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Interval management # Interval management
########################################################################## ##########################################################################
def _nextRevIvl(self, card, ease): def _nextRevIvl(self, card: Card, ease: int) -> int:
"Ideal next interval for CARD, given EASE." "Ideal next interval for CARD, given EASE."
delay = self._daysLate(card) delay = self._daysLate(card)
conf = self._revConf(card) conf = self._revConf(card)
@ -992,11 +1021,11 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# interval capped? # interval capped?
return min(interval, conf["maxIvl"]) return min(interval, conf["maxIvl"])
def _fuzzedIvl(self, ivl): def _fuzzedIvl(self, ivl: int) -> int:
min, max = self._fuzzIvlRange(ivl) min, max = self._fuzzIvlRange(ivl)
return random.randint(min, max) return random.randint(min, max)
def _fuzzIvlRange(self, ivl): def _fuzzIvlRange(self, ivl: int) -> List[int]:
if ivl < 2: if ivl < 2:
return [1, 1] return [1, 1]
elif ivl == 2: elif ivl == 2:
@ -1011,24 +1040,24 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
fuzz = max(fuzz, 1) fuzz = max(fuzz, 1)
return [ivl - fuzz, ivl + fuzz] return [ivl - fuzz, ivl + fuzz]
def _constrainedIvl(self, ivl, conf, prev): def _constrainedIvl(self, ivl: float, conf: Dict[str, Any], prev: int) -> int:
"Integer interval after interval factor and prev+1 constraints applied." "Integer interval after interval factor and prev+1 constraints applied."
new = ivl * conf.get("ivlFct", 1) new = ivl * conf.get("ivlFct", 1)
return int(max(new, prev + 1)) return int(max(new, prev + 1))
def _daysLate(self, card): def _daysLate(self, card: Card) -> int:
"Number of days later than scheduled." "Number of days later than scheduled."
due = card.odue if card.odid else card.due due = card.odue if card.odid else card.due
return max(0, self.today - due) return max(0, self.today - due)
def _updateRevIvl(self, card, ease): def _updateRevIvl(self, card: Card, ease: int) -> None:
idealIvl = self._nextRevIvl(card, ease) idealIvl = self._nextRevIvl(card, ease)
card.ivl = min( card.ivl = min(
max(self._adjRevIvl(card, idealIvl), card.ivl + 1), max(self._adjRevIvl(card, idealIvl), card.ivl + 1),
self._revConf(card)["maxIvl"], self._revConf(card)["maxIvl"],
) )
def _adjRevIvl(self, card, idealIvl): def _adjRevIvl(self, card: Card, idealIvl: int) -> int:
if self._spreadRev: if self._spreadRev:
idealIvl = self._fuzzedIvl(idealIvl) idealIvl = self._fuzzedIvl(idealIvl)
return idealIvl return idealIvl
@ -1036,7 +1065,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Dynamic deck handling # Dynamic deck handling
########################################################################## ##########################################################################
def rebuildDyn(self, did=None): def rebuildDyn(self, did: Optional[int] = None) -> Optional[List[int]]:
"Rebuild a dynamic deck." "Rebuild a dynamic deck."
did = did or self.col.decks.selected() did = did or self.col.decks.selected()
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
@ -1045,12 +1074,12 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self.emptyDyn(did) self.emptyDyn(did)
ids = self._fillDyn(deck) ids = self._fillDyn(deck)
if not ids: if not ids:
return return None
# and change to our new deck # and change to our new deck
self.col.decks.select(did) self.col.decks.select(did)
return ids return ids
def _fillDyn(self, deck): def _fillDyn(self, deck: Dict[str, Any]) -> List[int]:
search, limit, order = deck["terms"][0] search, limit, order = deck["terms"][0]
orderlimit = self._dynOrder(order, limit) orderlimit = self._dynOrder(order, limit)
if search.strip(): if search.strip():
@ -1066,7 +1095,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self._moveToDyn(deck["id"], ids) self._moveToDyn(deck["id"], ids)
return ids return ids
def emptyDyn(self, did, lim=None): def emptyDyn(self, did: Optional[int], lim: Optional[str] = None) -> None:
if not lim: if not lim:
lim = "did = %s" % did lim = "did = %s" % did
self.col.log(self.col.db.list("select id from cards where %s" % lim)) self.col.log(self.col.db.list("select id from cards where %s" % lim))
@ -1080,10 +1109,10 @@ due = odue, odue = 0, odid = 0, usn = ? where %s"""
self.col.usn(), self.col.usn(),
) )
def remFromDyn(self, cids): def remFromDyn(self, cids: List[int]) -> None:
self.emptyDyn(None, "id in %s and odid" % ids2str(cids)) self.emptyDyn(None, "id in %s and odid" % ids2str(cids))
def _dynOrder(self, o, l): def _dynOrder(self, o: int, l: int) -> str:
if o == DYN_OLDEST: if o == DYN_OLDEST:
t = "(select max(id) from revlog where cid=c.id)" t = "(select max(id) from revlog where cid=c.id)"
elif o == DYN_RANDOM: elif o == DYN_RANDOM:
@ -1110,7 +1139,7 @@ due = odue, odue = 0, odid = 0, usn = ? where %s"""
t = "c.due" t = "c.due"
return t + " limit %d" % l return t + " limit %d" % l
def _moveToDyn(self, did, ids): def _moveToDyn(self, did: int, ids: List[int]) -> None:
deck = self.col.decks.get(did) deck = self.col.decks.get(did)
data = [] data = []
t = intTime() t = intTime()
@ -1134,7 +1163,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
data, data,
) )
def _dynIvlBoost(self, card): def _dynIvlBoost(self, card: Card) -> int:
assert card.odid and card.type == CARD_TYPE_REV assert card.odid and card.type == CARD_TYPE_REV
assert card.factor assert card.factor
elapsed = card.ivl - (card.odue - self.today) elapsed = card.ivl - (card.odue - self.today)
@ -1146,7 +1175,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
# Leeches # Leeches
########################################################################## ##########################################################################
def _checkLeech(self, card, conf) -> bool: def _checkLeech(self, card: Card, conf: Dict[str, Any]) -> bool:
"Leech handler. True if card was a leech." "Leech handler. True if card was a leech."
lf = conf["leechFails"] lf = conf["leechFails"]
if not lf: if not lf:
@ -1176,10 +1205,10 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
# Tools # Tools
########################################################################## ##########################################################################
def _cardConf(self, card): def _cardConf(self, card: Card) -> Dict[str, Any]:
return self.col.decks.confForDid(card.did) return self.col.decks.confForDid(card.did)
def _newConf(self, card): def _newConf(self, card: Card) -> Dict[str, Any]:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1199,7 +1228,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
perDay=self.reportLimit, perDay=self.reportLimit,
) )
def _lapseConf(self, card): def _lapseConf(self, card: Card) -> Dict[str, Any]:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1218,7 +1247,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
resched=conf["resched"], resched=conf["resched"],
) )
def _revConf(self, card): def _revConf(self, card: Card) -> Dict[str, Any]:
conf = self._cardConf(card) conf = self._cardConf(card)
# normal deck # normal deck
if not card.odid: if not card.odid:
@ -1226,10 +1255,10 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
# dynamic deck # dynamic deck
return self.col.decks.confForDid(card.odid)["rev"] return self.col.decks.confForDid(card.odid)["rev"]
def _deckLimit(self): def _deckLimit(self) -> str:
return ids2str(self.col.decks.active()) return ids2str(self.col.decks.active())
def _resched(self, card): def _resched(self, card: Card) -> bool:
conf = self._cardConf(card) conf = self._cardConf(card)
if not conf["dyn"]: if not conf["dyn"]:
return True return True
@ -1238,7 +1267,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
# Daily cutoff # Daily cutoff
########################################################################## ##########################################################################
def _updateCutoff(self): def _updateCutoff(self) -> None:
oldToday = self.today oldToday = self.today
# days since col created # days since col created
self.today = int((time.time() - self.col.crt) // 86400) self.today = int((time.time() - self.col.crt) // 86400)
@ -1261,7 +1290,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
if unburied < self.today: if unburied < self.today:
self.unburyCards() self.unburyCards()
def _checkDay(self): def _checkDay(self) -> None:
# check if the day has rolled over # check if the day has rolled over
if time.time() > self.dayCutoff: if time.time() > self.dayCutoff:
self.reset() self.reset()
@ -1269,7 +1298,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
# Deck finished state # Deck finished state
########################################################################## ##########################################################################
def finishedMsg(self): def finishedMsg(self) -> str:
return ( return (
"<b>" "<b>"
+ _("Congratulations! You have finished this deck for now.") + _("Congratulations! You have finished this deck for now.")
@ -1277,7 +1306,7 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?"""
+ self._nextDueMsg() + self._nextDueMsg()
) )
def _nextDueMsg(self): def _nextDueMsg(self) -> str:
line = [] line = []
# the new line replacements are so we don't break translations # the new line replacements are so we don't break translations
# in a point release # in a point release
@ -1321,7 +1350,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
) )
return "<p>".join(line) return "<p>".join(line)
def revDue(self): def revDue(self) -> Optional[int]:
"True if there are any rev cards due." "True if there are any rev cards due."
return self.col.db.scalar( return self.col.db.scalar(
( (
@ -1332,7 +1361,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
self.today, self.today,
) )
def newDue(self): def newDue(self) -> Optional[int]:
"True if there are any new cards due." "True if there are any new cards due."
return self.col.db.scalar( return self.col.db.scalar(
( (
@ -1342,7 +1371,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
% self._deckLimit() % self._deckLimit()
) )
def haveBuried(self): def haveBuried(self) -> bool:
sdids = ids2str(self.col.decks.active()) sdids = ids2str(self.col.decks.active())
cnt = self.col.db.scalar( cnt = self.col.db.scalar(
f"select 1 from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s limit 1" f"select 1 from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s limit 1"
@ -1353,7 +1382,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# Next time reports # Next time reports
########################################################################## ##########################################################################
def nextIvlStr(self, card, ease, short=False): def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str:
"Return the next interval for CARD as a string." "Return the next interval for CARD as a string."
ivl_secs = self.nextIvl(card, ease) ivl_secs = self.nextIvl(card, ease)
if not ivl_secs: if not ivl_secs:
@ -1365,7 +1394,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
s = "<" + s s = "<" + s
return s return s
def nextIvl(self, card, ease): def nextIvl(self, card: Card, ease: int) -> float:
"Return the next interval for CARD, in seconds." "Return the next interval for CARD, in seconds."
if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN): if card.queue in (QUEUE_TYPE_NEW, QUEUE_TYPE_LRN, QUEUE_TYPE_DAY_LEARN_RELEARN):
return self._nextLrnIvl(card, ease) return self._nextLrnIvl(card, ease)
@ -1380,7 +1409,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
return self._nextRevIvl(card, ease) * 86400 return self._nextRevIvl(card, ease) * 86400
# this isn't easily extracted from the learn code # this isn't easily extracted from the learn code
def _nextLrnIvl(self, card, ease): def _nextLrnIvl(self, card: Card, ease: int) -> Union[int, float]:
if card.queue == 0: if card.queue == 0:
card.left = self._startingLeft(card) card.left = self._startingLeft(card)
conf = self._lrnConf(card) conf = self._lrnConf(card)
@ -1405,7 +1434,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
# Suspending # Suspending
########################################################################## ##########################################################################
def suspendCards(self, ids): def suspendCards(self, ids: List[int]) -> None:
"Suspend cards." "Suspend cards."
self.col.log(ids) self.col.log(ids)
self.remFromDyn(ids) self.remFromDyn(ids)
@ -1417,7 +1446,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
self.col.usn(), self.col.usn(),
) )
def unsuspendCards(self, ids): def unsuspendCards(self, ids: List[int]) -> None:
"Unsuspend cards." "Unsuspend cards."
self.col.log(ids) self.col.log(ids)
self.col.db.execute( self.col.db.execute(
@ -1427,7 +1456,7 @@ To study outside of the normal schedule, click the Custom Study button below."""
self.col.usn(), self.col.usn(),
) )
def buryCards(self, cids): def buryCards(self, cids: List[int]) -> None:
self.col.log(cids) self.col.log(cids)
self.remFromDyn(cids) self.remFromDyn(cids)
self.removeLrn(cids) self.removeLrn(cids)
@ -1439,7 +1468,7 @@ update cards set queue={QUEUE_TYPE_SIBLING_BURIED},mod=?,usn=? where id in """
self.col.usn(), self.col.usn(),
) )
def buryNote(self, nid): def buryNote(self, nid: int) -> None:
"Bury all cards for note until next session." "Bury all cards for note until next session."
cids = self.col.db.list( cids = self.col.db.list(
"select id from cards where nid = ? and queue >= 0", nid "select id from cards where nid = ? and queue >= 0", nid
@ -1449,7 +1478,7 @@ update cards set queue={QUEUE_TYPE_SIBLING_BURIED},mod=?,usn=? where id in """
# Sibling spacing # Sibling spacing
########################################################################## ##########################################################################
def _burySiblings(self, card): def _burySiblings(self, card: Card) -> None:
toBury = [] toBury = []
nconf = self._newConf(card) nconf = self._newConf(card)
buryNew = nconf.get("bury", True) buryNew = nconf.get("bury", True)
@ -1493,7 +1522,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
# Resetting # Resetting
########################################################################## ##########################################################################
def forgetCards(self, ids): def forgetCards(self, ids: List[int]) -> None:
"Put cards at the end of the new queue." "Put cards at the end of the new queue."
self.remFromDyn(ids) self.remFromDyn(ids)
self.col.db.execute( self.col.db.execute(
@ -1509,7 +1538,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
self.sortCards(ids, start=pmax + 1) self.sortCards(ids, start=pmax + 1)
self.col.log(ids) self.col.log(ids)
def reschedCards(self, ids, imin, imax): def reschedCards(self, ids: List[int], imin: int, imax: int) -> None:
"Put cards in review queue with a new interval in days (min, max)." "Put cards in review queue with a new interval in days (min, max)."
d = [] d = []
t = self.today t = self.today
@ -1535,7 +1564,7 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
) )
self.col.log(ids) self.col.log(ids)
def resetCards(self, ids): def resetCards(self, ids: List[int]) -> None:
"Completely reset cards for export." "Completely reset cards for export."
sids = ids2str(ids) sids = ids2str(ids)
# we want to avoid resetting due number of existing new cards on export # we want to avoid resetting due number of existing new cards on export
@ -1555,7 +1584,14 @@ usn=:usn,mod=:mod,factor=:fact where id=:id""",
# Repositioning new cards # Repositioning new cards
########################################################################## ##########################################################################
def sortCards(self, cids, start=1, step=1, shuffle=False, shift=False): def sortCards(
self,
cids: List[int],
start: int = 1,
step: int = 1,
shuffle: bool = False,
shift: bool = False,
) -> None:
scids = ids2str(cids) scids = ids2str(cids)
now = intTime() now = intTime()
nids = [] nids = []
@ -1605,15 +1641,15 @@ and due >= ? and queue = {QUEUE_TYPE_NEW}"""
"update cards set due=:due,mod=:now,usn=:usn where id = :cid", d "update cards set due=:due,mod=:now,usn=:usn where id = :cid", d
) )
def randomizeCards(self, did): def randomizeCards(self, did: int) -> None:
cids = self.col.db.list("select id from cards where did = ?", did) cids = self.col.db.list("select id from cards where did = ?", did)
self.sortCards(cids, shuffle=True) self.sortCards(cids, shuffle=True)
def orderCards(self, did): def orderCards(self, did: int) -> None:
cids = self.col.db.list("select id from cards where did = ? order by nid", did) cids = self.col.db.list("select id from cards where did = ? order by nid", did)
self.sortCards(cids) self.sortCards(cids)
def resortConf(self, conf): def resortConf(self, conf) -> None:
for did in self.col.decks.didsForConf(conf): for did in self.col.decks.didsForConf(conf):
if conf["new"]["order"] == 0: if conf["new"]["order"] == 0:
self.randomizeCards(did) self.randomizeCards(did)
@ -1621,7 +1657,7 @@ and due >= ? and queue = {QUEUE_TYPE_NEW}"""
self.orderCards(did) self.orderCards(did)
# for post-import # for post-import
def maybeRandomizeDeck(self, did=None): def maybeRandomizeDeck(self, did: Optional[int] = None) -> None:
if not did: if not did:
did = self.col.decks.selected() did = self.col.decks.selected()
conf = self.col.decks.confForDid(did) conf = self.col.decks.confForDid(did)