fsrs

py-fsrs

Py-FSRS is the official Python implementation of the FSRS scheduler algorithm, which can be used to develop spaced repetition systems.

 1"""
 2py-fsrs
 3-------
 4
 5Py-FSRS is the official Python implementation of the FSRS scheduler algorithm, which can be used to develop spaced repetition systems.
 6"""
 7
 8from fsrs.scheduler import Scheduler
 9from fsrs.state import State
10from fsrs.card import Card
11from fsrs.rating import Rating
12from fsrs.review_log import ReviewLog
13from typing import TYPE_CHECKING
14
15if TYPE_CHECKING:
16    from fsrs.optimizer import Optimizer
17
18
19# lazy load the Optimizer module due to heavy dependencies
20def __getattr__(name: str) -> type:
21    if name == "Optimizer":
22        global Optimizer
23        from fsrs.optimizer import Optimizer
24
25        return Optimizer
26    raise AttributeError
27
28
29__all__ = ["Scheduler", "Card", "Rating", "ReviewLog", "State", "Optimizer"]
@dataclass(init=False)
class Scheduler:
138@dataclass(init=False)
139class Scheduler:
140    """
141    The FSRS scheduler.
142
143    Enables the reviewing and future scheduling of cards according to the FSRS algorithm.
144
145    Attributes:
146        parameters: The model weights of the FSRS scheduler.
147        desired_retention: The desired retention rate of cards scheduled with the scheduler.
148        learning_steps: Small time intervals that schedule cards in the Learning state.
149        relearning_steps: Small time intervals that schedule cards in the Relearning state.
150        maximum_interval: The maximum number of days a Review-state card can be scheduled into the future.
151        enable_fuzzing: Whether to apply a small amount of random 'fuzz' to calculated intervals.
152    """
153
154    parameters: tuple[float, ...]
155    desired_retention: float
156    learning_steps: tuple[timedelta, ...]
157    relearning_steps: tuple[timedelta, ...]
158    maximum_interval: int
159    enable_fuzzing: bool
160
161    def __init__(
162        self,
163        parameters: Sequence[float] = DEFAULT_PARAMETERS,
164        desired_retention: float = 0.9,
165        learning_steps: tuple[timedelta, ...] | list[timedelta] = (
166            timedelta(minutes=1),
167            timedelta(minutes=10),
168        ),
169        relearning_steps: tuple[timedelta, ...] | list[timedelta] = (
170            timedelta(minutes=10),
171        ),
172        maximum_interval: int = 36500,
173        enable_fuzzing: bool = True,
174    ) -> None:
175        self._validate_parameters(parameters=parameters)
176
177        self.parameters = tuple(parameters)
178        self.desired_retention = desired_retention
179        self.learning_steps = tuple(learning_steps)
180        self.relearning_steps = tuple(relearning_steps)
181        self.maximum_interval = maximum_interval
182        self.enable_fuzzing = enable_fuzzing
183
184        self._DECAY = -self.parameters[20]
185        self._FACTOR = 0.9 ** (1 / self._DECAY) - 1
186
187    def _validate_parameters(self, *, parameters: Sequence[float]) -> None:
188        if len(parameters) != len(LOWER_BOUNDS_PARAMETERS):
189            raise ValueError(
190                f"Expected {len(LOWER_BOUNDS_PARAMETERS)} parameters, got {len(parameters)}."
191            )
192
193        error_messages = []
194        for index, (parameter, lower_bound, upper_bound) in enumerate(
195            zip(parameters, LOWER_BOUNDS_PARAMETERS, UPPER_BOUNDS_PARAMETERS)
196        ):
197            if not lower_bound <= parameter <= upper_bound:
198                error_message = f"parameters[{index}] = {parameter} is out of bounds: ({lower_bound}, {upper_bound})"
199                error_messages.append(error_message)
200
201        if len(error_messages) > 0:
202            raise ValueError(
203                "One or more parameters are out of bounds:\n"
204                + "\n".join(error_messages)
205            )
206
207    def get_card_retrievability(
208        self, card: Card, current_datetime: datetime | None = None
209    ) -> float:
210        """
211        Calculates a Card object's current retrievability for a given date and time.
212
213        The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime.
214
215        Args:
216            card: The card whose retrievability is to be calculated
217            current_datetime: The current date and time
218
219        Returns:
220            float: The retrievability of the Card object.
221        """
222
223        if card.last_review is None:
224            return 0
225
226        if current_datetime is None:
227            current_datetime = datetime.now(timezone.utc)
228
229        elapsed_days = max(0, (current_datetime - card.last_review).days)
230
231        return (1 + self._FACTOR * elapsed_days / card.stability) ** self._DECAY
232
233    def review_card(
234        self,
235        card: Card,
236        rating: Rating,
237        review_datetime: datetime | None = None,
238        review_duration: int | None = None,
239    ) -> tuple[Card, ReviewLog]:
240        """
241        Reviews a card with a given rating at a given time for a specified duration.
242
243        Args:
244            card: The card being reviewed.
245            rating: The chosen rating for the card being reviewed.
246            review_datetime: The date and time of the review.
247            review_duration: The number of miliseconds it took to review the card or None if unspecified.
248
249        Returns:
250            tuple[Card,ReviewLog]: A tuple containing the updated, reviewed card and its corresponding review log.
251
252        Raises:
253            ValueError: If the `review_datetime` argument is not timezone-aware and set to UTC.
254        """
255
256        if review_datetime is not None and (
257            (review_datetime.tzinfo is None) or (review_datetime.tzinfo != timezone.utc)
258        ):
259            raise ValueError("datetime must be timezone-aware and set to UTC")
260
261        card = copy(card)
262
263        if review_datetime is None:
264            review_datetime = datetime.now(timezone.utc)
265
266        days_since_last_review = (
267            (review_datetime - card.last_review).days if card.last_review else None
268        )
269
270        match card.state:
271            case State.Learning:
272                assert card.step is not None
273
274                # update the card's stability and difficulty
275                if card.stability is None or card.difficulty is None:
276                    card.stability = self._initial_stability(rating=rating)
277                    card.difficulty = self._initial_difficulty(
278                        rating=rating, clamp=True
279                    )
280
281                elif days_since_last_review is not None and days_since_last_review < 1:
282                    card.stability = self._short_term_stability(
283                        stability=card.stability, rating=rating
284                    )
285                    card.difficulty = self._next_difficulty(
286                        difficulty=card.difficulty, rating=rating
287                    )
288
289                else:
290                    card.stability = self._next_stability(
291                        difficulty=card.difficulty,
292                        stability=card.stability,
293                        retrievability=self.get_card_retrievability(
294                            card,
295                            current_datetime=review_datetime,
296                        ),
297                        rating=rating,
298                    )
299                    card.difficulty = self._next_difficulty(
300                        difficulty=card.difficulty, rating=rating
301                    )
302
303                # calculate the card's next interval
304                ## first if-clause handles edge case where the Card in the Learning state was previously
305                ## scheduled with a Scheduler with more learning_steps than the current Scheduler
306                if len(self.learning_steps) == 0 or (
307                    card.step >= len(self.learning_steps)
308                    and rating in (Rating.Hard, Rating.Good, Rating.Easy)
309                ):
310                    card.state = State.Review
311                    card.step = None
312
313                    next_interval_days = self._next_interval(stability=card.stability)
314                    next_interval = timedelta(days=next_interval_days)
315
316                else:
317                    match rating:
318                        case Rating.Again:
319                            card.step = 0
320                            next_interval = self.learning_steps[card.step]
321
322                        case Rating.Hard:
323                            # card step stays the same
324
325                            if card.step == 0 and len(self.learning_steps) == 1:
326                                next_interval = self.learning_steps[0] * 1.5
327                            elif card.step == 0 and len(self.learning_steps) >= 2:
328                                next_interval = (
329                                    self.learning_steps[0] + self.learning_steps[1]
330                                ) / 2.0
331                            else:
332                                next_interval = self.learning_steps[card.step]
333
334                        case Rating.Good:
335                            if card.step + 1 == len(
336                                self.learning_steps
337                            ):  # the last step
338                                card.state = State.Review
339                                card.step = None
340
341                                next_interval_days = self._next_interval(
342                                    stability=card.stability
343                                )
344                                next_interval = timedelta(days=next_interval_days)
345
346                            else:
347                                card.step += 1
348                                next_interval = self.learning_steps[card.step]
349
350                        case Rating.Easy:
351                            card.state = State.Review
352                            card.step = None
353
354                            next_interval_days = self._next_interval(
355                                stability=card.stability
356                            )
357                            next_interval = timedelta(days=next_interval_days)
358
359            case State.Review:
360                assert card.stability is not None
361                assert card.difficulty is not None
362
363                # update the card's stability and difficulty
364                if days_since_last_review is not None and days_since_last_review < 1:
365                    card.stability = self._short_term_stability(
366                        stability=card.stability, rating=rating
367                    )
368                else:
369                    card.stability = self._next_stability(
370                        difficulty=card.difficulty,
371                        stability=card.stability,
372                        retrievability=self.get_card_retrievability(
373                            card,
374                            current_datetime=review_datetime,
375                        ),
376                        rating=rating,
377                    )
378
379                card.difficulty = self._next_difficulty(
380                    difficulty=card.difficulty, rating=rating
381                )
382
383                # calculate the card's next interval
384                match rating:
385                    case Rating.Again:
386                        # if there are no relearning steps (they were left blank)
387                        if len(self.relearning_steps) == 0:
388                            next_interval_days = self._next_interval(
389                                stability=card.stability
390                            )
391                            next_interval = timedelta(days=next_interval_days)
392
393                        else:
394                            card.state = State.Relearning
395                            card.step = 0
396
397                            next_interval = self.relearning_steps[card.step]
398
399                    case Rating.Hard | Rating.Good | Rating.Easy:
400                        next_interval_days = self._next_interval(
401                            stability=card.stability
402                        )
403                        next_interval = timedelta(days=next_interval_days)
404
405            case State.Relearning:
406                assert card.stability is not None
407                assert card.difficulty is not None
408                assert card.step is not None
409
410                # update the card's stability and difficulty
411                if days_since_last_review is not None and days_since_last_review < 1:
412                    card.stability = self._short_term_stability(
413                        stability=card.stability, rating=rating
414                    )
415                    card.difficulty = self._next_difficulty(
416                        difficulty=card.difficulty, rating=rating
417                    )
418
419                else:
420                    card.stability = self._next_stability(
421                        difficulty=card.difficulty,
422                        stability=card.stability,
423                        retrievability=self.get_card_retrievability(
424                            card,
425                            current_datetime=review_datetime,
426                        ),
427                        rating=rating,
428                    )
429                    card.difficulty = self._next_difficulty(
430                        difficulty=card.difficulty, rating=rating
431                    )
432
433                # calculate the card's next interval
434                ## first if-clause handles edge case where the Card in the Relearning state was previously
435                ## scheduled with a Scheduler with more relearning_steps than the current Scheduler
436                if len(self.relearning_steps) == 0 or (
437                    card.step >= len(self.relearning_steps)
438                    and rating in (Rating.Hard, Rating.Good, Rating.Easy)
439                ):
440                    card.state = State.Review
441                    card.step = None
442
443                    next_interval_days = self._next_interval(stability=card.stability)
444                    next_interval = timedelta(days=next_interval_days)
445
446                else:
447                    match rating:
448                        case Rating.Again:
449                            card.step = 0
450                            next_interval = self.relearning_steps[card.step]
451
452                        case Rating.Hard:
453                            # card step stays the same
454
455                            if card.step == 0 and len(self.relearning_steps) == 1:
456                                next_interval = self.relearning_steps[0] * 1.5
457                            elif card.step == 0 and len(self.relearning_steps) >= 2:
458                                next_interval = (
459                                    self.relearning_steps[0] + self.relearning_steps[1]
460                                ) / 2.0
461                            else:
462                                next_interval = self.relearning_steps[card.step]
463
464                        case Rating.Good:
465                            if card.step + 1 == len(
466                                self.relearning_steps
467                            ):  # the last step
468                                card.state = State.Review
469                                card.step = None
470
471                                next_interval_days = self._next_interval(
472                                    stability=card.stability
473                                )
474                                next_interval = timedelta(days=next_interval_days)
475
476                            else:
477                                card.step += 1
478                                next_interval = self.relearning_steps[card.step]
479
480                        case Rating.Easy:
481                            card.state = State.Review
482                            card.step = None
483
484                            next_interval_days = self._next_interval(
485                                stability=card.stability
486                            )
487                            next_interval = timedelta(days=next_interval_days)
488
489        if self.enable_fuzzing and card.state == State.Review:
490            next_interval = self._get_fuzzed_interval(interval=next_interval)
491
492        card.due = review_datetime + next_interval
493        card.last_review = review_datetime
494
495        review_log = ReviewLog(
496            card_id=card.card_id,
497            rating=rating,
498            review_datetime=review_datetime,
499            review_duration=review_duration,
500        )
501
502        return card, review_log
503
504    def reschedule_card(self, card: Card, review_logs: list[ReviewLog]) -> Card:
505        """
506        Reschedules/updates the given card with the current scheduler provided that card's review logs.
507
508        If the current card was previously scheduled with a different scheduler, you may want to reschedule/update
509        it as if it had always been scheduled with this current scheduler. For example, you may want to reschedule
510        each of your cards with a new scheduler after computing the optimal parameters with the Optimizer.
511
512        Args:
513            card: The card to be rescheduled/updated.
514            review_logs: A list of that card's review logs (order doesn't matter).
515
516        Returns:
517            Card: A new card that has been rescheduled/updated with this current scheduler.
518
519        Raises:
520            ValueError: If any of the review logs are for a card other than the one specified, this will raise an error.
521
522        """
523
524        for review_log in review_logs:
525            if review_log.card_id != card.card_id:
526                raise ValueError(
527                    f"ReviewLog card_id {review_log.card_id} does not match Card card_id {card.card_id}"
528                )
529
530        review_logs = sorted(review_logs, key=lambda log: log.review_datetime)
531
532        rescheduled_card = Card(card_id=card.card_id, due=card.due)
533
534        for review_log in review_logs:
535            rescheduled_card, _ = self.review_card(
536                card=rescheduled_card,
537                rating=review_log.rating,
538                review_datetime=review_log.review_datetime,
539            )
540
541        return rescheduled_card
542
543    def to_dict(
544        self,
545    ) -> SchedulerDict:
546        """
547        Returns a dictionary representation of the Scheduler object.
548
549        Returns:
550            SchedulerDict: A dictionary representation of the Scheduler object.
551        """
552
553        return {
554            "parameters": list(self.parameters),
555            "desired_retention": self.desired_retention,
556            "learning_steps": [
557                int(learning_step.total_seconds())
558                for learning_step in self.learning_steps
559            ],
560            "relearning_steps": [
561                int(relearning_step.total_seconds())
562                for relearning_step in self.relearning_steps
563            ],
564            "maximum_interval": self.maximum_interval,
565            "enable_fuzzing": self.enable_fuzzing,
566        }
567
568    @classmethod
569    def from_dict(cls, source_dict: SchedulerDict) -> Self:
570        """
571        Creates a Scheduler object from an existing dictionary.
572
573        Args:
574            source_dict: A dictionary representing an existing Scheduler object.
575
576        Returns:
577            Self: A Scheduler object created from the provided dictionary.
578        """
579
580        return cls(
581            parameters=source_dict["parameters"],
582            desired_retention=source_dict["desired_retention"],
583            learning_steps=[
584                timedelta(seconds=learning_step)
585                for learning_step in source_dict["learning_steps"]
586            ],
587            relearning_steps=[
588                timedelta(seconds=relearning_step)
589                for relearning_step in source_dict["relearning_steps"]
590            ],
591            maximum_interval=source_dict["maximum_interval"],
592            enable_fuzzing=source_dict["enable_fuzzing"],
593        )
594
595    def to_json(self, indent: int | str | None = None) -> str:
596        """
597        Returns a JSON-serialized string of the Scheduler object.
598
599        Args:
600            indent: Equivalent argument to the indent in json.dumps()
601
602        Returns:
603            str: A JSON-serialized string of the Scheduler object.
604        """
605
606        return json.dumps(self.to_dict(), indent=indent)
607
608    @classmethod
609    def from_json(cls, source_json: str) -> Self:
610        """
611        Creates a Scheduler object from a JSON-serialized string.
612
613        Args:
614            source_json: A JSON-serialized string of an existing Scheduler object.
615
616        Returns:
617            Self: A Scheduler object created from the JSON string.
618        """
619
620        source_dict: SchedulerDict = json.loads(source_json)
621        return cls.from_dict(source_dict=source_dict)
622
623    def _clamp_difficulty(self, *, difficulty: float) -> float:
624        if isinstance(difficulty, Real):
625            difficulty = min(max(difficulty, MIN_DIFFICULTY), MAX_DIFFICULTY)
626        else:  # type(difficulty) is torch.Tensor
627            difficulty = difficulty.clamp(min=MIN_DIFFICULTY, max=MAX_DIFFICULTY)  # type: ignore[attr-defined]
628
629        return difficulty
630
631    def _clamp_stability(self, *, stability: float) -> float:
632        if isinstance(stability, Real):
633            stability = max(stability, STABILITY_MIN)
634        else:  # type(stability) is torch.Tensor
635            stability = stability.clamp(min=STABILITY_MIN)  # type: ignore[attr-defined]
636
637        return stability
638
639    def _initial_stability(self, *, rating: Rating) -> float:
640        initial_stability = self.parameters[rating - 1]
641
642        initial_stability = self._clamp_stability(stability=initial_stability)
643
644        return initial_stability
645
646    def _initial_difficulty(self, *, rating: Rating, clamp: bool) -> float:
647        initial_difficulty = (
648            self.parameters[4] - (math.e ** (self.parameters[5] * (rating - 1))) + 1
649        )
650
651        if clamp:
652            initial_difficulty = self._clamp_difficulty(difficulty=initial_difficulty)
653
654        return initial_difficulty
655
656    def _next_interval(self, *, stability: float) -> int:
657        next_interval = (stability / self._FACTOR) * (
658            (self.desired_retention ** (1 / self._DECAY)) - 1
659        )
660
661        if not isinstance(next_interval, Real):  # type(next_interval) is torch.Tensor
662            next_interval = next_interval.detach().item()
663
664        next_interval = round(next_interval)  # intervals are full days
665
666        # must be at least 1 day long
667        next_interval = max(next_interval, 1)
668
669        # can not be longer than the maximum interval
670        next_interval = min(next_interval, self.maximum_interval)
671
672        return next_interval
673
674    def _short_term_stability(self, *, stability: float, rating: Rating) -> float:
675        short_term_stability_increase = (
676            math.e ** (self.parameters[17] * (rating - 3 + self.parameters[18]))
677        ) * (stability ** -self.parameters[19])
678
679        if rating in (Rating.Good, Rating.Easy):
680            if isinstance(short_term_stability_increase, Real):
681                short_term_stability_increase = max(short_term_stability_increase, 1.0)
682            else:  # type(short_term_stability_increase) is torch.Tensor
683                short_term_stability_increase = short_term_stability_increase.clamp(
684                    min=1.0
685                )
686
687        short_term_stability = stability * short_term_stability_increase
688
689        short_term_stability = self._clamp_stability(stability=short_term_stability)
690
691        return short_term_stability
692
693    def _next_difficulty(self, *, difficulty: float, rating: Rating) -> float:
694        def _linear_damping(*, delta_difficulty: float, difficulty: float) -> float:
695            return (10.0 - difficulty) * delta_difficulty / 9.0
696
697        def _mean_reversion(*, arg_1: float, arg_2: float) -> float:
698            return self.parameters[7] * arg_1 + (1 - self.parameters[7]) * arg_2
699
700        arg_1 = self._initial_difficulty(rating=Rating.Easy, clamp=False)
701
702        delta_difficulty = -(self.parameters[6] * (rating - 3))
703        arg_2 = difficulty + _linear_damping(
704            delta_difficulty=delta_difficulty, difficulty=difficulty
705        )
706
707        next_difficulty = _mean_reversion(arg_1=arg_1, arg_2=arg_2)
708
709        next_difficulty = self._clamp_difficulty(difficulty=next_difficulty)
710
711        return next_difficulty
712
713    def _next_stability(
714        self,
715        *,
716        difficulty: float,
717        stability: float,
718        retrievability: float,
719        rating: Rating,
720    ) -> float:
721        if rating == Rating.Again:
722            next_stability = self._next_forget_stability(
723                difficulty=difficulty,
724                stability=stability,
725                retrievability=retrievability,
726            )
727
728        elif rating in (Rating.Hard, Rating.Good, Rating.Easy):
729            next_stability = self._next_recall_stability(
730                difficulty=difficulty,
731                stability=stability,
732                retrievability=retrievability,
733                rating=rating,
734            )
735
736        next_stability = self._clamp_stability(stability=next_stability)
737
738        return next_stability
739
740    def _next_forget_stability(
741        self, *, difficulty: float, stability: float, retrievability: float
742    ) -> float:
743        next_forget_stability_long_term_params = (
744            self.parameters[11]
745            * (difficulty ** -self.parameters[12])
746            * (((stability + 1) ** (self.parameters[13])) - 1)
747            * (math.e ** ((1 - retrievability) * self.parameters[14]))
748        )
749
750        next_forget_stability_short_term_params = stability / (
751            math.e ** (self.parameters[17] * self.parameters[18])
752        )
753
754        return min(
755            next_forget_stability_long_term_params,
756            next_forget_stability_short_term_params,
757        )
758
759    def _next_recall_stability(
760        self,
761        *,
762        difficulty: float,
763        stability: float,
764        retrievability: float,
765        rating: Rating,
766    ) -> float:
767        hard_penalty = self.parameters[15] if rating == Rating.Hard else 1
768        easy_bonus = self.parameters[16] if rating == Rating.Easy else 1
769
770        return stability * (
771            1
772            + (math.e ** (self.parameters[8]))
773            * (11 - difficulty)
774            * (stability ** -self.parameters[9])
775            * ((math.e ** ((1 - retrievability) * self.parameters[10])) - 1)
776            * hard_penalty
777            * easy_bonus
778        )
779
780    def _get_fuzzed_interval(self, *, interval: timedelta) -> timedelta:
781        """
782        Takes the current calculated interval and adds a small amount of random fuzz to it.
783        For example, a card that would've been due in 50 days, after fuzzing, might be due in 49, or 51 days.
784
785        Args:
786            interval: The calculated next interval, before fuzzing.
787
788        Returns:
789            timedelta: The new interval, after fuzzing.
790        """
791
792        interval_days = interval.days
793
794        if interval_days < 2.5:  # fuzz is not applied to intervals less than 2.5
795            return interval
796
797        def _get_fuzz_range(*, interval_days: int) -> tuple[int, int]:
798            """
799            Helper function that computes the possible upper and lower bounds of the interval after fuzzing.
800            """
801
802            delta = 1.0
803            for fuzz_range in FUZZ_RANGES:
804                delta += fuzz_range["factor"] * max(
805                    min(interval_days, fuzz_range["end"]) - fuzz_range["start"], 0.0
806                )
807
808            min_ivl = int(round(interval_days - delta))
809            max_ivl = int(round(interval_days + delta))
810
811            # make sure the min_ivl and max_ivl fall into a valid range
812            min_ivl = max(2, min_ivl)
813            max_ivl = min(max_ivl, self.maximum_interval)
814            min_ivl = min(min_ivl, max_ivl)
815
816            return min_ivl, max_ivl
817
818        min_ivl, max_ivl = _get_fuzz_range(interval_days=interval_days)
819
820        fuzzed_interval_days = (
821            random() * (max_ivl - min_ivl + 1)
822        ) + min_ivl  # the next interval is a random value between min_ivl and max_ivl
823
824        fuzzed_interval_days = min(round(fuzzed_interval_days), self.maximum_interval)
825
826        fuzzed_interval = timedelta(days=fuzzed_interval_days)
827
828        return fuzzed_interval

