import uuid from ..models.flashcard import Flashcard, FlashcardEvent from ...outbound.postgres.repositories.flashcard_repository import FlashcardRepository from ...outbound.postgres.repositories.vocab_repository import VocabRepository from ...outbound.postgres.repositories.dictionary_repository import DictionaryRepository VALID_DIRECTIONS = {"target_to_en", "en_to_target"} VALID_EVENT_TYPES = {"shown", "answered", "skipped"} class FlashcardService: """Generates flashcards from resolved vocab bank entries and records study events. Flashcard text is derived directly from the dictionary: the lemma headword is the target-language side and the sense gloss is the English side. Both directions are created by default. Usage:: service = FlashcardService( flashcard_repo=PostgresFlashcardRepository(db), vocab_repo=PostgresVocabRepository(db), dict_repo=PostgresDictionaryRepository(db), ) # Generate both directions for a resolved bank entry cards = await service.generate_flashcard_from_entry(entry_id) # Record that the user answered correctly event = await service.record_flashcard_event( flashcard_id=cards[0].id, user_id=user_id, event_type="answered", response="banque", ) """ def __init__( self, flashcard_repo: FlashcardRepository, vocab_repo: VocabRepository, dict_repo: DictionaryRepository, ) -> None: self.flashcard_repo = flashcard_repo self.vocab_repo = vocab_repo self.dict_repo = dict_repo async def generate_flashcard_from_entry( self, entry_id: uuid.UUID, direction: str | None = None, ) -> list[Flashcard]: """Create flashcard(s) from a vocab bank entry that has a resolved sense. Looks up the sense gloss (English meaning) and lemma headword (target-language word) and creates one card per direction. Pass ``direction`` to generate only ``"target_to_en"`` or ``"en_to_target"``; omit it to create both. Raises ``ValueError`` if the entry does not exist, has no resolved sense, or if the underlying sense or lemma rows cannot be found in the dictionary. Usage:: # Both directions — typical case cards = await service.generate_flashcard_from_entry(entry_id) assert len(cards) == 2 # One direction only cards = await service.generate_flashcard_from_entry( entry_id, direction="target_to_en" ) """ if direction is not None and direction not in VALID_DIRECTIONS: raise ValueError(f"Invalid direction '{direction}'. Must be one of {VALID_DIRECTIONS}") entry = await self.vocab_repo.get_entry(entry_id) if entry is None: raise ValueError(f"Bank entry {entry_id} not found") if entry.sense_id is None: raise ValueError( "Entry has no resolved sense; disambiguate before generating flashcards" ) sense = await self.dict_repo.get_sense(uuid.UUID(entry.sense_id)) if sense is None: raise ValueError(f"Sense {entry.sense_id} not found in dictionary") lemma = await self.dict_repo.get_lemma(uuid.UUID(sense.lemma_id)) if lemma is None: raise ValueError(f"Lemma for sense {entry.sense_id} not found in dictionary") pair = await self.vocab_repo.get_language_pair(uuid.UUID(entry.language_pair_id)) if pair is None: raise ValueError(f"Language pair {entry.language_pair_id} not found") user_id = uuid.UUID(entry.user_id) directions = [direction] if direction else ["target_to_en", "en_to_target"] flashcards = [] for d in directions: if d == "target_to_en": prompt, answer = lemma.headword, sense.gloss else: prompt, answer = sense.gloss, lemma.headword card = await self.flashcard_repo.create_flashcard( user_id=user_id, bank_entry_id=entry_id, source_lang=pair.source_lang, target_lang=pair.target_lang, prompt_text=prompt, answer_text=answer, card_direction=d, ) flashcards.append(card) return flashcards async def record_flashcard_event( self, flashcard_id: uuid.UUID, user_id: uuid.UUID, event_type: str, response: str | None = None, ) -> FlashcardEvent: """Record a study event against a flashcard — shown, answered, or skipped. ``response`` is the user's free-text answer and is only meaningful for ``event_type="answered"``; it is stored as-is without grading. Raises ``ValueError`` for unrecognised event types. Usage:: event = await service.record_flashcard_event( flashcard_id=card.id, user_id=user_id, event_type="answered", response="banque", ) """ if event_type not in VALID_EVENT_TYPES: raise ValueError( f"Invalid event_type '{event_type}'. Must be one of {VALID_EVENT_TYPES}" ) return await self.flashcard_repo.record_event( flashcard_id=flashcard_id, user_id=user_id, event_type=event_type, user_response=response, )