working on filter builder
This commit is contained in:
28
QuizTask.py
28
QuizTask.py
@ -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), "{}"))}"
|
||||
345
Quizard.py
345
Quizard.py
@ -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")
|
||||
@ -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)
|
||||
@ -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: ...
|
||||
@ -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: ...
|
||||
@ -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])
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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]]
|
||||
@ -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]: ...
|
||||
@ -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),
|
||||
)
|
||||
12
modules/quizgen.py
Normal file
12
modules/quizgen.py
Normal file
@ -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]
|
||||
118
modules/tag/__init__.py
Normal file
118
modules/tag/__init__.py
Normal file
@ -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)
|
||||
@ -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)
|
||||
@ -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))}"
|
||||
38
modules/task/__init__.py
Normal file
38
modules/task/__init__.py
Normal file
@ -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))}"
|
||||
22
modules/task/factory/__init__.py
Normal file
22
modules/task/factory/__init__.py
Normal file
@ -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
|
||||
24
modules/task/factory/metadata.py
Normal file
24
modules/task/factory/metadata.py
Normal file
@ -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),
|
||||
)
|
||||
@ -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]]
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -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
|
||||
14
modules/variant/__init__.py
Normal file
14
modules/variant/__init__.py
Normal file
@ -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
|
||||
16
modules/variant_builder/__init__.py
Normal file
16
modules/variant_builder/__init__.py
Normal file
@ -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]
|
||||
121
modules/variant_builder/filters/__init__.py
Normal file
121
modules/variant_builder/filters/__init__.py
Normal file
@ -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()
|
||||
33
modules/variant_builder/filters/composite.py
Normal file
33
modules/variant_builder/filters/composite.py
Normal file
@ -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
|
||||
57
modules/variant_builder/filters/dynamic/__init__.py
Normal file
57
modules/variant_builder/filters/dynamic/__init__.py
Normal file
@ -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
|
||||
30
modules/variant_builder/filters/dynamic/composite.py
Normal file
30
modules/variant_builder/filters/dynamic/composite.py
Normal file
@ -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
|
||||
)
|
||||
6
modules/variant_builder/filters/dynamic/must_not.py
Normal file
6
modules/variant_builder/filters/dynamic/must_not.py
Normal file
@ -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]):
|
||||
|
||||
@ -0,0 +1 @@
|
||||
e
|
||||
34
modules/variant_builder/filters/static/__init__.py
Normal file
34
modules/variant_builder/filters/static/__init__.py
Normal file
@ -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
|
||||
24
modules/variant_builder/filters/static/composite.py
Normal file
24
modules/variant_builder/filters/static/composite.py
Normal file
@ -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)
|
||||
16
modules/variant_builder/filters/static/composite_filter.py
Normal file
16
modules/variant_builder/filters/static/composite_filter.py
Normal file
@ -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)
|
||||
38
modules/variant_builder/filters/static/must_be_one_of.py
Normal file
38
modules/variant_builder/filters/static/must_be_one_of.py
Normal file
@ -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)
|
||||
@ -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)
|
||||
@ -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)
|
||||
19
modules/variant_builder/filters/static/must_not.py
Normal file
19
modules/variant_builder/filters/static/must_not.py
Normal file
@ -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
|
||||
6
modules/variant_builder/filters/types.py
Normal file
6
modules/variant_builder/filters/types.py
Normal file
@ -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]
|
||||
10
modules/variant_builder/task_pool.py
Normal file
10
modules/variant_builder/task_pool.py
Normal file
@ -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]]
|
||||
13
modules/variant_builder/task_selector.py
Normal file
13
modules/variant_builder/task_selector.py
Normal file
@ -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]: ...
|
||||
14
modules/variant_builder/variant_set.py
Normal file
14
modules/variant_builder/variant_set.py
Normal file
@ -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
|
||||
@ -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
|
||||
@ -1,5 +1,5 @@
|
||||
[project]
|
||||
name = "quizard"
|
||||
name = "QuizGen"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = [
|
||||
|
||||
@ -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]: ...
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,6 +0,0 @@
|
||||
def func(x):
|
||||
return x + 1
|
||||
|
||||
|
||||
def test_answer():
|
||||
assert func(3) == 5
|
||||
Reference in New Issue
Block a user