The FSRS scheduler.

Enables the reviewing and future scheduling of cards according to the FSRS algorithm.

Attributes: parameters: The model weights of the FSRS scheduler. desired_retention: The desired retention rate of cards scheduled with the scheduler. learning_steps: Small time intervals that schedule cards in the Learning state. relearning_steps: Small time intervals that schedule cards in the Relearning state. maximum_interval: The maximum number of days a Review-state card can be scheduled into the future. enable_fuzzing: Whether to apply a small amount of random 'fuzz' to calculated intervals.

Scheduler( parameters: Sequence[float] = (0.212, 1.2931, 2.3065, 8.2956, 6.4133, 0.8334, 3.0194, 0.001, 1.8722, 0.1666, 0.796, 1.4835, 0.0614, 0.2629, 1.6483, 0.6014, 1.8729, 0.5425, 0.0912, 0.0658, 0.1542), desired_retention: float = 0.9, learning_steps: tuple[datetime.timedelta, ...] | list[datetime.timedelta] = (datetime.timedelta(seconds=60), datetime.timedelta(seconds=600)), relearning_steps: tuple[datetime.timedelta, ...] | list[datetime.timedelta] = (datetime.timedelta(seconds=600),), maximum_interval: int = 36500, enable_fuzzing: bool = True)
161    def __init__(
162        self,
163        parameters: Sequence[float] = DEFAULT_PARAMETERS,
164        desired_retention: float = 0.9,
165        learning_steps: tuple[timedelta, ...] | list[timedelta] = (
166            timedelta(minutes=1),
167            timedelta(minutes=10),
168        ),
169        relearning_steps: tuple[timedelta, ...] | list[timedelta] = (
170            timedelta(minutes=10),
171        ),
172        maximum_interval: int = 36500,
173        enable_fuzzing: bool = True,
174    ) -> None:
175        self._validate_parameters(parameters=parameters)
176
177        self.parameters = tuple(parameters)
178        self.desired_retention = desired_retention
179        self.learning_steps = tuple(learning_steps)
180        self.relearning_steps = tuple(relearning_steps)
181        self.maximum_interval = maximum_interval
182        self.enable_fuzzing = enable_fuzzing
183
184        self._DECAY = -self.parameters[20]
185        self._FACTOR = 0.9 ** (1 / self._DECAY) - 1
parameters: tuple[float, ...]
desired_retention: float
learning_steps: tuple[datetime.timedelta, ...]
relearning_steps: tuple[datetime.timedelta, ...]
maximum_interval: int
enable_fuzzing: bool
def get_card_retrievability( self, card: Card, current_datetime: datetime.datetime | None = None) -> float:
207    def get_card_retrievability(
208        self, card: Card, current_datetime: datetime | None = None
209    ) -> float:
210        """
211        Calculates a Card object's current retrievability for a given date and time.
212
213        The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime.
214
215        Args:
216            card: The card whose retrievability is to be calculated
217            current_datetime: The current date and time
218
219        Returns:
220            float: The retrievability of the Card object.
221        """
222
223        if card.last_review is None:
224            return 0
225
226        if current_datetime is None:
227            current_datetime = datetime.now(timezone.utc)
228
229        elapsed_days = max(0, (current_datetime - card.last_review).days)
230
231        return (1 + self._FACTOR * elapsed_days / card.stability) ** self._DECAY

