from dataclasses import dataclass, field from enum import Enum from typing import ( Dict, Generic, List, Optional, Protocol, Set, Tuple, TypeVar, Union, cast, ) from option import NONE, Err, Ok, Option, Result, Some C = TypeVar("C", default=str) V = TypeVar("V", default=str) @dataclass class Tags(Generic[C, V]): _dict: Dict[C, Set[V]] = field(default_factory=dict) @staticmethod def from_list(tags: List[Tuple[C, V]]) -> "Tags[C, V]": tag_dict: Dict[C, Set[V]] = {} for tag in tags: tag_dict.setdefault(tag[0], set()).add(tag[1]) return Tags(tag_dict) def has_tag(self, category: C, value: V) -> bool: return value in self._dict.get(category, set()) def add_tag(self, category: C, value: V) -> None: self._dict.setdefault(category, set()).add(value) def remove_tag(self, category: C, value: V) -> None: self._dict.get(category, set()).discard(value) def __str__(self): if len(self._dict) == 0: return "No tags" lines = [] for category, values in self._dict.items(): cat_str = category.value if isinstance(category, Enum) else str(category) val_strs = [v.value if isinstance(v, Enum) else str(v) for v in values] value_list = ", ".join(str(v) for v in sorted(val_strs)) lines.append(f"{cat_str}: {value_list}") return "\n".join(lines) def indent(text: str, spaces: int = 2) -> str: prefix = " " * spaces return "\n".join(prefix + line for line in text.splitlines()) @dataclass class QuizTask(Generic[C, V]): question: str answer: str tags: Option[Tags[C, V]] def __init__( self, question: str, answer: str, tags: Optional[Union[Tags[C, V], List[Tuple[C, V]]]] = None, ): self.question = question self.answer = answer if tags is None: self.tags = cast(Option[Tags[C, V]], NONE) elif isinstance(tags, list): self.tags = Some(Tags[C, V].from_list(tags)) else: self.tags = Some(tags) def has_tag(self, category: C, value: V) -> bool: return self.tags.map_or(lambda t: t.has_tag(category, value), False) def __str__(self): return f"Question:\n{indent(self.question)}\nAnswer:\n{indent(self.answer)}\nTags:\n{indent(self.tags.map_or(lambda t: str(t), "{}"))}" class TaskGenerator(Protocol, Generic[C, V]): def generate(self) -> QuizTask[C, V]: ... @dataclass class TaskPool(Generic[C, V]): tasks: list[TaskGenerator[C, V]] def __len__(self): return len(self.tasks) @dataclass class QuizVariant: tasks: list[QuizTask] @dataclass class VariantSet: variants: list[QuizVariant] class Quizard(Generic[C, V]): def shuffle_tasks_to_variants( self, tasks: TaskPool[C, V], number_of_variants: int, seed: int = 0 ) -> Result[VariantSet, str]: if len(tasks) == 0: return Err("There must be at least one task") return Err("Not implemented")