MIL-003: Step 2 - New flash cards

Review record RC-025 (Go, autonomous mode, not independent).

Task: MIL-003#1
Task: MIL-003#2
Task: MIL-003#3
Task: MIL-003#4
Task: MIL-003#5
This commit is contained in:
2026-10-14 21:59:29 +08:00
parent c6c737ce40
commit 8d66c43eea
36 changed files with 1167 additions and 102 deletions
@@ -0,0 +1,5 @@
"""Application data transfer objects.
@file __init__.py
@brief Holds the plain data that crosses to the presentation layer.
"""
@@ -0,0 +1,21 @@
"""Card data for the presentation layer.
@file card_dto.py
@brief The CardDto: a card as plain text.
"""
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class CardDto:
"""A card as plain data."""
## @brief Word on the card front.
front_text: str
## @brief Translation on the card back.
back_text: str
## @brief Language code of the front.
front_language: str
## @brief Language code of the back.
back_language: str
@@ -0,0 +1,5 @@
"""Application use cases.
@file __init__.py
@brief Holds the use cases of the flash card program.
"""
@@ -0,0 +1,27 @@
"""Start a study session.
@file open_study_session.py
@brief The open_study_session use case.
"""
from collections.abc import Callable, Sequence
from flash_card.application.use_cases.study_session import StudySession
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
from flash_card.domain.repositories.vocabulary_repository import VocabularyRepository
def open_study_session(
vocabulary_repository: VocabularyRepository,
choose: Callable[[Sequence[Card]], Card],
) -> StudySession:
"""Load the word list and build a session over all its cards.
@param vocabulary_repository Source of the word list.
@param choose Picks one card from a non-empty sequence.
@return The new session.
@throws VocabularyError When the word list cannot be loaded.
"""
deck = Deck.from_vocabulary(vocabulary_repository.load())
return StudySession(deck, choose)
@@ -0,0 +1,66 @@
"""One study session.
@file study_session.py
@brief The StudySession use case: draws the cards the user studies.
"""
from collections.abc import Callable, Sequence
from flash_card.application.dto.card_dto import CardDto
from flash_card.domain.entities.card import Card
from flash_card.domain.entities.deck import Deck
class StudySession:
"""Draws cards from a deck, one after another."""
def __init__(self, deck: Deck, choose: Callable[[Sequence[Card]], Card]) -> None:
"""Create a session.
@param deck The cards still to learn.
@param choose Picks one card from a non-empty sequence.
"""
## @brief The cards still to learn.
self._deck = deck
## @brief Picks one card from a non-empty sequence.
self._choose = choose
## @brief The card shown now, or None.
self._current: Card | None = None
@property
def current_card(self) -> CardDto | None:
"""Return the card shown now.
@return The card, or None before the first draw or when none is left.
"""
return self._to_dto(self._current)
@property
def remaining(self) -> int:
"""Return the number of cards still to learn.
@return The card count.
"""
return len(self._deck)
def next_card(self) -> CardDto | None:
"""Draw the next card, different from the current one when possible.
@return The new card, or None when the deck is empty.
"""
if self._deck.is_empty:
self._current = None
return None
self._current = self._deck.draw(self._choose, exclude=self._current)
return self._to_dto(self._current)
def _to_dto(self, card: Card | None) -> CardDto | None:
"""Map a card to its DTO.
@param card The card, or None.
@return The DTO, or None when there is no card.
"""
if card is None:
return None
pair = self._deck.language_pair
return CardDto(card.front, card.back, pair.front_code, pair.back_code)