Calculates a Card object's current retrievability for a given date and time.

The retrievability of a card is the predicted probability that the card is correctly recalled at the provided datetime.

Args: card: The card whose retrievability is to be calculated current_datetime: The current date and time

Returns: float: The retrievability of the Card object.

def review_card( self, card: Card, rating: Rating, review_datetime: datetime.datetime | None = None, review_duration: int | None = None) -> tuple[Card, ReviewLog]:
233    def review_card(
234        self,
235        card: Card,
236        rating: Rating,
237        review_datetime: datetime | None = None,
238        review_duration: int | None = None,
239    ) -> tuple[Card, ReviewLog]:
240        """
241        Reviews a card with a given rating at a given time for a specified duration.
242
243        Args:
244            card: The card being reviewed.
245            rating: The chosen rating for the card being reviewed.
246            review_datetime: The date and time of the review.
247            review_duration: The number of miliseconds it took to review the card or None if unspecified.
248
249        Returns:
250            tuple[Card,ReviewLog]: A tuple containing the updated, reviewed card and its corresponding review log.
251
252        Raises:
253            ValueError: If the `review_datetime` argument is not timezone-aware and set to UTC.
254        """
255
256        if review_datetime is not None and (
257            (review_datetime.tzinfo is None) or (review_datetime.tzinfo != timezone.utc)
258        ):
259            raise ValueError("datetime must be timezone-aware and set to UTC")
260
261        card = copy(card)
262
263        if review_datetime is None:
264            review_datetime = datetime.now(timezone.utc)
265
266        days_since_last_review = (
267            (review_datetime - card.last_review).days if card.last_review else None
268        )
269
270        match card.state:
271            case State.Learning:
272                assert card.step is not None
273
274                # update the card's stability and difficulty
275                if card.stability is None or card.difficulty is None:
276                    card.stability = self._initial_stability(rating=rating)
277                    card.difficulty = self._initial_difficulty(
278                        rating=rating, clamp=True
279                    )
280
281                elif days_since_last_review is not None and days_since_last_review < 1:
282                    card.stability = self._short_term_stability(
283                        stability=card.stability, rating=rating
284                    )
285                    card.difficulty = self._next_difficulty(
286                        difficulty=card.difficulty, rating=rating
287                    )
288
289                else:
290                    card.stability = self._next_stability(
291                        difficulty=card.difficulty,
292                        stability=card.stability,
293                        retrievability=self.get_card_retrievability(
294                            card,
295                            current_datetime=review_datetime,
296                        ),
297                        rating=rating,
298                    )
299                    card.difficulty = self._next_difficulty(
300                        difficulty=card.difficulty, rating=rating
301                    )
302
303                # calculate the card's next interval
304                ## first if-clause handles edge case where the Card in the Learning state was previously
305                ## scheduled with a Scheduler with more learning_steps than the current Scheduler
306                if len(self.learning_steps) == 0 or (
307                    card.step >= len(self.learning_steps)
308                    and rating in (Rating.Hard, Rating.Good, Rating.Easy)
309                ):
310                    card.state = State.Review
311                    card.step = None
312
313                    next_interval_days = self._next_interval(stability=card.stability)
314                    next_interval = timedelta(days=next_interval_days)
315
316                else:
317                    match rating:
318                        case Rating.Again:
319                            card.step = 0
320                            next_interval = self.learning_steps[card.step]
321
322                        case Rating.Hard:
323                            # card step stays the same
324
325                            if card.step == 0 and len(self.learning_steps) == 1:
326                                next_interval = self.learning_steps[0] * 1.5
327                            elif card.step == 0 and len(self.learning_steps) >= 2:
328                                next_interval = (
329                                    self.learning_steps[0] + self.learning_steps[1]
330                                ) / 2.0
331                            else:
332                                next_interval = self.learning_steps[card.step]
333
334                        case Rating.Good:
335                            if card.step + 1 == len(
336                                self.learning_steps
337                            ):  # the last step
338                                card.state = State.Review
339                                card.step = None
340
341                                next_interval_days = self._next_interval(
342                                    stability=card.stability
343                                )
344                                next_interval = timedelta(days=next_interval_days)
345
346                            else:
347                                card.step += 1
348                                next_interval = self.learning_steps[card.step]
349
350                        case Rating.Easy:
351                            card.state = State.Review
352                            card.step = None
353
354                            next_interval_days = self._next_interval(
355                                stability=card.stability
356                            )
357                            next_interval = timedelta(days=next_interval_days)
358
359            case State.Review:
360                assert card.stability is not None
361                assert card.difficulty is not None
362
363                # update the card's stability and difficulty
364                if days_since_last_review is not None and days_since_last_review < 1:
365                    card.stability = self._short_term_stability(
366                        stability=card.stability, rating=rating
367                    )
368                else:
369                    card.stability = self._next_stability(
370                        difficulty=card.difficulty,
371                        stability=card.stability,
372                        retrievability=self.get_card_retrievability(
373                            card,
374                            current_datetime=review_datetime,
375                        ),
376                        rating=rating,
377                    )
378
379                card.difficulty = self._next_difficulty(
380                    difficulty=card.difficulty, rating=rating
381                )
382
383                # calculate the card's next interval
384                match rating:
385                    case Rating.Again:
386                        # if there are no relearning steps (they were left blank)
387                        if len(self.relearning_steps) == 0:
388                            next_interval_days = self._next_interval(
389                                stability=card.stability
390                            )
391                            next_interval = timedelta(days=next_interval_days)
392
393                        else:
394                            card.state = State.Relearning
395                            card.step = 0
396
397                            next_interval = self.relearning_steps[card.step]
398
399                    case Rating.Hard | Rating.Good | Rating.Easy:
400                        next_interval_days = self._next_interval(
401                            stability=card.stability
402                        )
403                        next_interval = timedelta(days=next_interval_days)
404
405            case State.Relearning:
406                assert card.stability is not None
407                assert card.difficulty is not None
408                assert card.step is not None
409
410                # update the card's stability and difficulty
411                if days_since_last_review is not None and days_since_last_review < 1:
412                    card.stability = self._short_term_stability(
413                        stability=card.stability, rating=rating
414                    )
415                    card.difficulty = self._next_difficulty(
416                        difficulty=card.difficulty, rating=rating
417                    )
418
419                else:
420                    card.stability = self._next_stability(
421                        difficulty=card.difficulty,
422                        stability=card.stability,
423                        retrievability=self.get_card_retrievability(
424                            card,
425                            current_datetime=review_datetime,
426                        ),
427                        rating=rating,
428                    )
429                    card.difficulty = self._next_difficulty(
430                        difficulty=card.difficulty, rating=rating
431                    )
432
433                # calculate the card's next interval
434                ## first if-clause handles edge case where the Card in the Relearning state was previously
435                ## scheduled with a Scheduler with more relearning_steps than the current Scheduler
436                if len(self.relearning_steps) == 0 or (
437                    card.step >= len(self.relearning_steps)
438                    and rating in (Rating.Hard, Rating.Good, Rating.Easy)
439                ):
440                    card.state = State.Review
441                    card.step = None
442
443                    next_interval_days = self._next_interval(stability=card.stability)
444                    next_interval = timedelta(days=next_interval_days)
445
446                else:
447                    match rating:
448                        case Rating.Again:
449                            card.step = 0
450                            next_interval = self.relearning_steps[card.step]
451
452                        case Rating.Hard:
453                            # card step stays the same
454
455                            if card.step == 0 and len(self.relearning_steps) == 1:
456                                next_interval = self.relearning_steps[0] * 1.5
457                            elif card.step == 0 and len(self.relearning_steps) >= 2:
458                                next_interval = (
459                                    self.relearning_steps[0] + self.relearning_steps[1]
460                                ) / 2.0
461                            else:
462                                next_interval = self.relearning_steps[card.step]
463
464                        case Rating.Good:
465                            if card.step + 1 == len(
466                                self.relearning_steps
467                            ):  # the last step
468                                card.state = State.Review
469                                card.step = None
470
471                                next_interval_days = self._next_interval(
472                                    stability=card.stability
473                                )
474                                next_interval = timedelta(days=next_interval_days)
475
476                            else:
477                                card.step += 1
478                                next_interval = self.relearning_steps[card.step]
479
480                        case Rating.Easy:
481                            card.state = State.Review
482                            card.step = None
483
484                            next_interval_days = self._next_interval(
485                                stability=card.stability
486                            )
487                            next_interval = timedelta(days=next_interval_days)
488
489        if self.enable_fuzzing and card.state == State.Review:
490            next_interval = self._get_fuzzed_interval(interval=next_interval)
491
492        card.due = review_datetime + next_interval
493        card.last_review = review_datetime
494
495        review_log = ReviewLog(
496            card_id=card.card_id,
497            rating=rating,
498            review_datetime=review_datetime,
499            review_duration=review_duration,
500        )
501
502        return card, review_log

