346 lines
11 KiB
Python
346 lines
11 KiB
Python
import uuid
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import (Dict, Generic, List, Optional, Protocol, Set, Tuple,
|
|
TypeVar, Union, overload, runtime_checkable)
|
|
|
|
from option import Err, 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 cat, val in tags:
|
|
tag_dict.setdefault(cat, set()).add(val)
|
|
return Tags(tag_dict)
|
|
|
|
@overload
|
|
def has_tag(self, category: C, value: V) -> bool: ...
|
|
|
|
@overload
|
|
def has_tag(self, category: Tuple[C, V]) -> bool: ...
|
|
|
|
def has_tag(
|
|
self, category: Union[C, Tuple[C, V]], value: Optional[V] = None
|
|
) -> bool:
|
|
if isinstance(category, tuple):
|
|
cat, val = category
|
|
return val in self._dict.get(cat, set())
|
|
else:
|
|
assert (
|
|
value is not None
|
|
), "Value must be provided if category is not a tuple"
|
|
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 __str__(self) -> str:
|
|
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 = sorted(
|
|
[v.value if isinstance(v, Enum) else str(v) for v in values]
|
|
)
|
|
lines.append(f"{cat_str}: {', '.join(val_strs)}")
|
|
return "\n".join(lines)
|
|
|
|
def __iter__(self):
|
|
for category, values in self._dict.items():
|
|
for value in values:
|
|
yield category, value
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class QuizTaskGeneratorMetadata(Generic[C, V]):
|
|
name: Option[str] = Option.maybe(None)
|
|
description: Option[str] = Option.maybe(None)
|
|
id: uuid.UUID = uuid.uuid4()
|
|
|
|
@staticmethod
|
|
def from_values(
|
|
name: Optional[str] = None,
|
|
description: Optional[str] = None,
|
|
) -> "QuizTaskGeneratorMetadata[C, V]":
|
|
return QuizTaskGeneratorMetadata(
|
|
name=Option.maybe(name),
|
|
description=Option.maybe(description),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class QuizTask(Generic[C, V]):
|
|
question: str
|
|
answer: str
|
|
tags: Tags[C, V] = Tags[C, V]()
|
|
generator_metadata: QuizTaskGeneratorMetadata[C, V] = QuizTaskGeneratorMetadata[
|
|
C, V
|
|
].from_values()
|
|
id: uuid.UUID = uuid.uuid4()
|
|
|
|
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 isinstance(tags, List):
|
|
self.tags = Tags[C, V].from_list(tags)
|
|
elif isinstance(tags, Tags):
|
|
self.tags = tags
|
|
|
|
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), "{}"))}"
|
|
|
|
|
|
@runtime_checkable
|
|
class TaskGenerator(Protocol, Generic[C, V]):
|
|
metadata: QuizTaskGeneratorMetadata[C, V]
|
|
default_amount: Option[int] = Option.maybe(None)
|
|
|
|
def generate(self) -> QuizTask[C, V]: ...
|
|
|
|
|
|
class TaskGeneratorPool(List[TaskGenerator[C, V]], Generic[C, V]): ...
|
|
|
|
|
|
@dataclass
|
|
class TaskPool(List[QuizTask[C, V]], Generic[C, V]): ...
|
|
|
|
|
|
class QuizVariant[C, V]:
|
|
tasks: list[QuizTask[C, V]]
|
|
|
|
|
|
type VariantList[C, V] = List[QuizVariant[C, V]]
|
|
|
|
|
|
@runtime_checkable
|
|
class TaskStaticConstraint(Protocol[C, V]):
|
|
# dull func to distinct dynamic and static types
|
|
def _sta(self):
|
|
return None
|
|
|
|
def is_satisfied(self, task: QuizTask[C, V]) -> bool: ...
|
|
|
|
def negative(self) -> "TaskStaticConstraint[C, V]":
|
|
self.is_satisfied = lambda task: not self.is_satisfied(task)
|
|
return self
|
|
|
|
type TaskOrGenerator[C, V] = Union[TaskGenerator[C, V], QuizTask[C, V]]
|
|
|
|
class MustBeAnyConstrain(TaskStaticConstraint[C, V]):
|
|
must_be_generated_by: List[TaskGenerator[C, V]] = []
|
|
must_be: List[QuizTask[C, V]] = []
|
|
|
|
@overload
|
|
def __init__(self, item: List[TaskOrGenerator[C, V]]):
|
|
...
|
|
|
|
@overload
|
|
def __init__(self, item: TaskOrGenerator[C, V], **kwargs: TaskOrGenerator[C, V]):
|
|
...
|
|
|
|
def __init__(self, item: Union[List[TaskOrGenerator[C, V]], TaskOrGenerator[C, V]], **kwargs: Union[TaskGenerator[C, V], QuizTask[C, V]]):
|
|
all_items = []
|
|
if isinstance(item, List):
|
|
all_items.extend(item)
|
|
else:
|
|
all_items.append(item)
|
|
all_items.extend(kwargs.values())
|
|
self.must_be_generated_by = [
|
|
v for v in all_items if isinstance(v, TaskGenerator)
|
|
]
|
|
self.must_be = [v for v in all_items if isinstance(v, QuizTask)]
|
|
|
|
def is_satisfied(self, task: QuizTask[C, V]) -> bool:
|
|
return any(
|
|
[
|
|
task.generator_metadata.id == g.metadata.id
|
|
for g in self.must_be_generated_by
|
|
]
|
|
) or any([task.id == t.id for t in self.must_be])
|
|
|
|
|
|
class MustIncludeAnyTag(TaskStaticConstraint[C, V]):
|
|
def __init__(self, tags: Tags[C, V]):
|
|
self.tag_to_be_included = tags
|
|
|
|
def is_satisfied(self, task: QuizTask[C, V]) -> bool:
|
|
return any(task.tags.has_tag(tag) for tag in self.tag_to_be_included)
|
|
|
|
|
|
class MustIncludeAllTags(TaskStaticConstraint[C, V]):
|
|
def __init__(self, tags: Tags[C, V]):
|
|
self.tags_to_be_included = tags
|
|
|
|
def is_satisfied(self, task: QuizTask[C, V]) -> bool:
|
|
return all(task.tags.has_tag(tag) for tag in self.tags_to_be_included)
|
|
|
|
|
|
class MustNotStatic(TaskStaticConstraint[C, V]):
|
|
def __init__(self, constrain: TaskStaticConstraint[C, V]):
|
|
self.constrain = constrain
|
|
|
|
def is_satisfied(self, task: QuizTask[C, V]) -> bool:
|
|
return not self.constrain.is_satisfied(task)
|
|
|
|
|
|
@runtime_checkable
|
|
class TaskDynamicConstraint(Protocol[C, V]):
|
|
# dull func to distinct dynamic and static types
|
|
def _dyn(self):
|
|
return None
|
|
|
|
def is_satisfied(
|
|
self,
|
|
task: QuizTask[C, V],
|
|
variant_set: VariantList[C, V],
|
|
current_variant: QuizVariant[C, V],
|
|
) -> bool: ...
|
|
|
|
|
|
class VariantConstraint(Protocol[C, V]):
|
|
def check_dynamic_criteria(self, variant_set: VariantList[C, V]) -> bool: ...
|
|
|
|
|
|
class VariantTask(Generic[C, V]):
|
|
static_constrains: List[TaskStaticConstraint[C, V]] = []
|
|
dynamic_constrains: List[TaskDynamicConstraint[C, V]] = []
|
|
|
|
class Not:
|
|
def __init__(self, variant_task: "VariantTask[C, V]"):
|
|
self._variant_task = variant_task
|
|
|
|
def add_constrain(
|
|
self, constrain: Union[TaskStaticConstraint[C, V], TaskDynamicConstraint[C, V]]
|
|
):
|
|
if isinstance(constrain, TaskStaticConstraint):
|
|
self.static_constrains.append(constrain)
|
|
elif isinstance(constrain, TaskDynamicConstraint):
|
|
self.dynamic_constrains.append(constrain)
|
|
|
|
@overload
|
|
def must_be_any(
|
|
self, items: List[Union[TaskGenerator[C, V], QuizTask[C, V]]]
|
|
) -> "VariantTask[C, V]": ...
|
|
|
|
@overload
|
|
def must_be_any(
|
|
self,
|
|
**kwargs: Union[TaskGenerator[C, V], QuizTask[C, V]],
|
|
) -> "VariantTask[C, V]": ...
|
|
|
|
def must_be_any(
|
|
self,
|
|
items: Union[
|
|
List[Union[TaskGenerator[C, V], QuizTask[C, V]]],
|
|
TaskGenerator[C, V],
|
|
QuizTask[C, V],
|
|
],
|
|
**kwargs: Union[TaskGenerator[C, V], QuizTask[C, V]],
|
|
) -> "VariantTask[C, V]":
|
|
if items:
|
|
self.add_constrain(MustBeAnyConstrain(*items))
|
|
elif kwargs:
|
|
self.add_constrain(MustBeAnyConstrain(**kwargs))
|
|
return self
|
|
|
|
@overload
|
|
def must_include_any_tag(self, tags: Tags[C, V]) -> "VariantTask[C, V]": ...
|
|
|
|
@overload
|
|
def must_include_any_tag(self, tags: Tuple[C, V], **kwargs: Tuple[C, V]) -> "VariantTask[C, V]": ...
|
|
|
|
def must_include_any_tag(
|
|
self, tags: Union[Tags[C, V], Tuple[C, V]], **kwargs: Tuple[C, V]
|
|
) -> "VariantTask[C, V]":
|
|
if isinstance(tags, Tuple):
|
|
self.add_constrain(MustIncludeAnyTag(Tags[C, V].from_list([tags])))
|
|
elif isinstance(tags, Tags):
|
|
self.add_constrain(MustIncludeAnyTag(tags))
|
|
if kwargs:
|
|
self.add_constrain(
|
|
MustIncludeAnyTag(Tags[C, V].from_list(list(kwargs.values())))
|
|
)
|
|
return self
|
|
|
|
|
|
def must_include_tag(self, tag: Tuple[C, V]) -> "VariantTask[C, V]":
|
|
return self.must_include_any_tag(tag)
|
|
|
|
@overload
|
|
def must_include_all_tags(self, tags: Tags[C, V]) -> "VariantTask[C, V]": ...
|
|
|
|
@overload
|
|
def must_include_all_tags(self, tags: Tuple[C, V], **kwargs: Tuple[C, V]) -> "VariantTask[C, V]": ...
|
|
|
|
def must_include_all_tags(
|
|
self, tags: Union[Tags[C, V], Tuple[C, V]], **kwargs: Tuple[C, V]
|
|
) -> "VariantTask[C, V]":
|
|
if isinstance(tags, List):
|
|
self.add_constrain(MustIncludeAllTags(Tags[C, V].from_list(tags)))
|
|
elif isinstance(tags, Tags):
|
|
self.add_constrain(MustIncludeAllTags(tags))
|
|
if kwargs:
|
|
self.add_constrain(
|
|
MustIncludeAllTags(Tags[C, V].from_list(list(kwargs.values())))
|
|
)
|
|
return self
|
|
|
|
vt = VariantTask()
|
|
|
|
vt.must_be_any()
|
|
|
|
|
|
class OrderedVariantGenerator(Generic[C, V]):
|
|
tasks: List[VariantTask[C, V]]
|
|
|
|
def __init__(
|
|
self, number_of_tasks: int, task_generator_pool: TaskGeneratorPool[C, V]
|
|
):
|
|
self.tasks = [VariantTask[C, V]() for _ in range(number_of_tasks)]
|
|
self.task_generator_pool = task_generator_pool
|
|
|
|
|
|
class Quizard(Generic[C, V]):
|
|
taskPool = TaskPool[C, V]()
|
|
|
|
def __init__(self, taskGenerators: List[TaskGenerator[C, V]], default_amount: int):
|
|
self.taskGenerators = taskGenerators
|
|
self.default_amount = default_amount
|
|
|
|
def fillTaskPool(self):
|
|
for taskGenerator in self.taskGenerators:
|
|
n = (
|
|
self.default_amount
|
|
if taskGenerator.default_amount.is_none
|
|
else taskGenerator.default_amount.value
|
|
)
|
|
for _ in range(n):
|
|
task = taskGenerator.generate()
|
|
task.generator_metadata = taskGenerator.metadata
|
|
self.taskPool.append(task)
|
|
|
|
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 in the pool")
|
|
|
|
return Err("Not implemented")
|