From 28afae563e3e32187b389db4680bcc08b9fc55a9 Mon Sep 17 00:00:00 2001 From: Eddy Pedroni Date: Wed, 25 Sep 2024 16:12:53 +0200 Subject: General improvements, CLI initial implementation --- src/card.py | 3 +++ src/cli.py | 0 src/flashcard_cli.py | 33 +++++++++++++++++++++++++++++++++ src/parser.py | 20 +++++++++++++------- src/parser_unittest.py | 10 +++------- src/scheduler.py | 30 ++++++++++++++++++++++++++++++ src/scheduler_brutal.py | 3 ++- 7 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 src/cli.py create mode 100644 src/flashcard_cli.py create mode 100644 src/scheduler.py diff --git a/src/card.py b/src/card.py index c4f7541..9f888e6 100644 --- a/src/card.py +++ b/src/card.py @@ -5,6 +5,9 @@ Defines a struct representing a single card. The struct takes the form: """ from collections import namedtuple +from hashlib import md5 Card = namedtuple('Card', ['front', 'back']) +def getId(card: Card) -> int: + return int(md5((card.front + card.back).encode("utf-8")).hexdigest(), 16) diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..e69de29 diff --git a/src/flashcard_cli.py b/src/flashcard_cli.py new file mode 100644 index 0000000..9e9633d --- /dev/null +++ b/src/flashcard_cli.py @@ -0,0 +1,33 @@ +import click +from json import load +from os.path import isfile + +from card import Card +from scheduler import getSchedulerClass, SCHEDULERS +from parser import parseFiles + + +@click.group() +def cli(): + pass + +@cli.command() +@click.option("--scheduler", "scheduler_name", default="brutal", type=click.Choice(SCHEDULERS, case_sensitive=False)) +@click.argument("state_file", nargs=1, type=click.Path()) +@click.argument("card_files", nargs=-1, type=click.Path(exists=True)) +def practice(scheduler_name, state_file, card_files): + """ + Run a practice session with the specified scheduler, using the provided state and card files. + """ + cards = parseFiles(card_files) + state = json.load(state_file) if isfile(state_file) else {} + + scheduler = getSchedulerClass(scheduler_name)(cards, state) + + + + + +if __name__ == '__main__': + cli() + diff --git a/src/parser.py b/src/parser.py index d2b4ce7..ba7d247 100644 --- a/src/parser.py +++ b/src/parser.py @@ -30,13 +30,13 @@ id: card.Card from pathlib import Path from enum import Enum from typing import TextIO, Iterator -from card import Card +from card import Card, getId def _getCard(front_lines: list[str], back_lines: list[str]) -> tuple[int, Card]: front_text = "".join(front_lines).strip() back_text = "".join(back_lines).strip() card = Card(front_text, back_text) - id = hash(card) + id = getId(card) return id, card def _getCards(f: TextIO) -> Iterator[tuple[id, Card]]: @@ -90,13 +90,19 @@ def _getCards(f: TextIO) -> Iterator[tuple[id, Card]]: yield _getCard(front_lines, back_lines) -def parse(path: Path) -> dict[int, Card]: +def parseFile(path: str) -> dict[int, Card]: """ Parse a .fcard file and return a dictionary of Card instances indexed by ID. """ - if not path.is_file(): - print(f"[Warning] Not a file: {path}") - return {} - with open(path, "r") as f: return {id : card for id, card in _getCards(f)} + +def parseFiles(paths: list[str]) -> dict[int, Card]: + """ + Parse a list of .fcard files and return a dictionary of Card instances indexed by ID. + """ + cards = {} + for p in paths: + cards |= parseFile(p) + return cards + diff --git a/src/parser_unittest.py b/src/parser_unittest.py index 9ada265..8d0d600 100644 --- a/src/parser_unittest.py +++ b/src/parser_unittest.py @@ -41,7 +41,7 @@ Another back with open(path, "w") as f: f.write(file_contents) - cards = parser.parse(path) + cards = parser.parseFile(path) assert expected == set(cards.values()) @@ -51,11 +51,7 @@ def test_emptyFile(tmp_path): with open(path, "w") as f: f.write("") - cards = parser.parse(path) - assert cards == {} - -def test_missingFile(tmp_path): - cards = parser.parse(tmp_path / "missing_file.fcard") + cards = parser.parseFile(path) assert cards == {} def checkException(tmp_path, file_contents): @@ -64,7 +60,7 @@ def checkException(tmp_path, file_contents): f.write(file_contents) with pytest.raises(Exception): - cards = parser.parse(path) + cards = parser.parseFile(path) def test_doesNotStartWithFront(tmp_path): checkException(tmp_path, "BACK\noops") diff --git a/src/scheduler.py b/src/scheduler.py new file mode 100644 index 0000000..67e204a --- /dev/null +++ b/src/scheduler.py @@ -0,0 +1,30 @@ +from typing import Protocol +from abc import abstractmethod +from card import Card + +class Scheduler(Protocol): + @abstractmethod + def __init__(self, cards: dict[int, Card], state: dict): raise NotImplementedError + + @abstractmethod + def practice(self, size: int) -> list[int]: raise NotImplementedError + + @abstractmethod + def test(self, size: int) -> list[int]: raise NotImplementedError + + @abstractmethod + def update(self, results: dict[int, int]) -> None: raise NotImplementedError + + @abstractmethod + def getState(self) -> dict: raise NotImplementedError + +SCHEDULERS = ["brutal"] + +def getSchedulerClass(name: str) -> Scheduler: + match name: + case "brutal": + from scheduler_brutal import SchedulerBrutal + return SchedulerBrutal + case _: + raise Exception(f"Unknown scheduler: {name}") + diff --git a/src/scheduler_brutal.py b/src/scheduler_brutal.py index f41d203..e4e7ee2 100644 --- a/src/scheduler_brutal.py +++ b/src/scheduler_brutal.py @@ -1,12 +1,13 @@ """ """ +from scheduler import Scheduler from card import Card from random import shuffle HISTORY_DEPTH = 8 -class SchedulerBrutal: +class SchedulerBrutal(Scheduler): def __init__(self, cards: dict[int, Card], state: dict): self._cards = cards self._state = {} -- cgit v1.2.3