Reviews a card with a given rating at a given time for a specified duration.

Args: card: The card being reviewed. rating: The chosen rating for the card being reviewed. review_datetime: The date and time of the review. review_duration: The number of miliseconds it took to review the card or None if unspecified.

Returns: tuple[Card,ReviewLog]: A tuple containing the updated, reviewed card and its corresponding review log.

Raises: ValueError: If the review_datetime argument is not timezone-aware and set to UTC.

def reschedule_card( self, card: Card, review_logs: list[ReviewLog]) -> Card:
504    def reschedule_card(self, card: Card, review_logs: list[ReviewLog]) -> Card:
505        """
506        Reschedules/updates the given card with the current scheduler provided that card's review logs.
507
508        If the current card was previously scheduled with a different scheduler, you may want to reschedule/update
509        it as if it had always been scheduled with this current scheduler. For example, you may want to reschedule
510        each of your cards with a new scheduler after computing the optimal parameters with the Optimizer.
511
512        Args:
513            card: The card to be rescheduled/updated.
514            review_logs: A list of that card's review logs (order doesn't matter).
515
516        Returns:
517            Card: A new card that has been rescheduled/updated with this current scheduler.
518
519        Raises:
520            ValueError: If any of the review logs are for a card other than the one specified, this will raise an error.
521
522        """
523
524        for review_log in review_logs:
525            if review_log.card_id != card.card_id:
526                raise ValueError(
527                    f"ReviewLog card_id {review_log.card_id} does not match Card card_id {card.card_id}"
528                )
529
530        review_logs = sorted(review_logs, key=lambda log: log.review_datetime)
531
532        rescheduled_card = Card(card_id=card.card_id, due=card.due)
533
534        for review_log in review_logs:
535            rescheduled_card, _ = self.review_card(
536                card=rescheduled_card,
537                rating=review_log.rating,
538                review_datetime=review_log.review_datetime,
539            )
540
541        return rescheduled_card

