117 lines
2.9 KiB
Python
117 lines
2.9 KiB
Python
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
|
|
|
|
from utils.utils import indent
|
|
|
|
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)
|
|
|
|
|
|
@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")
|