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"]
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.