Reschedules/updates the given card with the current scheduler provided that card's review logs.

If the current card was previously scheduled with a different scheduler, you may want to reschedule/update it as if it had always been scheduled with this current scheduler. For example, you may want to reschedule each of your cards with a new scheduler after computing the optimal parameters with the Optimizer.

Args: card: The card to be rescheduled/updated. review_logs: A list of that card's review logs (order doesn't matter).

Returns: Card: A new card that has been rescheduled/updated with this current scheduler.

Raises: ValueError: If any of the review logs are for a card other than the one specified, this will raise an error.

def to_dict(self) -> fsrs.scheduler.SchedulerDict:
543    def to_dict(
544        self,
545    ) -> SchedulerDict:
546        """
547        Returns a dictionary representation of the Scheduler object.
548
549        Returns:
550            SchedulerDict: A dictionary representation of the Scheduler object.
551        """
552
553        return {
554            "parameters": list(self.parameters),
555            "desired_retention": self.desired_retention,
556            "learning_steps": [
557                int(learning_step.total_seconds())
558                for learning_step in self.learning_steps
559            ],
560            "relearning_steps": [
561                int(relearning_step.total_seconds())
562                for relearning_step in self.relearning_steps
563            ],
564            "maximum_interval": self.maximum_interval,
565            "enable_fuzzing": self.enable_fuzzing,
566        }

