From 70117539aaa429d960e880ae1d11f99dcf8d72fa Mon Sep 17 00:00:00 2001 From: Joshua Hamilton Date: Sat, 7 Dec 2024 16:02:46 -0600 Subject: [PATCH] format with ruff --- setup.py | 2 +- src/leitner_box/__init__.py | 2 +- src/leitner_box/leitner_box.py | 121 ++++++++++++++++++++------------- tests/test_leitner_box.py | 60 ++++++++++------ 4 files changed, 114 insertions(+), 71 deletions(-) diff --git a/setup.py b/setup.py index c1057cf..b908cbe 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,3 @@ import setuptools -setuptools.setup() \ No newline at end of file +setuptools.setup() diff --git a/src/leitner_box/__init__.py b/src/leitner_box/__init__.py index bc39c4b..3203983 100644 --- a/src/leitner_box/__init__.py +++ b/src/leitner_box/__init__.py @@ -4,4 +4,4 @@ The classic Leitner System for Spaced Repetition, implemented as a python package. """ -from .leitner_box import Scheduler, Card, Rating, ReviewLog \ No newline at end of file +from .leitner_box import Scheduler, Card, Rating, ReviewLog diff --git a/src/leitner_box/leitner_box.py b/src/leitner_box/leitner_box.py index 7849cd3..73ca9b2 100644 --- a/src/leitner_box/leitner_box.py +++ b/src/leitner_box/leitner_box.py @@ -15,6 +15,7 @@ from typing import Any, Literal from copy import deepcopy + class Rating(IntEnum): """ Enum representing the two possible ratings when reviewing a Card object. @@ -23,6 +24,7 @@ class Rating(IntEnum): Fail = 0 Pass = 1 + class Card: """ Represents a flashcard in the Leitner System. @@ -37,8 +39,9 @@ class Card: box: int due: datetime | None - def __init__(self, card_id: int | None = None, box: int=1, due: datetime | None = None) -> None: - + def __init__( + self, card_id: int | None = None, box: int = 1, due: datetime | None = None + ) -> None: if card_id is None: card_id = int(datetime.now(timezone.utc).timestamp() * 1000) self.card_id = card_id @@ -47,34 +50,28 @@ def __init__(self, card_id: int | None = None, box: int=1, due: datetime | None self.due = due def to_dict(self) -> dict[str, int | str | None]: - return_dict: dict[str, int | str | None] = { "card_id": self.card_id, "box": self.box, } if self.due is not None: - return_dict["due"] = self.due.isoformat() else: - return_dict["due"] = self.due return return_dict - + @staticmethod def from_dict(source_dict: dict[str, Any]) -> "Card": + card_id = int(source_dict["card_id"]) + box = int(source_dict["box"]) - card_id = int(source_dict['card_id']) - box = int(source_dict['box']) - - if source_dict['due'] is None: - - due = source_dict['due'] + if source_dict["due"] is None: + due = source_dict["due"] else: - due = datetime.fromisoformat(source_dict["due"]) return Card(card_id=card_id, box=box, due=due) @@ -83,7 +80,7 @@ def from_dict(source_dict: dict[str, Any]) -> "Card": class ReviewLog: """ Represents the log entry of a Card object that has been reviewed. - + Attributes: card (Card): Copy of the card object that was reviewed. rating (Rating): The rating given to the card during the review. @@ -96,33 +93,42 @@ class ReviewLog: review_datetime: datetime review_duration: int | None - def __init__(self, card: Card, rating: Rating, review_datetime: datetime, review_duration: int | None = None) -> None: - + def __init__( + self, + card: Card, + rating: Rating, + review_datetime: datetime, + review_duration: int | None = None, + ) -> None: self.card = deepcopy(card) self.rating = rating self.review_datetime = review_datetime self.review_duration = review_duration def to_dict(self) -> dict[str, dict[str, int | str | None] | int | str | None]: - return_dict = { "card": self.card.to_dict(), "rating": self.rating.value, "review_datetime": self.review_datetime.isoformat(), - "review_duration": self.review_duration + "review_duration": self.review_duration, } return return_dict - + @staticmethod def from_dict(source_dict: dict[str, Any]) -> "ReviewLog": - - card = Card.from_dict(source_dict['card']) + card = Card.from_dict(source_dict["card"]) rating = Rating(int(source_dict["rating"])) review_datetime = datetime.fromisoformat(source_dict["review_datetime"]) - review_duration = source_dict['review_duration'] + review_duration = source_dict["review_duration"] + + return ReviewLog( + card=card, + rating=rating, + review_datetime=review_datetime, + review_duration=review_duration, + ) - return ReviewLog(card=card, rating=rating, review_datetime=review_datetime, review_duration=review_duration) class Scheduler: """ @@ -140,13 +146,18 @@ class Scheduler: start_datetime: datetime on_fail: str - def __init__(self, box_intervals: list[int]=[1, 2, 7], start_datetime: datetime | None = None, on_fail: Literal['first_box', 'prev_box']='first_box') -> None: - + def __init__( + self, + box_intervals: list[int] = [1, 2, 7], + start_datetime: datetime | None = None, + on_fail: Literal["first_box", "prev_box"] = "first_box", + ) -> None: if box_intervals[0] != 1: + raise ValueError( + "Box 1 must have an interval of 1 day. This may change in future versions." + ) - raise ValueError("Box 1 must have an interval of 1 day. This may change in future versions.") - - self.box_intervals = box_intervals # how many days in between you review each box; default box1 - everyday, box2 - every 2 days, box3, every seven days + self.box_intervals = box_intervals # how many days in between you review each box; default box1 - everyday, box2 - every 2 days, box3, every seven days if start_datetime is None: self.start_datetime = datetime.now() else: @@ -155,7 +166,13 @@ def __init__(self, box_intervals: list[int]=[1, 2, 7], start_datetime: datetime self.on_fail = on_fail - def review_card(self, card: Card, rating: Rating, review_datetime: datetime | None = None, review_duration: int | None = None) -> tuple[Card, ReviewLog]: + def review_card( + self, + card: Card, + rating: Rating, + review_datetime: datetime | None = None, + review_duration: int | None = None, + ) -> tuple[Card, ReviewLog]: """ Reviews a card with a given rating at a specified time. @@ -175,61 +192,69 @@ def review_card(self, card: Card, rating: Rating, review_datetime: datetime | No if review_datetime is None: review_datetime = datetime.now() - review_log = ReviewLog(card=card, rating=rating, review_datetime=review_datetime, review_duration=review_duration) + review_log = ReviewLog( + card=card, + rating=rating, + review_datetime=review_datetime, + review_duration=review_duration, + ) - review_datetime = review_datetime.replace(tzinfo=None) # review log datetimes can log timezone info, but it is dropped immediately after + review_datetime = review_datetime.replace( + tzinfo=None + ) # review log datetimes can log timezone info, but it is dropped immediately after # the card to be returned after review new_card = deepcopy(card) if new_card.due is None: - new_card.due = review_datetime.replace(hour=0, minute=0, second=0, microsecond=0) # beginning of the day of review + new_card.due = review_datetime.replace( + hour=0, minute=0, second=0, microsecond=0 + ) # beginning of the day of review card_is_due = review_datetime >= new_card.due if not card_is_due: raise RuntimeError(f"Card is not due for review until {new_card.due}.") if rating == Rating.Fail: - - if self.on_fail == 'first_box': + if self.on_fail == "first_box": new_card.box = 1 - elif self.on_fail == 'prev_box' and new_card.box > 1: + elif self.on_fail == "prev_box" and new_card.box > 1: new_card.box -= 1 elif rating == Rating.Pass: - if new_card.box < len(self.box_intervals): new_card.box += 1 - interval = self.box_intervals[new_card.box-1] + interval = self.box_intervals[new_card.box - 1] - begin_datetime = (self.start_datetime - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + begin_datetime = (self.start_datetime - timedelta(days=1)).replace( + hour=0, minute=0, second=0, microsecond=0 + ) i = 1 next_due_date = begin_datetime + (timedelta(days=interval) * i) while next_due_date <= review_datetime: - next_due_date = begin_datetime + (timedelta(days=interval) * i) i += 1 new_card.due = next_due_date return new_card, review_log - - def to_dict(self) -> dict[str, list[int] | int | str]: + def to_dict(self) -> dict[str, list[int] | int | str]: return_dict: dict[str, list[int] | int | str] = { "box_intervals": self.box_intervals, "start_datetime": self.start_datetime.isoformat(), - "on_fail": self.on_fail + "on_fail": self.on_fail, } return return_dict - + @staticmethod def from_dict(source_dict: dict[str, Any]) -> "Scheduler": + box_intervals = source_dict["box_intervals"] + start_datetime = datetime.fromisoformat(source_dict["start_datetime"]) + on_fail = source_dict["on_fail"] - box_intervals = source_dict['box_intervals'] - start_datetime = datetime.fromisoformat(source_dict['start_datetime']) - on_fail = source_dict['on_fail'] - - return Scheduler(box_intervals=box_intervals, start_datetime=start_datetime, on_fail=on_fail) \ No newline at end of file + return Scheduler( + box_intervals=box_intervals, start_datetime=start_datetime, on_fail=on_fail + ) diff --git a/tests/test_leitner_box.py b/tests/test_leitner_box.py index 05ca0af..27191f4 100644 --- a/tests/test_leitner_box.py +++ b/tests/test_leitner_box.py @@ -5,10 +5,9 @@ import pytest from copy import deepcopy -class TestLeitnerBox: +class TestLeitnerBox: def test_basic_review_schedule(self): - # create Leitner system at 2:30pm on Jan. 1, 2024 start_datetime = datetime(2024, 1, 1, 14, 30, 0, 0) scheduler = Scheduler(start_datetime=start_datetime) @@ -77,10 +76,9 @@ def test_basic_review_schedule(self): assert card.due == datetime(2024, 1, 16, 0, 0, 0, 0) def test_basic_review_schedule_with_on_fail_prev_box(self): - # create Leitner system at 2:30pm on Jan. 1, 2024 start_datetime = datetime(2024, 1, 1, 14, 30, 0, 0) - on_fail = 'prev_box' + on_fail = "prev_box" scheduler = Scheduler(start_datetime=start_datetime, on_fail=on_fail) # create new Card @@ -146,7 +144,6 @@ def test_basic_review_schedule_with_on_fail_prev_box(self): assert card.due == datetime(2024, 1, 21, 0, 0, 0, 0) def test_basic_review_schedule_with_utc(self): - # create Leitner system at 2:30pm on Jan. 1, 2024 UTC start_datetime = datetime(2024, 1, 1, 14, 30, 0, 0, timezone.utc) scheduler = Scheduler(start_datetime=start_datetime) @@ -216,9 +213,10 @@ def test_basic_review_schedule_with_utc(self): assert card.due == datetime(2024, 1, 16, 0, 0, 0, 0) def test_basic_review_schedule_with_la_timezone(self): - # create Leitner system at 2:30pm on Jan. 1, 2024 UTC - start_datetime = datetime(2024, 1, 1, 14, 30, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + start_datetime = datetime( + 2024, 1, 1, 14, 30, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) scheduler = Scheduler(start_datetime=start_datetime) # create new Card @@ -229,17 +227,23 @@ def test_basic_review_schedule_with_la_timezone(self): # fail the card 2:35pm on Jan. 1, 2024 rating = Rating.Fail - review_datetime = datetime(2024, 1, 1, 14, 35, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 1, 14, 35, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) card, review_log = scheduler.review_card(card, rating, review_datetime) assert card.box == 1 assert card.due == datetime(2024, 1, 2, 0, 0, 0, 0) assert card.due != datetime(2024, 1, 2, 0, 0, 0, 0, timezone.utc) - assert card.due != datetime(2024, 1, 2, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + assert card.due != datetime( + 2024, 1, 2, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) # pass the card on Jan. 2 rating = Rating.Pass - review_datetime = datetime(2024, 1, 2, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 2, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) card, review_log = scheduler.review_card(card, rating, review_datetime) assert card.box == 2 @@ -247,13 +251,17 @@ def test_basic_review_schedule_with_la_timezone(self): # attempt to pass the card on Jan. 3 when it is not due rating = Rating.Pass - review_datetime = datetime(2024, 1, 3, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 3, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) with pytest.raises(RuntimeError): card, review_log = scheduler.review_card(card, rating, review_datetime) # pass card on Jan. 4 rating = Rating.Pass - review_datetime = datetime(2024, 1, 4, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 4, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) card, review_log = scheduler.review_card(card, rating, review_datetime) assert card.box == 3 @@ -261,7 +269,9 @@ def test_basic_review_schedule_with_la_timezone(self): # pass card on Jan. 7 rating = Rating.Pass - review_datetime = datetime(2024, 1, 7, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 7, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) card, review_log = scheduler.review_card(card, rating, review_datetime) # card is still in box 3 @@ -270,7 +280,9 @@ def test_basic_review_schedule_with_la_timezone(self): # fail card on Jan. 14 rating = Rating.Fail - review_datetime = datetime(2024, 1, 14, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 14, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) card, review_log = scheduler.review_card(card, rating, review_datetime) # card moves back to box 1 @@ -278,7 +290,9 @@ def test_basic_review_schedule_with_la_timezone(self): assert card.due == datetime(2024, 1, 15, 0, 0, 0, 0) rating = Rating.Pass - review_datetime = datetime(2024, 1, 15, 0, 0, 0, 0, tzinfo=ZoneInfo('America/Los_Angeles')) + review_datetime = datetime( + 2024, 1, 15, 0, 0, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles") + ) card, review_log = scheduler.review_card(card, rating, review_datetime) @@ -287,11 +301,12 @@ def test_basic_review_schedule_with_la_timezone(self): assert card.due == datetime(2024, 1, 16, 0, 0, 0, 0) def test_box_intervals(self): - # create Leitner system at 2:30pm on Jan. 1, 2024 - box_intervals = [1,2,3,5] + box_intervals = [1, 2, 3, 5] start_datetime = datetime(2024, 1, 1, 14, 30, 0, 0) - scheduler = Scheduler(box_intervals=box_intervals, start_datetime=start_datetime) + scheduler = Scheduler( + box_intervals=box_intervals, start_datetime=start_datetime + ) # create new Card card = Card() @@ -363,7 +378,6 @@ def test_box_intervals(self): assert card.due == datetime(2024, 1, 21, 0, 0, 0, 0) def test_serialize(self): - scheduler = Scheduler() card = Card() @@ -388,7 +402,9 @@ def test_serialize(self): # review the card and perform more tests rating = Rating.Pass review_duration = 3000 - card, review_log = scheduler.review_card(card=card, rating=rating, review_duration=review_duration) + card, review_log = scheduler.review_card( + card=card, rating=rating, review_duration=review_duration + ) # review log is json-serializable assert type(json.dumps(review_log.to_dict())) is str @@ -405,4 +421,6 @@ def test_serialize(self): assert review_log.to_dict() == copied_review_log.to_dict() assert copied_review_log.review_duration == review_duration # can use the review log to recreate the card that was reviewed - assert old_card.to_dict() == Card.from_dict(review_log.to_dict()['card']).to_dict() \ No newline at end of file + assert ( + old_card.to_dict() == Card.from_dict(review_log.to_dict()["card"]).to_dict() + )