diff --git a/QuizTask.py b/QuizTask.py deleted file mode 100644 index bd5165f..0000000 --- a/QuizTask.py +++ /dev/null @@ -1,28 +0,0 @@ -@dataclass -class QuizTask(Generic[C, V]): - question: str - answer: str - tags: Option[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 = Some(Tags[C, V].from_list(tags)) - else: - self.tags = Option.maybe(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), "{}"))}" diff --git a/Quizard.py b/Quizard.py deleted file mode 100644 index 61e4ea3..0000000 --- a/Quizard.py +++ /dev/null @@ -1,345 +0,0 @@ -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") diff --git a/modules/constrains/__init__.py b/modules/constrains/__init__.py deleted file mode 100644 index 90e677d..0000000 --- a/modules/constrains/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -from dataclasses import dataclass, field -from typing import Generic, Iterable, List, Protocol, Union, cast, overload, override - -from modules.constrains.static import VTaskConstraintStatic -from modules.constrains.static.must_be_any import MustBeAnyConstraint -from modules.constrains.static.must_include_all_tags import MustIncludeAllTagsConstraint -from modules.constrains.static.must_include_any_tag import MustIncludeAnyTagConstraint -from modules.constrains.static.must_not import MustNotStatic -from modules.constrains.types import QTaskOrFactory -from modules.tags import Tag, Tags -from utils.types import A, C, Q, V - - -@dataclass -class VTaskConstraintsAbstract(Generic[C, V, Q, A]): - static_constraints: List[VTaskConstraintStatic[C, V, Q, A]] = field( - default_factory=list - ) - dynamic_constraints: None = None - - def add_constraint( - self, constraint: VTaskConstraintStatic[C, V, Q, A] - ) -> "VTaskConstraints[C, V, Q, A]": - self.static_constraints.append(constraint) - # unsafe, but needed to avoid typing the same thing for two times... - return cast("VTaskConstraints[C, V, Q, A]", self) - - def __call__( - self, constraint: VTaskConstraintStatic[C, V, Q, A] - ) -> "VTaskConstraints[C, V, Q, A]": - return self.add_constraint(constraint) - - @overload - def be_any( - self, item: Iterable[QTaskOrFactory[C, V, Q, A]] - ) -> "VTaskConstraints[C, V, Q, A]": ... - - @overload - def be_any( - self, item: QTaskOrFactory[C, V, Q, A], **kwargs: QTaskOrFactory[C, V, Q, A] - ) -> "VTaskConstraints[C, V, Q, A]": ... - - def be_any( - self, - item: Union[Iterable[QTaskOrFactory[C, V, Q, A]], QTaskOrFactory[C, V, Q, A]], - **kwargs: QTaskOrFabric[C, V, Q, A], - ) -> "VTaskConstraints[C, V, Q, A]": - return self.add_constraint(MustBeAnyConstraint(item, **kwargs)) - - @overload - def include_any_tag(self, tag: Tags[C, V]) -> "VTaskConstraints[C, V, Q, A]": ... - - @overload - def include_any_tag( - self, tag: Iterable[Tag[C, V]] - ) -> "VTaskConstraints[C, V, Q, A]": ... - - @overload - def include_any_tag( - self, tag: Tag[C, V], **kwargs: Tag[C, V] - ) -> "VTaskConstraints[C, V, Q, A]": ... - - def include_any_tag( - self, - tag: Union[Tags[C, V], Tag[C, V], Iterable[Tag[C, V]]], - **kwargs: Tag[C, V], - ) -> "VTaskConstraints[C, V, Q, A]": - return self.add_constraint(MustIncludeAnyTagConstraint(tag, **kwargs)) - - @overload - def include_all_tags(self, tag: Tags[C, V]) -> "VTaskConstraints[C, V, Q, A]": ... - - @overload - def include_all_tags( - self, tag: Iterable[Tag[C, V]] - ) -> "VTaskConstraints[C, V, Q, A]": ... - - @overload - def include_all_tags( - self, tag: Tag[C, V], **kwargs: Tag[C, V] - ) -> "VTaskConstraints[C, V, Q, A]": ... - - def include_all_tags( - self, - tag: Union[Tags[C, V], Tag[C, V], Iterable[Tag[C, V]]], - **kwargs: Tag[C, V], - ): - return self.add_constraint(MustIncludeAllTagsConstraint(tag, **kwargs)) - - -class VTaskConstraintsNegator(VTaskConstraintsAbstract[C, V, Q, A]): - def __init__(self, v_task_constraints: "VTaskConstraints[C, V, Q, A]"): - self.v_task_constraints = v_task_constraints - - def add_constraint( - self, constraint: VTaskConstraintStatic[C, V, Q, A] - ) -> "VTaskConstraints[C, V, Q, A]": - return self.v_task_constraints.add_constraint(MustNotStatic(constraint)) - - -@dataclass -class VTaskConstraints(VTaskConstraintsAbstract[C, V, Q, A]): - nt: VTaskConstraintsNegator[C, V, Q, A] = field(init=False) - - def __post_init__(self): - self.nt = VTaskConstraintsNegator(self) diff --git a/modules/constrains/dynamic/__init__.py b/modules/constrains/dynamic/__init__.py deleted file mode 100644 index 0e48d11..0000000 --- a/modules/constrains/dynamic/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Protocol, runtime_checkable - -from modules.task import QTask -from modules.task_pool import QTaskPool -from modules.variant import QVariant -from modules.variant_set import QVariantSet -from utils.types import A, C, Q, V - - -@runtime_checkable -class VTaskConstraintDynamic(Protocol[C, V, Q, A]): - def _dyn(self): - return None - - def check_if_satisfied( - self, - task: QTask[C, V, Q, A], - task_pool: QTaskPool[C, V, Q, A], - previous_variants: QVariantSet[C, V, Q, A], - current_variant: QVariant[C, V, Q, A], - ) -> bool: ... diff --git a/modules/constrains/static/__init__.py b/modules/constrains/static/__init__.py deleted file mode 100644 index cbf6297..0000000 --- a/modules/constrains/static/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Protocol, runtime_checkable - -from modules.task import QTask -from utils.types import A, C, Q, V - - -@runtime_checkable -class VTaskConstraintStatic(Protocol[C, V, Q, A]): - # dull func to distinct dynamic and static types - def _sta(self): - return None - - def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: ... diff --git a/modules/constrains/static/must_be_any.py b/modules/constrains/static/must_be_any.py deleted file mode 100644 index 1efb2c6..0000000 --- a/modules/constrains/static/must_be_any.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Iterable, List, Union, overload - -from modules.constrains.static import VTaskStaticConstraint -from modules.constrains.types import QTaskOrFabric -from modules.fabric import QTaskFabric -from modules.task import QTask -from utils.types import A, C, Q, V - - -class MustBeAnyConstraint(VTaskStaticConstraint[C, V, Q, A]): - must_be_generated_by: List[QTaskFabric[C, V, Q, A]] = [] - must_be_one_of_tasks: List[QTask[C, V, Q, A]] = [] - - @overload - def __init__(self, item: Iterable[QTaskOrFabric[C, V, Q, A]]): ... - - @overload - def __init__( - self, item: QTaskOrFabric[C, V, Q, A], **kwargs: QTaskOrFabric[C, V, Q, A] - ): ... - - def __init__( - self, - item: Union[Iterable[QTaskOrFabric[C, V, Q, A]], QTaskOrFabric[C, V, Q, A]], - **kwargs: QTaskOrFabric[C, V, Q, A], - ): - 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, QTaskFabric)] - self.must_be_one_of_tasks = [v for v in all_items if isinstance(v, QTask)] - - def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: - return any( - [ - task.fabric_metadata.unwrap_or(None) == g.metadata.id - for g in self.must_be_generated_by - ] - ) or any([task.id == t.id for t in self.must_be_one_of_tasks]) diff --git a/modules/constrains/static/must_include_all_tags.py b/modules/constrains/static/must_include_all_tags.py deleted file mode 100644 index 889ba29..0000000 --- a/modules/constrains/static/must_include_all_tags.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Iterable, Tuple, Union, overload - -from modules.constrains.static import VTaskConstraintStatic -from modules.tags import Tag, Tags -from modules.task import QTask -from utils.types import A, C, Q, V - - -class MustIncludeAllTagsConstraint(VTaskConstraintStatic[C, V, Q, A]): - tags: Tags[C, V] = Tags() - - @overload - def __init__(self, tag: Tags[C, V]): ... - - @overload - def __init__(self, tag: Iterable[Tag[C, V]]) - - @overload - def __init__(self, tag: Tag[C, V], **kwargs: Tag[C, V]): ... - - def __init__( - self, - tag: Union[Tags[C, V], Tag[C, V], Iterable[Tag[C, V]]], - **kwargs: Tag[C, V], - ): - - if isinstance(tag, Tags): - self.tags = tag - elif isinstance(tag, Iterable): - self.tags = Tags[C, V].from_iter(tag) - else: - self.tags = Tags[C, V].from_iter(kwargs.values()) - if isinstance(tag, Tag) - self.tags.add_tag(tag) - - def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: - return all(task.tags.has_tag(tag) for tag in self.tags) diff --git a/modules/constrains/static/must_include_any_tag.py b/modules/constrains/static/must_include_any_tag.py deleted file mode 100644 index ffea0c9..0000000 --- a/modules/constrains/static/must_include_any_tag.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Iterable, List, Tuple, Union, overload - -from modules.constrains.static import VTaskStaticConstraint -from modules.tags import Tag, Tags -from modules.task import QTask -from utils.types import A, C, Q, V - - -class MustIncludeAnyTagConstraint(VTaskStaticConstraint[C, V, Q, A]): - tags: Tags[C, V] = Tags() - - @overload - def __init__(self, tag: Tags[C, V]): ... - - @overload - def __init__(self, tag: Iterable[Tag[C, V]]) - - @overload - def __init__(self, tag: Tag[C, V], **kwargs: Tag[C, V]): ... - - def __init__( - self, - tag: Union[Tags[C, V], Tag[C, V], Iterable[Tag[C, V]]], - **kwargs: Tag[C, V], - ): - - if isinstance(tag, Tags): - self.tags = tag - elif isinstance(tag, Iterable): - self.tags = Tags[C, V].from_iter(tag) - else: - self.tags = Tags[C, V].from_iter(kwargs.values()) - if isinstance(tag, Tag) - self.tags.add_tag(tag) - - def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: - return any(task.tags.has_tag(tag) for tag in self.tags) diff --git a/modules/constrains/static/must_not.py b/modules/constrains/static/must_not.py deleted file mode 100644 index bcbddb6..0000000 --- a/modules/constrains/static/must_not.py +++ /dev/null @@ -1,13 +0,0 @@ -from dataclasses import dataclass - -from modules.constrains.static import VTaskStaticConstraint -from modules.task import QTask -from utils.types import A, C, Q, V - - -@dataclass -class MustNotStatic(VTaskStaticConstraint[C, V, Q, A]): - constraint: VTaskStaticConstraint[C, V, Q, A] - - def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: - return not self.constraint.is_satisfied(task) diff --git a/modules/constrains/types.py b/modules/constrains/types.py deleted file mode 100644 index 39cfaec..0000000 --- a/modules/constrains/types.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Union - -from modules.fabric import QTaskFabric -from modules.task import QTask - -type QTaskOrFabric[C, V, Q, A] = Union[QTask[C, V, Q, A], QTaskFabric[C, V, Q, A]] diff --git a/modules/fabric.py b/modules/fabric.py deleted file mode 100644 index 025fbc8..0000000 --- a/modules/fabric.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Generic, Protocol, runtime_checkable - -from option import Option - -from modules.fabric_metadata import QTaskFactoryMetadata -from modules.task import QTask -from utils.types import A, C, Q, V - - -@runtime_checkable -class QTaskFactory(Protocol, Generic[C, V, Q, A]): - metadata: QTaskFactoryMetadata[C, V] - default_tasks_to_generate: Option[int] = Option.maybe(None) - - def generate(self) -> QTask[C, V, Q, A]: ... diff --git a/modules/fabric_metadata.py b/modules/fabric_metadata.py deleted file mode 100644 index b5d2651..0000000 --- a/modules/fabric_metadata.py +++ /dev/null @@ -1,24 +0,0 @@ -import uuid -from dataclasses import dataclass, field -from typing import Generic, Optional - -from option import Option - -from utils.utils import C, V - - -@dataclass(frozen=True) -class QTaskFactoryMetadata(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, - ) -> "QTaskFactoryMetadata[C, V]": - return QTaskFactoryMetadata( - name=Option.maybe(name), - description=Option.maybe(description), - ) diff --git a/modules/quizgen.py b/modules/quizgen.py new file mode 100644 index 0000000..35d99e2 --- /dev/null +++ b/modules/quizgen.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Generic + +from modules.utils.types import A, C, Q, V +from modules.variant_builder.task_pool import QTaskPool +from modules.variant_builder.variant_set import QVariantSet + + +@dataclass +class QuizGen(Generic[C, V, Q, A]): + task_pool: QTaskPool[C, V, Q, A] + previos_variants: QVariantSet[C, V, Q, A] diff --git a/modules/tag/__init__.py b/modules/tag/__init__.py new file mode 100644 index 0000000..5a0bc6b --- /dev/null +++ b/modules/tag/__init__.py @@ -0,0 +1,118 @@ +from collections.abc import Iterable +from dataclasses import dataclass, field +from enum import Enum +from typing import Generic, overload, override + +from modules.utils.types import C, V + + +@dataclass(frozen=True) +class Tag(Generic[C, V]): + """ + Represents a single tag composed of a category and a value + + Attributes: + cat (C): The category of the tag + val (V): The value of the tag + """ + + cat: C + val: V + + +@dataclass +class Tags(Generic[C, V]): + """A collection of tags grouped by category + + Attributes: + _dict: (dict[C, set[V]]): Internal dictionary storing tags grouped by category. + """ + + _dict: dict[C, set[V]] = field(default_factory=dict) + + def __init__(self, iter: Iterable[Tag[C, V]] | None = None): + if iter: + for tag in iter: + self._dict.setdefault(tag.cat, set()).add(tag.val) + + @overload + def has_tag(self, category: C, value: V) -> bool: ... + + @overload + def has_tag(self, category: Tag[C, V]) -> bool: ... + + def has_tag(self, category: C | Tag[C, V], value: V | None = None) -> bool: + """Check if a tag exists in the collection. + + Args: + category (C | Tag[C, V]): The category or a full Tag instance. + value (V): The value to check (if category is not a Tag). + + Returns: + bool: True if the tag exists, False otherwise + + Raises: + AssertionError: If value is None and category is not a Tag. + """ + if isinstance(category, Tag): + tag = category + return tag.val in self._dict.get(tag.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()) + + @overload + def add_tag(self, category: C, value: V) -> None: ... + + @overload + def add_tag(self, category: Tag[C, V]) -> None: ... + + def add_tag(self, category: C | Tag[C, V], value: V | None = None) -> None: + """Add a tag to the collection. + + Args: + category (C | Tag[C, V]): The category or full Tag instance. + value (V | None): The value to add (if category is not a Tag). + + Raises: + AssertionError: If value is None and category is not a Tag. + """ + if isinstance(category, Tag): + tag = category + self._dict.get(tag.cat, set()).add(tag.val) + else: + assert ( + value is not None + ), "Value must be provided if category is not a tuple" + self._dict.get(category, set()).add(value) + + @override + def __str__(self) -> str: + """Return a human-readable string representation of the tags. + + Returns: + str: A formatted string listing categories and their values. + """ + if len(self._dict) == 0: + return "No tags" + + lines: list[str] = [] + for category, values in self._dict.items(): + cat_str = str(category.value if isinstance(category, Enum) else category) + val_strs = sorted( + [str(v.value if isinstance(v, Enum) else v for v in values)] + ) + lines.append(f"{cat_str}: {', '.join(val_strs)}") + return "\n".join(lines) + + def __iter__(self): + """Yield all tags in the collection one by one. + + Yields: + Tag[C, V]: Each tag in the collection. + """ + for category, values in self._dict.items(): + for value in values: + yield Tag(category, value) diff --git a/modules/tags.py b/modules/tags.py deleted file mode 100644 index 30da6f6..0000000 --- a/modules/tags.py +++ /dev/null @@ -1,85 +0,0 @@ -from dataclasses import dataclass, field -from enum import Enum -from typing import ( - Dict, - Generic, - Iterable, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, - overload, - override, -) - -from utils.utils import C, V - - -@dataclass(frozen=True) -class Tag(Generic[C, V]): - cat: C - val: V - - -@dataclass -class Tags(Generic[C, V]): - _dict: Dict[C, Set[V]] = {} - - @staticmethod - def from_iter(iter: Iterable[Tag[C, V]]) -> "Tags[C, V]": - tags: Tags[C, V] = Tags() - for tag in iter: - tags._dict.setdefault(tag.cat, set()).add(tag.val) - return tags - - @overload - def has_tag(self, category: C, value: V) -> bool: ... - - @overload - def has_tag(self, category: Tag[C, V]) -> bool: ... - - def has_tag(self, category: Union[C, Tag[C, V]], value: Optional[V] = None) -> bool: - if isinstance(category, Tag): - tag = category - return tag.val in self._dict.get(tag.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()) - - @overload - def add_tag(self, category: C, value: V) -> None: ... - - @overload - def add_tag(self, category: Tag[C, V]) -> None: ... - - def add_tag(self, category: Union[C, Tag[C, V]], value: Optional[V] = None) -> None: - if isinstance(category, Tag): - tag = category - self._dict.get(tag.cat, set()).add(tag.val) - else: - assert ( - value is not None - ), "Value must be provided if category is not a tuple" - self._dict.get(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 Tag(category, value) diff --git a/modules/task.py b/modules/task.py deleted file mode 100644 index 37ee010..0000000 --- a/modules/task.py +++ /dev/null @@ -1,35 +0,0 @@ -import uuid -from dataclasses import dataclass -from typing import Generic, List, Optional, Union - -from option import NONE, Option -from tags import Tag, Tags - -from modules.fabric_metadata import QTaskFactoryMetadata -from utils.types import A, C, Q, V -from utils.utils import indent - - -@dataclass -class QTask(Generic[C, V, Q, A]): - question: Q - answer: A - tags: Tags[C, V] = Tags() - fabric_metadata: Option[QTaskFactoryMetadata[C, V]] = Option.maybe(NONE) - id: uuid.UUID = uuid.uuid4() - - def __init__( - self, - question: Q, - answer: A, - tags: Optional[Union[Tags[C, V], List[Tag[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(str(self.question))}\nAnswer:\n{indent(str(self.answer))}\nTags:\n{indent(str(self.tags))}" diff --git a/modules/task/__init__.py b/modules/task/__init__.py new file mode 100644 index 0000000..0bc683d --- /dev/null +++ b/modules/task/__init__.py @@ -0,0 +1,38 @@ +import uuid +from dataclasses import dataclass +from typing import Generic, override + +from option import Option + +from modules.tag import Tag, Tags +from modules.task.factory.metadata import QTaskFactoryMetadata +from modules.utils.types import A, C, Q, V +from modules.utils.utils import indent + + +@dataclass +class QTask(Generic[C, V, Q, A]): + question: Q + answer: A + tags: Tags[C, V] + factory_metadata: Option[QTaskFactoryMetadata[C, V]] = Option[ + QTaskFactoryMetadata[C, V] + ].maybe(None) + id: uuid.UUID = uuid.uuid4() + + def __init__( + self, + question: Q, + answer: A, + tags: Tags[C, V] | list[Tag[C, V]] | None = None, + ): + self.question = question + self.answer = answer + if isinstance(tags, list): + self.tags = Tags[C, V](tags) + elif isinstance(tags, Tags): + self.tags = tags + + @override + def __str__(self) -> str: + return f"Question:\n{indent(str(self.question))}\nAnswer:\n{indent(str(self.answer))}\nTags:\n{indent(str(self.tags))}" diff --git a/modules/task/factory/__init__.py b/modules/task/factory/__init__.py new file mode 100644 index 0000000..c06e4fa --- /dev/null +++ b/modules/task/factory/__init__.py @@ -0,0 +1,22 @@ +import uuid +from abc import ABC, abstractmethod +from typing import Generic, runtime_checkable + +from option import Option + +from modules.task import QTask +from modules.task.factory.metadata import QTaskFactoryMetadata +from modules.utils.types import A, C, Q, V + + +@runtime_checkable +class QTaskFactory(ABC, Generic[C, V, Q, A]): + id: uuid.UUID + metadata: QTaskFactoryMetadata[C, V] + default_tasks_to_generate: Option[int] = Option[int].maybe(None) + + @abstractmethod + def generate(self) -> QTask[C, V, Q, A]: ... + + def get_id(self) -> uuid.UUID: + return self.metadata.id diff --git a/modules/task/factory/metadata.py b/modules/task/factory/metadata.py new file mode 100644 index 0000000..2fab93d --- /dev/null +++ b/modules/task/factory/metadata.py @@ -0,0 +1,24 @@ +import uuid +from dataclasses import dataclass, field +from typing import Generic + +from option import NONE, Option + +from modules.utils.types import C, V + + +@dataclass(frozen=True) +class QTaskFactoryMetadata(Generic[C, V]): + name: Option[str] = field(default=Option[str].maybe(None)) + description: Option[str] = field(default=Option[str].maybe(None)) + id: uuid.UUID = field(default_factory=uuid.uuid4) + + @staticmethod + def from_values( + name: str | None = None, + description: str | None = None, + ) -> "QTaskFactoryMetadata[C, V]": + return QTaskFactoryMetadata( + name=Option[str].maybe(name), + description=Option[str].maybe(description), + ) diff --git a/modules/task_pool.py b/modules/task_pool.py deleted file mode 100644 index 2f33e15..0000000 --- a/modules/task_pool.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Generic, List - -from task import QTask - -from utils.types import A, C, Q, V - - -class QTaskPool(Generic[C, V, Q, A]): - pool: List[QTask[C, V, Q, A]] diff --git a/utils/__init__.py b/modules/utils/__init__.py similarity index 100% rename from utils/__init__.py rename to modules/utils/__init__.py diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/modules/utils/__pycache__/__init__.cpython-313.pyc similarity index 100% rename from utils/__pycache__/__init__.cpython-313.pyc rename to modules/utils/__pycache__/__init__.cpython-313.pyc diff --git a/utils/__pycache__/utils.cpython-313.pyc b/modules/utils/__pycache__/utils.cpython-313.pyc similarity index 100% rename from utils/__pycache__/utils.cpython-313.pyc rename to modules/utils/__pycache__/utils.cpython-313.pyc diff --git a/utils/option.py b/modules/utils/option.py similarity index 100% rename from utils/option.py rename to modules/utils/option.py diff --git a/utils/types.py b/modules/utils/types.py similarity index 77% rename from utils/types.py rename to modules/utils/types.py index 4f2a02b..a13f1ac 100644 --- a/utils/types.py +++ b/modules/utils/types.py @@ -3,4 +3,4 @@ from typing import TypeVar C = TypeVar("C", default=str) V = TypeVar("V", default=str) Q = TypeVar("Q", default=str) -A = TypeVar("A", default=Q) +A = TypeVar("A", default=Q | None) diff --git a/utils/utils.py b/modules/utils/utils.py similarity index 73% rename from utils/utils.py rename to modules/utils/utils.py index 45d3753..50e3d76 100644 --- a/utils/utils.py +++ b/modules/utils/utils.py @@ -1,8 +1,5 @@ from typing import TypeVar -C = TypeVar("C", default=str) -V = TypeVar("V", default=str) - def indent(text: str, spaces: int = 2) -> str: prefix = " " * spaces diff --git a/modules/variant.py b/modules/variant.py deleted file mode 100644 index b2692b0..0000000 --- a/modules/variant.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Generic, List - -from modules.task import QTask -from utils.types import A, C, Q, V - - -class QVariant(Generic[C, V, Q, A]): - tasks: List[QTask[C, V, Q, A]] - - def __iter__(self): - for task in self.tasks: - yield task diff --git a/modules/variant/__init__.py b/modules/variant/__init__.py new file mode 100644 index 0000000..927f3d6 --- /dev/null +++ b/modules/variant/__init__.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass, field +from typing import Generic + +from modules.task import QTask +from modules.utils.types import A, C, Q, V + + +@dataclass +class QVariant(Generic[C, V, Q, A]): + tasks: list[QTask[C, V, Q, A]] = field(default_factory=list) + + def __iter__(self): + for task in self.tasks: + yield task diff --git a/modules/variant_builder/__init__.py b/modules/variant_builder/__init__.py new file mode 100644 index 0000000..4302334 --- /dev/null +++ b/modules/variant_builder/__init__.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import Generic + +from modules.utils.types import A, C, Q, V +from modules.variant import QVariant +from modules.variant_builder.task_pool import QTaskPool +from modules.variant_builder.task_selector import QTaskSelector +from modules.variant_builder.variant_set import QVariantSet + + +@dataclass +class VariantBuilder(Generic[C, V, Q, A]): + task_pool: QTaskPool[C, V, Q, A] + previos_variants: QVariantSet[C, V, Q, A] + current_variant: QVariant[C, V, Q, A] + task_selector: QTaskSelector[C, V, Q, A] diff --git a/modules/variant_builder/filters/__init__.py b/modules/variant_builder/filters/__init__.py new file mode 100644 index 0000000..6d6744f --- /dev/null +++ b/modules/variant_builder/filters/__init__.py @@ -0,0 +1,121 @@ +from collections.abc import Iterable +from dataclasses import dataclass, field +from typing import Callable, Generic, Self, overload, override + +from modules.tag import Tag, Tags +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.composite import CompositeFilter +from modules.variant_builder.filters.dynamic import FilterDynamic +from modules.variant_builder.filters.dynamic.composite import CompositeFilterDynamic +from modules.variant_builder.filters.static import FilterStatic +from modules.variant_builder.filters.static.composite import CompositeFilterStatic +from modules.variant_builder.filters.static.must_be_one_of import MustBeOneOfFilter +from modules.variant_builder.filters.static.must_include_all_tags import ( + MustIncludeAllTagsFilter, +) +from modules.variant_builder.filters.static.must_include_any_tag import ( + MustIncludeAnyTagFilter, +) +from modules.variant_builder.filters.types import QTaskOrFactory + + +@dataclass +class FilterBuilder(Generic[C, V, Q, A]): + static_filters: list[FilterStatic[C, V, Q, A]] = field(default_factory=list) + dynamic_filters: list[FilterDynamic[C, V, Q, A]] = field(default_factory=list) + + def add_static(self, filter: FilterStatic[C, V, Q, A]) -> Self: + self.static_filters.append(filter) + return self + + def add_dynamic(self, filter: FilterDynamic[C, V, Q, A]) -> Self: + self.dynamic_filters.append(filter) + return self + + def add(self, filter: FilterStatic[C, V, Q, A] | FilterDynamic[C, V, Q, A]) -> Self: + if isinstance(filter, FilterStatic): + self.static_filters.append(filter) + else: + self.dynamic_filters.append(filter) + return self + + def __call__( + self, filter: FilterStatic[C, V, Q, A] | FilterDynamic[C, V, Q, A] + ) -> Self: + return self.add(filter) + + @overload + def be_one_of(self, item: Iterable[QTaskOrFactory[C, V, Q, A]]) -> Self: ... + + @overload + def be_one_of( + self, item: QTaskOrFactory[C, V, Q, A], **kwargs: QTaskOrFactory[C, V, Q, A] + ) -> Self: ... + + def be_one_of( + self, + item: Iterable[QTaskOrFactory[C, V, Q, A]] | QTaskOrFactory[C, V, Q, A], + **kwargs: QTaskOrFactory[C, V, Q, A], + ) -> Self: + return self.add_static(MustBeOneOfFilter(item, **kwargs)) + + @overload + def include_any_tag(self, tag: Tags[C, V]) -> Self: ... + + @overload + def include_any_tag(self, tag: Iterable[Tag[C, V]]) -> Self: ... + + @overload + def include_any_tag(self, tag: Tag[C, V], **kwargs: Tag[C, V]) -> Self: ... + + def include_any_tag( + self, + tag: Tags[C, V] | Tag[C, V] | Iterable[Tag[C, V]], + **kwargs: Tag[C, V], + ) -> Self: + return self.add_static(MustIncludeAnyTagFilter(tag, **kwargs)) + + @overload + def include_all_tags(self, tag: Tags[C, V]) -> Self: ... + + @overload + def include_all_tags(self, tag: Iterable[Tag[C, V]]) -> Self: ... + + @overload + def include_all_tags(self, tag: Tag[C, V], **kwargs: Tag[C, V]) -> Self: ... + + def include_all_tags( + self, + tag: Tags[C, V] | Tag[C, V] | Iterable[Tag[C, V]], + **kwargs: Tag[C, V], + ) -> Self: + return self.add_static(MustIncludeAllTagsFilter(tag, **kwargs)) + + def be_inverse_of( + self, + filter: Callable[["FilterBuilder[C, V, Q, A]"], "FilterBuilder[C, V, Q, A]"], + ) -> "FilterBuilder[C, V, Q, A]": + return filter(FilterBuilder[C, V, Q, A]()).invert() + + def build_static( + self, + ) -> FilterStatic[C, V, Q, A]: + return CompositeFilterStatic(self.static_filters) + + def build_dynamic( + self, + ) -> CompositeFilterDynamic[C, V, Q, A]: + return CompositeFilterDynamic(self.dynamic_filters) + + def build(self) -> CompositeFilter[C, V, Q, A]: + return CompositeFilter(self.build_static(), self.build_dynamic()) + + def invert(self) -> Self: + for i in range(len(self.static_filters)): + self.static_filters[i] = ~self.static_filters[i] + for i in range(len(self.dynamic_filters)): + self.dynamic_filters[i] = ~self.dynamic_filters[i] + return self + + def __invert__(self) -> Self: + return self.invert() diff --git a/modules/variant_builder/filters/composite.py b/modules/variant_builder/filters/composite.py new file mode 100644 index 0000000..1f02d7e --- /dev/null +++ b/modules/variant_builder/filters/composite.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass +from typing import Generic, Self + +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.dynamic import FilterDynamic +from modules.variant_builder.filters.dynamic.composite import CompositeFilterDynamic +from modules.variant_builder.filters.static import FilterStatic +from modules.variant_builder.filters.static.composite import CompositeFilterStatic + + +class CompositeFilter(Generic[C, V, Q, A]): + static: CompositeFilterStatic[C, V, Q, A] + dynamic: CompositeFilterDynamic[C, V, Q, A] + + def __init__( + self, + static: CompositeFilterStatic[C, V, Q, A] | list[FilterStatic[C, V, Q, A]], + dynamic: CompositeFilterDynamic[C, V, Q, A] | list[FilterDynamic[C, V, Q, A]], + ): + if isinstance(static, list): + self.static = CompositeFilterStatic(static) + else: + self.static = static + + if isinstance(dynamic, list): + self.dynamic = CompositeFilterDynamic(dynamic) + else: + self.dynamic = dynamic + + def invert(self) -> Self: + self.static = ~self.static + self.dynamic = ~self.dynamic + return self diff --git a/modules/variant_builder/filters/dynamic/__init__.py b/modules/variant_builder/filters/dynamic/__init__.py new file mode 100644 index 0000000..bc281c3 --- /dev/null +++ b/modules/variant_builder/filters/dynamic/__init__.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Generic, Protocol, override, runtime_checkable + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant import QVariant +from modules.variant_builder.task_pool import QTaskPool +from modules.variant_builder.variant_set import QVariantSet + + +@runtime_checkable +class FilterDynamic(ABC, Generic[C, V, Q, A]): + @abstractmethod + def check_if_satisfied( + self, + task: QTask[C, V, Q, A], + task_pool: QTaskPool[C, V, Q, A], + previous_variants: QVariantSet[C, V, Q, A], + current_variant: QVariant[C, V, Q, A], + task_number: int, + ) -> bool: ... + + def __invert__(self) -> "FilterDynamic[C, V, Q, A]": + return DynamicFilterNegator(self) + + def inverse(self) -> "FilterDynamic[C, V, Q, A]": + return DynamicFilterNegator(self) + + def invert(self) -> "FilterDynamic[C, V, Q, A]": + return DynamicFilterNegator(self) + + +@dataclass(frozen=True) +class DynamicFilterNegator(FilterDynamic[C, V, Q, A]): + filter: FilterDynamic[C, V, Q, A] + + @override + def check_if_satisfied( + self, + task: QTask[C, V, Q, A], + task_pool: QTaskPool[C, V, Q, A], + previous_variants: QVariantSet[C, V, Q, A], + current_variant: QVariant[C, V, Q, A], + task_number: int, + ) -> bool: + return not self.filter.check_if_satisfied( + task, task_pool, previous_variants, current_variant, task_number + ) + + @override + def __invert__(self) -> "FilterDynamic[C, V, Q, A]": + return self.filter + + @override + def invert(self) -> "FilterDynamic[C, V, Q, A]": + return self.filter diff --git a/modules/variant_builder/filters/dynamic/composite.py b/modules/variant_builder/filters/dynamic/composite.py new file mode 100644 index 0000000..9e84833 --- /dev/null +++ b/modules/variant_builder/filters/dynamic/composite.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from typing import override + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant import QVariant +from modules.variant_builder.filters.dynamic import FilterDynamic +from modules.variant_builder.task_pool import QTaskPool +from modules.variant_builder.variant_set import QVariantSet + + +@dataclass +class CompositeFilterDynamic(FilterDynamic[C, V, Q, A]): + filters: list[FilterDynamic[C, V, Q, A]] + + @override + def check_if_satisfied( + self, + task: QTask[C, V, Q, A], + task_pool: QTaskPool[C, V, Q, A], + previous_variants: QVariantSet[C, V, Q, A], + current_variant: QVariant[C, V, Q, A], + task_number: int, + ) -> bool: + return all( + filter.check_if_satisfied( + task, task_pool, previous_variants, current_variant, task_number + ) + for filter in self.filters + ) diff --git a/modules/variant_builder/filters/dynamic/must_not.py b/modules/variant_builder/filters/dynamic/must_not.py new file mode 100644 index 0000000..4b4ef62 --- /dev/null +++ b/modules/variant_builder/filters/dynamic/must_not.py @@ -0,0 +1,6 @@ +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.dynamic import FilterDynamic + + +class FilterNegatorDynamic(FilterDynamic[C, V, Q, A]): + diff --git a/modules/variant_builder/filters/static/CompositeFilterStatic b/modules/variant_builder/filters/static/CompositeFilterStatic new file mode 100644 index 0000000..feac656 --- /dev/null +++ b/modules/variant_builder/filters/static/CompositeFilterStatic @@ -0,0 +1 @@ + e diff --git a/modules/variant_builder/filters/static/__init__.py b/modules/variant_builder/filters/static/__init__.py new file mode 100644 index 0000000..e4c79d4 --- /dev/null +++ b/modules/variant_builder/filters/static/__init__.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Generic, override, runtime_checkable + +from modules.task import QTask +from modules.utils.types import A, C, Q, V + + +class FilterStatic(ABC, Generic[C, V, Q, A]): + @abstractmethod + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: ... + + def __invert__(self) -> "FilterStatic[C, V, Q, A]": + return StaticFilterNegator(self) + + def invert(self) -> "FilterStatic[C, V, Q, A]": + return StaticFilterNegator(self) + + +@dataclass(frozen=True) +class StaticFilterNegator(FilterStatic[C, V, Q, A]): + filter: FilterStatic[C, V, Q, A] + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + return not self.filter.is_satisfied(task) + + @override + def __invert__(self) -> "FilterStatic[C, V, Q, A]": + return self.filter + + @override + def invert(self) -> "FilterStatic[C, V, Q, A]": + return self.filter diff --git a/modules/variant_builder/filters/static/composite.py b/modules/variant_builder/filters/static/composite.py new file mode 100644 index 0000000..5326b37 --- /dev/null +++ b/modules/variant_builder/filters/static/composite.py @@ -0,0 +1,24 @@ +from collections.abc import Iterable +from dataclasses import dataclass, field, replace +from typing import Self, override + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.static import FilterStatic + + +@dataclass(frozen=True) +class CompositeFilterStatic(FilterStatic[C, V, Q, A]): + filters: list[FilterStatic[C, V, Q, A]] + is_inverted: bool = field(default=False) + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + if not self.is_inverted: + return all(filter.is_satisfied(task) for filter in self.filters) + else: + return any(not filter.is_satisfied(task) for filter in self.filters) + + @override + def invert(self) -> "CompositeFilterStatic[C, V, Q, A]": + return replace(self, is_inverted=not self.is_inverted) diff --git a/modules/variant_builder/filters/static/composite_filter.py b/modules/variant_builder/filters/static/composite_filter.py new file mode 100644 index 0000000..48c5a11 --- /dev/null +++ b/modules/variant_builder/filters/static/composite_filter.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +from typing import override + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters import FilterBuilder +from modules.variant_builder.filters.static import FilterStatic + + +@dataclass +class CompositeFilterStatic(FilterStatic[C, V, Q, A]): + filters: list[FilterStatic[C, V, Q, A]] + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + return all(filter.is_satisfied(task) for filter in self.filters) diff --git a/modules/variant_builder/filters/static/must_be_one_of.py b/modules/variant_builder/filters/static/must_be_one_of.py new file mode 100644 index 0000000..0bc3886 --- /dev/null +++ b/modules/variant_builder/filters/static/must_be_one_of.py @@ -0,0 +1,38 @@ +import uuid +from collections.abc import Iterable +from dataclasses import field +from typing import overload, override + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.static import FilterStatic +from modules.variant_builder.filters.types import QTaskOrFactory + + +class MustBeOneOfFilter(FilterStatic[C, V, Q, A]): + must_be_one_of: list[uuid.UUID] = field(default_factory=list[uuid.UUID]) + + @overload + def __init__(self, item: Iterable[QTaskOrFactory[C, V, Q, A]]) -> None: ... + + @overload + def __init__( + self, item: QTaskOrFactory[C, V, Q, A], **kwargs: QTaskOrFactory[C, V, Q, A] + ) -> None: ... + + def __init__( + self, + item: Iterable[QTaskOrFactory[C, V, Q, A]] | QTaskOrFactory[C, V, Q, A], + **kwargs: QTaskOrFactory[C, V, Q, A], + ): + if isinstance(item, Iterable): + self.must_be_one_of.extend(map(lambda i: i.id, item)) + else: + self.must_be_one_of.append(item.id) + self.must_be_one_of.extend(map(lambda i: i.id, kwargs.values())) + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + task_id = task.id + factory_id = task.factory_metadata.map(lambda m: m.id).unwrap_or(None) + return any(task_id == id or factory_id == id for id in self.must_be_one_of) diff --git a/modules/variant_builder/filters/static/must_include_all_tags.py b/modules/variant_builder/filters/static/must_include_all_tags.py new file mode 100644 index 0000000..c0846e9 --- /dev/null +++ b/modules/variant_builder/filters/static/must_include_all_tags.py @@ -0,0 +1,38 @@ +from collections.abc import Iterable +from dataclasses import field +from typing import overload, override + +from modules.tag import Tag, Tags +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.static import FilterStatic + + +class MustIncludeAllTagsFilter(FilterStatic[C, V, Q, A]): + tags: Tags[C, V] = field(default_factory=Tags[C, V]) + + @overload + def __init__(self, tag: Tags[C, V]) -> None: ... + + @overload + def __init__(self, tag: Iterable[Tag[C, V]]) -> None: ... + + @overload + def __init__(self, tag: Tag[C, V], **kwargs: Tag[C, V]) -> None: ... + + def __init__( + self, + tag: Tags[C, V] | Tag[C, V] | Iterable[Tag[C, V]], + **kwargs: Tag[C, V], + ): + if isinstance(tag, Tags): + self.tags = tag + elif isinstance(tag, Iterable): + self.tags = Tags[C, V](tag) + else: + self.tags = Tags[C, V](kwargs.values()) + self.tags.add_tag(tag) + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + return all(task.tags.has_tag(tag) for tag in self.tags) diff --git a/modules/variant_builder/filters/static/must_include_any_tag.py b/modules/variant_builder/filters/static/must_include_any_tag.py new file mode 100644 index 0000000..1e66224 --- /dev/null +++ b/modules/variant_builder/filters/static/must_include_any_tag.py @@ -0,0 +1,38 @@ +from collections.abc import Iterable +from dataclasses import field +from typing import overload, override + +from modules.tag import Tag, Tags +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.static import FilterStatic + + +class MustIncludeAnyTagFilter(FilterStatic[C, V, Q, A]): + tags: Tags[C, V] = field(default_factory=Tags[C, V]) + + @overload + def __init__(self, tag: Tags[C, V]) -> None: ... + + @overload + def __init__(self, tag: Iterable[Tag[C, V]]) -> None: ... + + @overload + def __init__(self, tag: Tag[C, V], **kwargs: Tag[C, V]) -> None: ... + + def __init__( + self, + tag: Tags[C, V] | Tag[C, V] | Iterable[Tag[C, V]], + **kwargs: Tag[C, V], + ): + if isinstance(tag, Tags): + self.tags = tag + elif isinstance(tag, Iterable): + self.tags = Tags[C, V](tag) + else: + self.tags = Tags[C, V](kwargs.values()) + self.tags.add_tag(tag) + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + return any(task.tags.has_tag(tag) for tag in self.tags) diff --git a/modules/variant_builder/filters/static/must_not.py b/modules/variant_builder/filters/static/must_not.py new file mode 100644 index 0000000..4b0c503 --- /dev/null +++ b/modules/variant_builder/filters/static/must_not.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from typing import override + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.filters.static import FilterStatic + + +@dataclass +class StaticFilterNegator(FilterStatic[C, V, Q, A]): + filter: FilterStatic[C, V, Q, A] + + @override + def is_satisfied(self, task: QTask[C, V, Q, A]) -> bool: + return not self.filter.is_satisfied(task) + + @override + def __invert__(self) -> "FilterStatic[C, V, Q, A]": + return self.filter diff --git a/modules/variant_builder/filters/types.py b/modules/variant_builder/filters/types.py new file mode 100644 index 0000000..4a0e426 --- /dev/null +++ b/modules/variant_builder/filters/types.py @@ -0,0 +1,6 @@ +from typing import Union + +from modules.task import QTask +from modules.task.factory import QTaskFactory + +type QTaskOrFactory[C, V, Q, A] = QTask[C, V, Q, A] | QTaskFactory[C, V, Q, A] diff --git a/modules/variant_builder/task_pool.py b/modules/variant_builder/task_pool.py new file mode 100644 index 0000000..a72ad4f --- /dev/null +++ b/modules/variant_builder/task_pool.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass, field +from typing import Generic + +from modules.task import QTask +from modules.utils.types import A, C, Q, V + + +@dataclass(frozen=True) +class QTaskPool(Generic[C, V, Q, A]): + pool: list[QTask[C, V, Q, A]] diff --git a/modules/variant_builder/task_selector.py b/modules/variant_builder/task_selector.py new file mode 100644 index 0000000..0e66b2e --- /dev/null +++ b/modules/variant_builder/task_selector.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass +from typing import Protocol + +from modules.task import QTask +from modules.utils.types import A, C, Q, V +from modules.variant_builder.task_pool import QTaskPool + + +@dataclass +class QTaskSelector(Protocol[C, V, Q, A]): + task_pool: QTaskPool[C, V, Q, A] + + def select(self, filtered_task_pool_indexes: list[int]) -> QTask[C, V, Q, A]: ... diff --git a/modules/variant_builder/variant_set.py b/modules/variant_builder/variant_set.py new file mode 100644 index 0000000..817a4c1 --- /dev/null +++ b/modules/variant_builder/variant_set.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass, field +from typing import Generic + +from modules.utils.types import A, C, Q, V +from modules.variant import QVariant + + +@dataclass +class QVariantSet(Generic[C, V, Q, A]): + variants: list[QVariant[C, V, Q, A]] = field(default_factory=list) + + def __iter__(self): + for variant in self.variants: + yield variant diff --git a/modules/variant_set.py b/modules/variant_set.py deleted file mode 100644 index 0ff458a..0000000 --- a/modules/variant_set.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Generic, List - -from modules.variant import QVariant -from utils.types import A, C, Q, V - - -class QVariantSet(Generic[C, V, Q, A]): - variants: List[QVariant[C, V, Q, A]] - - def __iter__(self): - for variant in self.variants: - yield variant diff --git a/pyproject.toml b/pyproject.toml index f351843..96f38a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "quizard" +name = "QuizGen" version = "0.1.0" description = "" authors = [ diff --git a/quiz_task_generator.py b/quiz_task_generator.py deleted file mode 100644 index 118603f..0000000 --- a/quiz_task_generator.py +++ /dev/null @@ -1,32 +0,0 @@ -import uuid -from dataclasses import dataclass -from typing import Generic, Optional, Protocol, TypeVar - -from option import Option - -C = TypeVar("C", default=str) -V = TypeVar("V", default=str) - - -@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), - ) - - -class TaskGenerator(Protocol, Generic[C, V]): - metadata: QuizTaskGeneratorMetadata[C, V] - default_amount: Option[int] = Option.maybe(None) - - def generate(self) -> QuizTask[C, V]: ... diff --git a/tests/__pycache__/test_quizard.cpython-313-pytest-8.3.5.pyc b/tests/__pycache__/test_quizard.cpython-313-pytest-8.3.5.pyc deleted file mode 100644 index e977cef..0000000 Binary files a/tests/__pycache__/test_quizard.cpython-313-pytest-8.3.5.pyc and /dev/null differ diff --git a/tests/__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc b/tests/__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc deleted file mode 100644 index 2ce28fc..0000000 Binary files a/tests/__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc and /dev/null differ diff --git a/tests/__pycache__/tests.cpython-313.pyc b/tests/__pycache__/tests.cpython-313.pyc deleted file mode 100644 index 4972ef2..0000000 Binary files a/tests/__pycache__/tests.cpython-313.pyc and /dev/null differ diff --git a/tests/test_quizard.py b/tests/test_quizard.py deleted file mode 100644 index 7b817c6..0000000 --- a/tests/test_quizard.py +++ /dev/null @@ -1,6 +0,0 @@ -def func(x): - return x + 1 - - -def test_answer(): - assert func(3) == 5