Returns a dictionary representation of the Scheduler object.

Returns: SchedulerDict: A dictionary representation of the Scheduler object.

@classmethod
def from_dict(cls, source_dict: fsrs.scheduler.SchedulerDict) -> Self:
568    @classmethod
569    def from_dict(cls, source_dict: SchedulerDict) -> Self:
570        """
571        Creates a Scheduler object from an existing dictionary.
572
573        Args:
574            source_dict: A dictionary representing an existing Scheduler object.
575
576        Returns:
577            Self: A Scheduler object created from the provided dictionary.
578        """
579
580        return cls(
581            parameters=source_dict["parameters"],
582            desired_retention=source_dict["desired_retention"],
583            learning_steps=[
584                timedelta(seconds=learning_step)
585                for learning_step in source_dict["learning_steps"]
586            ],
587            relearning_steps=[
588                timedelta(seconds=relearning_step)
589                for relearning_step in source_dict["relearning_steps"]
590            ],
591            maximum_interval=source_dict["maximum_interval"],
592            enable_fuzzing=source_dict["enable_fuzzing"],
593        )

Creates a Scheduler object from an existing dictionary.

Args: source_dict: A dictionary representing an existing Scheduler object.

Returns: Self: A Scheduler object created from the provided dictionary.

def to_json(self, indent: int | str | None = None) -> str:
595    def to_json(self, indent: int | str | None = None) -> str:
596        """
597        Returns a JSON-serialized string of the Scheduler object.
598
599        Args:
600            indent: Equivalent argument to the indent in json.dumps()
601
602        Returns:
603            str: A JSON-serialized string of the Scheduler object.
604        """
605
606        return json.dumps(self.to_dict(), indent=indent)

Returns a JSON-serialized string of the Scheduler object.

Args: indent: Equivalent argument to the indent in json.dumps()

Returns: str: A JSON-serialized string of the Scheduler object.

@classmethod
def from_json(cls, source_json: str) -> Self:
608    @classmethod
609    def from_json(cls, source_json: str) -> Self:
610        """
611        Creates a Scheduler object from a JSON-serialized string.
612
613        Args:
614            source_json: A JSON-serialized string of an existing Scheduler object.
615
616        Returns:
617            Self: A Scheduler object created from the JSON string.
618        """
619
620        source_dict: SchedulerDict = json.loads(source_json)
621        return cls.from_dict(source_dict=source_dict)

Creates a Scheduler object from a JSON-serialized string.

Args: source_json: A JSON-serialized string of an existing Scheduler object.

Returns: Self: A Scheduler object created from the JSON string.

@dataclass(init=False)
class Card:
 37@dataclass(init=False)
 38class Card:
 39    """
 40    Represents a flashcard in the FSRS system.
 41
 42    Attributes:
 43        card_id: The id of the card. Defaults to the epoch milliseconds of when the card was created.
 44        state: The card's current learning state.
 45        step: The card's current learning or relearning step or None if the card is in the Review state.
 46        stability: Core mathematical parameter used for future scheduling.
 47        difficulty: Core mathematical parameter used for future scheduling.
 48        due: The date and time when the card is due next.
 49        last_review: The date and time of the card's last review.
 50    """
 51
 52    card_id: int
 53    state: State
 54    step: int | None
 55    stability: float | None
 56    difficulty: float | None
 57    due: datetime
 58    last_review: datetime | None
 59
 60    def __init__(
 61        self,
 62        card_id: int | None = None,
 63        state: State = State.Learning,
 64        step: int | None = None,
 65        stability: float | None = None,
 66        difficulty: float | None = None,
 67        due: datetime | None = None,
 68        last_review: datetime | None = None,
 69    ) -> None:
 70        if card_id is None:
 71            # epoch milliseconds of when the card was created
 72            card_id = int(datetime.now(timezone.utc).timestamp() * 1000)
 73            # wait 1ms to prevent potential card_id collision on next Card creation
 74            time.sleep(0.001)
 75        self.card_id = card_id
 76
 77        self.state = state
 78
 79        if self.state == State.Learning and step is None:
 80            step = 0
 81        self.step = step
 82
 83        self.stability = stability
 84        self.difficulty = difficulty
 85
 86        if due is None:
 87            due = datetime.now(timezone.utc)
 88        self.due = due
 89
 90        self.last_review = last_review
 91
 92    def to_dict(self) -> CardDict:
 93        """
 94        Returns a dictionary representation of the Card object.
 95
 96        Returns:
 97            CardDict: A dictionary representation of the Card object.
 98        """
 99
100        return {
101            "card_id": self.card_id,
102            "state": self.state.value,
103            "step": self.step,
104            "stability": self.stability,
105            "difficulty": self.difficulty,
106            "due": self.due.isoformat(),
107            "last_review": self.last_review.isoformat() if self.last_review else None,
108        }
109
110    @classmethod
111    def from_dict(cls, source_dict: CardDict) -> Self:
112        """
113        Creates a Card object from an existing dictionary.
114
115        Args:
116            source_dict: A dictionary representing an existing Card object.
117
118        Returns:
119            Self: A Card object created from the provided dictionary.
120        """
121
122        return cls(
123            card_id=int(source_dict["card_id"]),
124            state=State(int(source_dict["state"])),
125            step=source_dict["step"],
126            stability=(
127                float(source_dict["stability"]) if source_dict["stability"] else None
128            ),
129            difficulty=(
130                float(source_dict["difficulty"]) if source_dict["difficulty"] else None
131            ),
132            due=datetime.fromisoformat(source_dict["due"]),
133            last_review=(
134                datetime.fromisoformat(source_dict["last_review"])
135                if source_dict["last_review"]
136                else None
137            ),
138        )
139
140    def to_json(self, indent: int | str | None = None) -> str:
141        """
142        Returns a JSON-serialized string of the Card object.
143
144        Args:
145            indent: Equivalent argument to the indent in json.dumps()
146
147        Returns:
148            str: A JSON-serialized string of the Card object.
149        """
150        return json.dumps(self.to_dict(), indent=indent)
151
152    @classmethod
153    def from_json(cls, source_json: str) -> Self:
154        """
155        Creates a Card object from a JSON-serialized string.
156
157        Args:
158            source_json: A JSON-serialized string of an existing Card object.
159
160        Returns:
161            Self: A Card object created from the JSON string.
162        """
163
164        source_dict: CardDict = json.loads(source_json)
165        return cls.from_dict(source_dict=source_dict)

Represents a flashcard in the FSRS system.

Attributes: card_id: The id of the card. Defaults to the epoch milliseconds of when the card was created. state: The card's current learning state. step: The card's current learning or relearning step or None if the card is in the Review state. stability: Core mathematical parameter used for future scheduling. difficulty: Core mathematical parameter used for future scheduling. due: The date and time when the card is due next. last_review: The date and time of the card's last review.

Card( card_id: int | None = None, state: State = <State.Learning: 1>, step: int | None = None, stability: float | None = None, difficulty: float | None = None, due: datetime.datetime | None = None, last_review: datetime.datetime | None = None)
60    def __init__(
61        self,
62        card_id: int | None = None,
63        state: State = State.Learning,
64        step: int | None = None,
65        stability: float | None = None,
66        difficulty: float | None = None,
67        due: datetime | None = None,
68        last_review: datetime | None = None,
69    ) -> None:
70        if card_id is None:
71            # epoch milliseconds of when the card was created
72            card_id = int(datetime.now(timezone.utc).timestamp() * 1000)
73            # wait 1ms to prevent potential card_id collision on next Card creation
74            time.sleep(0.001)
75        self.card_id = card_id
76
77        self.state = state
78
79        if self.state == State.Learning and step is None:
80            step = 0
81        self.step = step
82
83        self.stability = stability
84        self.difficulty = difficulty
85
86        if due is None:
87            due = datetime.now(timezone.utc)
88        self.due = due
89
90        self.last_review = last_review
card_id: int
state: State
step: int | None
stability: float | None
difficulty: float | None
due: datetime.datetime
last_review: datetime.datetime | None
def to_dict(self) -> fsrs.card.CardDict:
 92    def to_dict(self) -> CardDict:
 93        """
 94        Returns a dictionary representation of the Card object.
 95
 96        Returns:
 97            CardDict: A dictionary representation of the Card object.
 98        """
 99
100        return {
101            "card_id": self.card_id,
102            "state": self.state.value,
103            "step": self.step,
104            "stability": self.stability,
105            "difficulty": self.difficulty,
106            "due": self.due.isoformat(),
107            "last_review": self.last_review.isoformat() if self.last_review else None,
108        }

Returns a dictionary representation of the Card object.

Returns: CardDict: A dictionary representation of the Card object.

@classmethod
def from_dict(cls, source_dict: fsrs.card.CardDict) -> Self:
110    @classmethod
111    def from_dict(cls, source_dict: CardDict) -> Self:
112        """
113        Creates a Card object from an existing dictionary.
114
115        Args:
116            source_dict: A dictionary representing an existing Card object.
117
118        Returns:
119            Self: A Card object created from the provided dictionary.
120        """
121
122        return cls(
123            card_id=int(source_dict["card_id"]),
124            state=State(int(source_dict["state"])),
125            step=source_dict["step"],
126            stability=(
127                float(source_dict["stability"]) if source_dict["stability"] else None
128            ),
129            difficulty=(
130                float(source_dict["difficulty"]) if source_dict["difficulty"] else None
131            ),
132            due=datetime.fromisoformat(source_dict["due"]),
133            last_review=(
134                datetime.fromisoformat(source_dict["last_review"])
135                if source_dict["last_review"]
136                else None
137            ),
138        )

Creates a Card object from an existing dictionary.

Args: source_dict: A dictionary representing an existing Card object.

Returns: Self: A Card object created from the provided dictionary.

def to_json(self, indent: int | str | None = None) -> str:
140    def to_json(self, indent: int | str | None = None) -> str:
141        """
142        Returns a JSON-serialized string of the Card object.
143
144        Args:
145            indent: Equivalent argument to the indent in json.dumps()
146
147        Returns:
148            str: A JSON-serialized string of the Card object.
149        """
150        return json.dumps(self.to_dict(), indent=indent)

Returns a JSON-serialized string of the Card object.

Args: indent: Equivalent argument to the indent in json.dumps()

Returns: str: A JSON-serialized string of the Card object.

@classmethod
def from_json(cls, source_json: str) -> Self:
152    @classmethod
153    def from_json(cls, source_json: str) -> Self:
154        """
155        Creates a Card object from a JSON-serialized string.
156
157        Args:
158            source_json: A JSON-serialized string of an existing Card object.
159
160        Returns:
161            Self: A Card object created from the JSON string.
162        """
163
164        source_dict: CardDict = json.loads(source_json)
165        return cls.from_dict(source_dict=source_dict)

Creates a Card object from a JSON-serialized string.

Args: source_json: A JSON-serialized string of an existing Card object.

Returns: Self: A Card object created from the JSON string.

class Rating(enum.IntEnum):
 5class Rating(IntEnum):
 6    """
 7    Enum representing the four possible ratings when reviewing a card.
 8    """
 9
10    Again = 1
11    Hard = 2
12    Good = 3
13    Easy = 4

Enum representing the four possible ratings when reviewing a card.

Again = <Rating.Again: 1>
Hard = <Rating.Hard: 2>
Good = <Rating.Good: 3>
Easy = <Rating.Easy: 4>
@dataclass
class ReviewLog:
 33@dataclass
 34class ReviewLog:
 35    """
 36    Represents the log entry of a Card object that has been reviewed.
 37
 38    Attributes:
 39        card_id: The id of the card being reviewed.
 40        rating: The rating given to the card during the review.
 41        review_datetime: The date and time of the review.
 42        review_duration: The number of milliseconds it took to review the card or None if unspecified.
 43    """
 44
 45    card_id: int
 46    rating: Rating
 47    review_datetime: datetime
 48    review_duration: int | None
 49
 50    def to_dict(
 51        self,
 52    ) -> ReviewLogDict:
 53        """
 54        Returns a dictionary representation of the ReviewLog object.
 55
 56        Returns:
 57            ReviewLogDict: A dictionary representation of the ReviewLog object.
 58        """
 59
 60        return {
 61            "card_id": self.card_id,
 62            "rating": int(self.rating),
 63            "review_datetime": self.review_datetime.isoformat(),
 64            "review_duration": self.review_duration,
 65        }
 66
 67    @classmethod
 68    def from_dict(
 69        cls,
 70        source_dict: ReviewLogDict,
 71    ) -> Self:
 72        """
 73        Creates a ReviewLog object from an existing dictionary.
 74
 75        Args:
 76            source_dict: A dictionary representing an existing ReviewLog object.
 77
 78        Returns:
 79            Self: A ReviewLog object created from the provided dictionary.
 80        """
 81
 82        return cls(
 83            card_id=source_dict["card_id"],
 84            rating=Rating(int(source_dict["rating"])),
 85            review_datetime=datetime.fromisoformat(source_dict["review_datetime"]),
 86            review_duration=source_dict["review_duration"],
 87        )
 88
 89    def to_json(self, indent: int | str | None = None) -> str:
 90        """
 91        Returns a JSON-serialized string of the ReviewLog object.
 92
 93        Args:
 94            indent: Equivalent argument to the indent in json.dumps()
 95
 96        Returns:
 97            str: A JSON-serialized string of the ReviewLog object.
 98        """
 99
100        return json.dumps(self.to_dict(), indent=indent)
101
102    @classmethod
103    def from_json(cls, source_json: str) -> Self:
104        """
105        Creates a ReviewLog object from a JSON-serialized string.
106
107        Args:
108            source_json: A JSON-serialized string of an existing ReviewLog object.
109
110        Returns:
111            Self: A ReviewLog object created from the JSON string.
112        """
113
114        source_dict: ReviewLogDict = json.loads(source_json)
115        return cls.from_dict(source_dict=source_dict)

Represents the log entry of a Card object that has been reviewed.

Attributes: card_id: The id of the card being reviewed. rating: The rating given to the card during the review. review_datetime: The date and time of the review. review_duration: The number of milliseconds it took to review the card or None if unspecified.

ReviewLog( card_id: int, rating: Rating, review_datetime: datetime.datetime, review_duration: int | None)
card_id: int
rating: Rating
review_datetime: datetime.datetime
review_duration: int | None
def to_dict(self) -> fsrs.review_log.ReviewLogDict:
50    def to_dict(
51        self,
52    ) -> ReviewLogDict:
53        """
54        Returns a dictionary representation of the ReviewLog object.
55
56        Returns:
57            ReviewLogDict: A dictionary representation of the ReviewLog object.
58        """
59
60        return {
61            "card_id": self.card_id,
62            "rating": int(self.rating),
63            "review_datetime": self.review_datetime.isoformat(),
64            "review_duration": self.review_duration,
65        }

Returns a dictionary representation of the ReviewLog object.

Returns: ReviewLogDict: A dictionary representation of the ReviewLog object.

@classmethod
def from_dict(cls, source_dict: fsrs.review_log.ReviewLogDict) -> Self:
67    @classmethod
68    def from_dict(
69        cls,
70        source_dict: ReviewLogDict,
71    ) -> Self:
72        """
73        Creates a ReviewLog object from an existing dictionary.
74
75        Args:
76            source_dict: A dictionary representing an existing ReviewLog object.
77
78        Returns:
79            Self: A ReviewLog object created from the provided dictionary.
80        """
81
82        return cls(
83            card_id=source_dict["card_id"],
84            rating=Rating(int(source_dict["rating"])),
85            review_datetime=datetime.fromisoformat(source_dict["review_datetime"]),
86            review_duration=source_dict["review_duration"],
87        )

Creates a ReviewLog object from an existing dictionary.

Args: source_dict: A dictionary representing an existing ReviewLog object.

Returns: Self: A ReviewLog object created from the provided dictionary.

def to_json(self, indent: int | str | None = None) -> str:
 89    def to_json(self, indent: int | str | None = None) -> str:
 90        """
 91        Returns a JSON-serialized string of the ReviewLog object.
 92
 93        Args:
 94            indent: Equivalent argument to the indent in json.dumps()
 95
 96        Returns:
 97            str: A JSON-serialized string of the ReviewLog object.
 98        """
 99
100        return json.dumps(self.to_dict(), indent=indent)

Returns a JSON-serialized string of the ReviewLog object.

Args: indent: Equivalent argument to the indent in json.dumps()

Returns: str: A JSON-serialized string of the ReviewLog object.

@classmethod
def from_json(cls, source_json: str) -> Self:
102    @classmethod
103    def from_json(cls, source_json: str) -> Self:
104        """
105        Creates a ReviewLog object from a JSON-serialized string.
106
107        Args:
108            source_json: A JSON-serialized string of an existing ReviewLog object.
109
110        Returns:
111            Self: A ReviewLog object created from the JSON string.
112        """
113
114        source_dict: ReviewLogDict = json.loads(source_json)
115        return cls.from_dict(source_dict=source_dict)

Creates a ReviewLog object from a JSON-serialized string.

Args: source_json: A JSON-serialized string of an existing ReviewLog object.

Returns: Self: A ReviewLog object created from the JSON string.

class State(enum.IntEnum):
 5class State(IntEnum):
 6    """
 7    Enum representing the learning state of a Card object.
 8    """
 9
10    Learning = 1
11    Review = 2
12    Relearning = 3

Enum representing the learning state of a Card object.

Learning = <State.Learning: 1>
Review = <State.Review: 2>
Relearning = <State.Relearning: 3>
class Optimizer:
668    class Optimizer:
669        def __init__(self, *args, **kwargs) -> None:
670            raise ImportError(
671                'Optimizer is not installed.\nInstall it with: pip install "fsrs[optimizer]"'
672            )
Optimizer(*args, **kwargs)
669        def __init__(self, *args, **kwargs) -> None:
670            raise ImportError(
671                'Optimizer is not installed.\nInstall it with: pip install "fsrs[optimizer]"'
672            )