Compare commits
4 Commits
main
...
reorganize
| Author | SHA1 | Date | |
|---|---|---|---|
| b74238f14e | |||
| f9eb2f35a8 | |||
| 575d212488 | |||
| 1b677d0220 |
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")
|
|
||||||
BIN
__pycache__/test.cpython-313.pyc
Normal file
BIN
__pycache__/test.cpython-313.pyc
Normal file
Binary file not shown.
BIN
clean.xlsx
Normal file
BIN
clean.xlsx
Normal file
Binary file not shown.
142
docs/quick_start.md
Normal file
142
docs/quick_start.md
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
# Основные понятия
|
||||||
|
|
||||||
|
Для того чтобы составить варианты, вам сперва нужно: 1) написать все задачи/генераторы задач, 2) составить из них пул задач и 3) описать структуру варианта. Разберём по пунктам:
|
||||||
|
|
||||||
|
1.1 Задача - это основной строительный блок варианта. В `QuizGen` задача представляет собой класс с тремя значениями. Одно обязательные - это вопрос задачи `question`. И два опциональных -- ответ к задаче `answer` и теги `tags` (про которые позже)
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.tag import Tags
|
||||||
|
|
||||||
|
task = QTask(
|
||||||
|
question="Посчитайте дисперсию для следующего ряда: 5, 6, 7, 8, 9",
|
||||||
|
answer="2", # можно опустить или не давать ответ: answer = None
|
||||||
|
tags=Tags(
|
||||||
|
("тема", "дисперсия"),
|
||||||
|
("сложность", "лёгкая"),
|
||||||
|
...
|
||||||
|
) # теги можно определять любые. Первое значение - это категория, второе - это значение в этой категории.
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
1.2 Генератор задач - это класс, с функцией generate(), которая при вызове генерирует задачу `QTask`:
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
from moduels.task.factory.default import QTaskFactoryDefault
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.tag import Tags
|
||||||
|
|
||||||
|
#Сначала определим функцию, которая будет использоваться для генерации задач
|
||||||
|
def task_generator_function():
|
||||||
|
# генерируем два случайных целых числа от 1 до 9
|
||||||
|
alpha = random.randint(1, 9)
|
||||||
|
beta = random.randint(1, 9)
|
||||||
|
|
||||||
|
# Используем сгенерированные числа, чтобы составить задачу
|
||||||
|
question = f"Чему равна дисперсия величины V({alpha} * X + {beta} * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга?"
|
||||||
|
answer = f"{alpha ** 2 + beta ** 2}"
|
||||||
|
|
||||||
|
return QTask(
|
||||||
|
question,
|
||||||
|
answer,
|
||||||
|
)
|
||||||
|
|
||||||
|
# С её помощью создадим генератор задач
|
||||||
|
variance_task_factory = QTaskFactoryDefault(task_generator_function)
|
||||||
|
|
||||||
|
# Получаем генератор, который умеет производит задачи с разными числами, каждый раз, когда мы вызываем фукнцию generate():
|
||||||
|
|
||||||
|
variance_task_factory.generate() # => Чему равна дисперсия величины V(3 * X + 5 * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга? Ответ: 34
|
||||||
|
|
||||||
|
variance_task_factory.generate() # => Чему равна дисперсия величины V(6 * X + 2 * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга? Ответ: 40
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Пул задач. Пул - это массив задач и генераторов задач, из которых составляются варианты.
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
from modules.variant_builder.task_pool import QTaskPool
|
||||||
|
from modules.task import QTask
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
QTask(
|
||||||
|
question="Текст задачи 1"
|
||||||
|
),
|
||||||
|
QTask(
|
||||||
|
question="Текст задачи 2"
|
||||||
|
),
|
||||||
|
QTask(
|
||||||
|
question="Текст задачи 3"
|
||||||
|
),
|
||||||
|
variance_task_factory # каждый раз, когда в вариант отбирается генератор, он генерирует для этого варианта новую задачу с уникальными значениями
|
||||||
|
]
|
||||||
|
|
||||||
|
# Инициируем пул задач
|
||||||
|
task_pool = QTaskPool(tasks)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Описание структуры варианта. Для описания структуры варианта используется специальный класс `VariantFactory`. Чтобы его создать, из скольки задач будут состоять варианты, пул задач из которых подбираются задачи и (опционально) правило отбора задач. Если последнее не указать, то будет использоваться правило, которые старается минимизировать число пересечений между любыми двумя вариантами и по возможности равномерно распределить задачи.
|
||||||
|
|
||||||
|
Чтобы понять, как определяется структура варианта, рассмотрим простой пример. Допустим, мы хотим сделать вариант, состоящий из трёх задач и у каждой задачи есть две вариации.
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
from modules.variant_builder import VariantFactory
|
||||||
|
|
||||||
|
# Сначала создадим пул задач
|
||||||
|
task_pool = QTaskPool([
|
||||||
|
# Задача 1
|
||||||
|
QTask(
|
||||||
|
question="1.1"
|
||||||
|
tags=Tag("order", "first") # Тег, чтобы отличать первую задачу от второй и третий
|
||||||
|
),
|
||||||
|
QTask(
|
||||||
|
question="1.2"
|
||||||
|
tags=Tag("order", "first")
|
||||||
|
),
|
||||||
|
|
||||||
|
# Задача 2
|
||||||
|
QTask(
|
||||||
|
question="2.1"
|
||||||
|
tags=Tag("order", "second")
|
||||||
|
),
|
||||||
|
QTask(
|
||||||
|
question="2.2"
|
||||||
|
tags=Tag("order", "second")
|
||||||
|
),
|
||||||
|
|
||||||
|
# Задача 3
|
||||||
|
QTask(
|
||||||
|
question="3.1"
|
||||||
|
tags=Tag("order", "third")
|
||||||
|
),
|
||||||
|
QTask(
|
||||||
|
question="3.2"
|
||||||
|
tags=Tag("order", "third")
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Инициируем генератор вариантов
|
||||||
|
vf = VariantFactory(number_of_tasks=3, task_pool = task_pool)
|
||||||
|
|
||||||
|
# Теперь самое главное - укажем, что первая задача в варианте должна быть задачей один, вторая - задачей два, а третья - задачей три
|
||||||
|
vf.task[0].must.include_tag("order", "first") # первая задача должна быть задачей с пометкой order = first
|
||||||
|
vf.task[1].must.include_tag("order", "second") # вторая задача должна быть задачей с пометкой order = second
|
||||||
|
vf.task[2].must.include_tag("order", "third") # первая задача должна быть задачей с пометкой order = third
|
||||||
|
|
||||||
|
# доступные методы include_all_tags() - должна включать все теги из списка, include_any_tag() - должна включать хотя один тег из списка, be_one_of() - должна быть одной из задач или должна быть сгенерирована определённым генератором, not(lambda b: b.must...) - логическое отрицание, or(lambda b: b.must..., lambda b: b.must...) - логическое или.
|
||||||
|
|
||||||
|
# Сгенерируем 10 вариантов:
|
||||||
|
variants = vf.generate_variants(number_of_variants=10) # можем указать любое число
|
||||||
|
```
|
||||||
|
|
||||||
|
Генератор вариантов сгенерирует 10 вариантов так, чтобы они были максимально уникальны относительно друг друга. Варианты, которые составляет `VariantFactory` представлены классом `QVariant`, который есть просто собрание задач и по сути представляет собой обычный массив:
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
first_variant = variants[0] # первый вариант
|
||||||
|
first_task_of_first_variant = first_variant[0] # первая задача первого варианта
|
||||||
|
|
||||||
|
print(first_task_of_first_variant.question) # => 1.2
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
83
example.py
83
example.py
@ -1,54 +1,41 @@
|
|||||||
from enum import Enum
|
from modules.tag import Tag
|
||||||
from typing import Generic
|
from modules.task import QTask
|
||||||
|
from modules.task.factory import QTaskFactory
|
||||||
|
from modules.variant_builder import VariantFactory
|
||||||
|
from modules.variant_builder.task_pool import QTaskPool
|
||||||
|
|
||||||
from option import Some
|
tasks_on_first_topic = [QTask(f"1.{i + 1}", tags=Tag("topic", "1")) for i in range(5)]
|
||||||
|
tasks_on_second_topic = [QTask(f"2.{i + 1}", tags=Tag("topic", "2")) for i in range(5)]
|
||||||
|
tasks_on_third_topic = [QTask(f"3.{i + 1}", tags=Tag("topic", "3")) for i in range(5)]
|
||||||
|
tasks_on_forth_topic = [QTask(f"4.{i + 1}", tags=Tag("topic", "4")) for i in range(5)]
|
||||||
|
tasks_on_fifth_topic = [QTask(f"5.{i + 1}", tags=Tag("topic", "5")) for i in range(5)]
|
||||||
|
tasks_on_sixth_topic = [QTask(f"6.{i + 1}", tags=Tag("topic", "6")) for i in range(5)]
|
||||||
|
tasks_on_seventh_topic = [QTask(f"7.{i + 1}", tags=Tag("topic", "7")) for i in range(5)]
|
||||||
|
|
||||||
from Quizard import Quizard, QuizTask, QuizTaskGeneratorMetadata, TaskGenerator
|
task_pool = QTaskPool(
|
||||||
|
tasks_on_first_topic
|
||||||
|
+ tasks_on_second_topic
|
||||||
|
+ tasks_on_third_topic
|
||||||
|
+ tasks_on_forth_topic
|
||||||
|
+ tasks_on_fifth_topic
|
||||||
|
+ tasks_on_sixth_topic
|
||||||
|
+ tasks_on_seventh_topic
|
||||||
|
)
|
||||||
|
|
||||||
|
vf = VariantFactory(number_of_tasks=7, task_pool=task_pool)
|
||||||
|
|
||||||
class TagCategory(str, Enum):
|
_ = vf.task[0].must.include_tag("topic", "1")
|
||||||
TOPIC = "topic"
|
_ = vf.task[1].must.include_tag("topic", "2")
|
||||||
DIFFICULTY = "difficulty"
|
_ = vf.task[2].must.include_tag("topic", "3")
|
||||||
|
_ = vf.task[3].must.include_tag("topic", "4")
|
||||||
|
_ = vf.task[4].must.include_tag("topic", "5")
|
||||||
|
_ = vf.task[5].must.include_tag("topic", "6")
|
||||||
|
_ = vf.task[6].must.include_tag("topic", "7")
|
||||||
|
|
||||||
|
variants = vf.generate_variants(number_of_variants=30)
|
||||||
|
|
||||||
class TopicTag(str, Enum):
|
i = 0
|
||||||
AVERAGE = "average"
|
for variant in variants:
|
||||||
VARIANCE = "variance"
|
print(f"Variant {i + 1}:")
|
||||||
|
print(*[task.question for task in variant.tasks])
|
||||||
|
i += 1
|
||||||
class MyTaskGenerator(TaskGenerator[TagCategory, TopicTag]):
|
|
||||||
def __init__(self):
|
|
||||||
self.metadata = QuizTaskGeneratorMetadata[TagCategory, TopicTag].from_values(
|
|
||||||
name=self.__class__.__name__,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AverageTask(MyTaskGenerator):
|
|
||||||
def generate(self):
|
|
||||||
return QuizTask(
|
|
||||||
"What is an average of 3, 4, 5 and 6?",
|
|
||||||
"4.5",
|
|
||||||
tags=[
|
|
||||||
(TagCategory.TOPIC, TopicTag.AVERAGE),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AverageTask1(MyTaskGenerator):
|
|
||||||
def generate(self):
|
|
||||||
return QuizTask(
|
|
||||||
"What is an average of 1, 2, 3 and 4?",
|
|
||||||
"2.5",
|
|
||||||
tags=[
|
|
||||||
(TagCategory.TOPIC, TopicTag.VARIANCE),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
default_amount = Some(1)
|
|
||||||
|
|
||||||
|
|
||||||
quizard = Quizard([AverageTask(), AverageTask1()], 3)
|
|
||||||
|
|
||||||
quizard.fillTaskPool()
|
|
||||||
|
|
||||||
print(*[task.generator_metadata.id for task in quizard.taskPool], sep="\n\n")
|
|
||||||
|
|||||||
@ -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]
|
||||||
144
modules/tag/__init__.py
Normal file
144
modules/tag/__init__.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from enum import Enum
|
||||||
|
from itertools import chain
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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]]
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __init__(
|
||||||
|
self, iter: Iterable[Tag[C, V] | tuple[C, V]] | None = None
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
iter: Tag[C, V] | tuple[C, V] | None = None,
|
||||||
|
**kwargs: Tag[C, V] | tuple[C, V],
|
||||||
|
) -> None: ...
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
iter: Iterable[Tag[C, V] | tuple[C, V]] | Tag[C, V] | tuple[C, V] | None = None,
|
||||||
|
**kwargs: Tag[C, V] | tuple[C, V],
|
||||||
|
) -> None:
|
||||||
|
self._dict = {}
|
||||||
|
|
||||||
|
if isinstance(iter, Iterable) and not isinstance(iter, tuple):
|
||||||
|
for tag in iter:
|
||||||
|
if isinstance(tag, Tag):
|
||||||
|
self._dict.setdefault(tag.cat, set()).add(tag.val)
|
||||||
|
else:
|
||||||
|
self._dict.setdefault(tag[0], set()).add(tag[1])
|
||||||
|
else:
|
||||||
|
for tag in chain([iter], kwargs.values()):
|
||||||
|
if isinstance(iter, Tag):
|
||||||
|
self._dict.setdefault(tag.cat, set()).add(tag.val)
|
||||||
|
elif isinstance(iter, tuple):
|
||||||
|
self._dict.setdefault(tag[0], set()).add(tag[1])
|
||||||
|
|
||||||
|
@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 = [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)
|
||||||
BIN
modules/tag/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modules/tag/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
@ -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))}"
|
|
||||||
43
modules/task/__init__.py
Normal file
43
modules/task/__init__.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import TYPE_CHECKING, Generic, override
|
||||||
|
|
||||||
|
from option import Option
|
||||||
|
|
||||||
|
from modules.tag import Tag, Tags
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
from modules.utils.utils import indent
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.task.factory.metadata import QTaskFactoryMetadata
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QTask(Generic[C, V, Q, A]):
|
||||||
|
question: Q
|
||||||
|
answer: A
|
||||||
|
tags: Tags[C, V]
|
||||||
|
id: uuid.UUID
|
||||||
|
factory_metadata: Option["QTaskFactoryMetadata[C, V]"] = Option[
|
||||||
|
"QTaskFactoryMetadata[C, V]"
|
||||||
|
].maybe(None)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
question: Q,
|
||||||
|
answer: A = None,
|
||||||
|
tags: Tags[C, V] | list[Tag[C, V]] | Tag[C, V] | None = None,
|
||||||
|
):
|
||||||
|
self.question = question
|
||||||
|
self.answer = answer
|
||||||
|
if isinstance(tags, Tag):
|
||||||
|
self.tags = Tags[C, V]([tags])
|
||||||
|
if isinstance(tags, list):
|
||||||
|
self.tags = Tags[C, V](tags)
|
||||||
|
elif isinstance(tags, Tags):
|
||||||
|
self.tags = tags
|
||||||
|
self.id = uuid.uuid4()
|
||||||
|
|
||||||
|
@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))}"
|
||||||
BIN
modules/task/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modules/task/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
21
modules/task/factory/__init__.py
Normal file
21
modules/task/factory/__init__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import uuid
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Callable, Generic
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
BIN
modules/task/factory/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modules/task/factory/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/task/factory/__pycache__/metadata.cpython-313.pyc
Normal file
BIN
modules/task/factory/__pycache__/metadata.cpython-313.pyc
Normal file
Binary file not shown.
31
modules/task/factory/default.py
Normal file
31
modules/task/factory/default.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from typing import Callable, override
|
||||||
|
|
||||||
|
from modules.tag import Tags
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.task.factory import QTaskFactory
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
|
||||||
|
|
||||||
|
class QTaskFactoryDefault(QTaskFactory[C, V, Q, A]):
|
||||||
|
_generator: Callable[[], QTask[C, V, Q, A]]
|
||||||
|
|
||||||
|
def __init__(self, generator: Callable[[], QTask[C, V, Q, A]]):
|
||||||
|
self._generator = generator
|
||||||
|
|
||||||
|
@override
|
||||||
|
def generate(self) -> QTask[C, V, Q, A]:
|
||||||
|
return self._generator()
|
||||||
|
|
||||||
|
|
||||||
|
def task_generator_function():
|
||||||
|
alpha = random.randint(1, 9)
|
||||||
|
beta = random.randint(1, 9)
|
||||||
|
|
||||||
|
question = f"Чему равна дисперсия величины V({alpha} * X + {beta} * Y), если X и Y подчинены стандартному нормальному закону распределения и независимы друг от друга?"
|
||||||
|
answer = f"{alpha ** 2 + beta ** 2}"
|
||||||
|
tags = Tags(("тема", "дисперсия"), ("сложность", "лёгкая"))
|
||||||
|
|
||||||
|
return QTask(question, answer, tags)
|
||||||
|
|
||||||
|
|
||||||
|
factory = QTaskFactoryDefault(task_generator_function)
|
||||||
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 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]]
|
|
||||||
BIN
modules/utils/__pycache__/types.cpython-313.pyc
Normal file
BIN
modules/utils/__pycache__/types.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/utils/__pycache__/utils.cpython-313.pyc
Normal file
BIN
modules/utils/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
@ -3,4 +3,4 @@ from typing import TypeVar
|
|||||||
C = TypeVar("C", default=str)
|
C = TypeVar("C", default=str)
|
||||||
V = TypeVar("V", default=str)
|
V = TypeVar("V", default=str)
|
||||||
Q = TypeVar("Q", default=str)
|
Q = TypeVar("Q", default=str)
|
||||||
A = TypeVar("A", default=Q)
|
A = TypeVar("A", default=Q | None)
|
||||||
@ -1,9 +1,11 @@
|
|||||||
|
from random import Random
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
C = TypeVar("C", default=str)
|
|
||||||
V = TypeVar("V", default=str)
|
|
||||||
|
|
||||||
|
|
||||||
def indent(text: str, spaces: int = 2) -> str:
|
def indent(text: str, spaces: int = 2) -> str:
|
||||||
prefix = " " * spaces
|
prefix = " " * spaces
|
||||||
return "\n".join(prefix + line for line in text.splitlines())
|
return "\n".join(prefix + line for line in text.splitlines())
|
||||||
|
|
||||||
|
|
||||||
|
rnd = Random()
|
||||||
|
rnd.seed(42)
|
||||||
@ -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
|
||||||
BIN
modules/variant/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modules/variant/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
97
modules/variant_builder/__init__.py
Normal file
97
modules/variant_builder/__init__.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import Generic
|
||||||
|
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
from modules.variant import QVariant
|
||||||
|
from modules.variant_builder.context import DynamicCtx
|
||||||
|
from modules.variant_builder.default_task_selector import LeastUsedTaskSelector
|
||||||
|
from modules.variant_builder.filters import Filter
|
||||||
|
from modules.variant_builder.task_pool import QTaskPool
|
||||||
|
from modules.variant_builder.task_selector import QTaskSelector
|
||||||
|
from modules.variant_builder.variant_set import QVariantSet
|
||||||
|
from modules.variant_builder.variant_task import VariantTask
|
||||||
|
|
||||||
|
|
||||||
|
class VariantFactory(Generic[C, V, Q, A]):
|
||||||
|
task_pool: QTaskPool[C, V, Q, A]
|
||||||
|
previous_variants: QVariantSet[C, V, Q, A] = QVariantSet()
|
||||||
|
task_selector: QTaskSelector[C, V, Q, A]
|
||||||
|
task: list[VariantTask[C, V, Q, A]]
|
||||||
|
number_of_tasks: int
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
number_of_tasks: int,
|
||||||
|
task_pool: Iterable[QTask[C, V, Q, A]],
|
||||||
|
task_selector: QTaskSelector[C, V, Q, A] | None = None,
|
||||||
|
):
|
||||||
|
self.task = [VariantTask() for _ in range(number_of_tasks)]
|
||||||
|
self.number_of_tasks = number_of_tasks
|
||||||
|
self.task_pool = QTaskPool[C, V, Q, A](task_pool)
|
||||||
|
self.task_selector = (
|
||||||
|
task_selector if task_selector is not None else LeastUsedTaskSelector()
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_variants(self, number_of_variants: int) -> QVariantSet[C, V, Q, A]:
|
||||||
|
variant_task_filters: list[Filter[C, V, Q, A]] = [
|
||||||
|
b.must.build() for b in self.task
|
||||||
|
]
|
||||||
|
static_filter_matches_per_task = self._get_static_filter_matches(
|
||||||
|
variant_task_filters
|
||||||
|
)
|
||||||
|
|
||||||
|
dynamic_context = DynamicCtx(
|
||||||
|
self.task_pool,
|
||||||
|
)
|
||||||
|
|
||||||
|
for _ in range(number_of_variants):
|
||||||
|
variant_tasks: list[QTask[C, V, Q, A]] = []
|
||||||
|
for task_index in range(self.number_of_tasks):
|
||||||
|
dynamic_filtered_matches = self._get_dynamic_filter_matches(
|
||||||
|
static_filter_matches_per_task[task_index],
|
||||||
|
variant_task_filters[task_index],
|
||||||
|
dynamic_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_task = self.task_selector.select(
|
||||||
|
dynamic_filtered_matches, dynamic_context
|
||||||
|
)
|
||||||
|
variant_tasks.append(selected_task)
|
||||||
|
dynamic_context.current_variant_tasks.append(selected_task)
|
||||||
|
|
||||||
|
variant = QVariant(variant_tasks)
|
||||||
|
dynamic_context.previous_variants.append(variant)
|
||||||
|
dynamic_context.current_variant_tasks.clear()
|
||||||
|
|
||||||
|
return dynamic_context.previous_variants
|
||||||
|
|
||||||
|
def _get_static_filter_matches(
|
||||||
|
self, task_filters: list[Filter[C, V, Q, A]]
|
||||||
|
) -> list[list[QTask[C, V, Q, A]]]:
|
||||||
|
statical_filter_matches_per_task: list[list[QTask[C, V, Q, A]]] = [
|
||||||
|
[] for _ in range(self.number_of_tasks)
|
||||||
|
]
|
||||||
|
|
||||||
|
for task in self.task_pool:
|
||||||
|
task_filter_index = 0
|
||||||
|
for filter in task_filters:
|
||||||
|
if filter.static.is_satisfied(task):
|
||||||
|
statical_filter_matches_per_task[task_filter_index].append(task)
|
||||||
|
task_filter_index += 1
|
||||||
|
|
||||||
|
return statical_filter_matches_per_task
|
||||||
|
|
||||||
|
def _get_dynamic_filter_matches(
|
||||||
|
self,
|
||||||
|
statically_filtered_pool: list[QTask[C, V, Q, A]],
|
||||||
|
filter: Filter[C, V, Q, A],
|
||||||
|
ctx: DynamicCtx[C, V, Q, A],
|
||||||
|
) -> list[QTask[C, V, Q, A]]:
|
||||||
|
dynamic_filter_matches: list[QTask[C, V, Q, A]] = []
|
||||||
|
|
||||||
|
for task in statically_filtered_pool:
|
||||||
|
if filter.dynamic.check_if_satisfied(task, ctx):
|
||||||
|
dynamic_filter_matches.append(task)
|
||||||
|
|
||||||
|
return dynamic_filter_matches
|
||||||
BIN
modules/variant_builder/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
modules/variant_builder/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/variant_builder/__pycache__/context.cpython-313.pyc
Normal file
BIN
modules/variant_builder/__pycache__/context.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
modules/variant_builder/__pycache__/task_pool.cpython-313.pyc
Normal file
BIN
modules/variant_builder/__pycache__/task_pool.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
modules/variant_builder/__pycache__/variant_set.cpython-313.pyc
Normal file
BIN
modules/variant_builder/__pycache__/variant_set.cpython-313.pyc
Normal file
Binary file not shown.
BIN
modules/variant_builder/__pycache__/variant_task.cpython-313.pyc
Normal file
BIN
modules/variant_builder/__pycache__/variant_task.cpython-313.pyc
Normal file
Binary file not shown.
17
modules/variant_builder/context.py
Normal file
17
modules/variant_builder/context.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Generic
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DynamicCtx(Generic[C, V, Q, A]):
|
||||||
|
task_pool: QTaskPool[C, V, Q, A]
|
||||||
|
previous_variants: QVariantSet[C, V, Q, A] = field(
|
||||||
|
default_factory=QVariantSet[C, V, Q, A]
|
||||||
|
)
|
||||||
|
current_variant_tasks: list[QTask[C, V, Q, A]] = field(default_factory=list)
|
||||||
58
modules/variant_builder/default_task_selector.py
Normal file
58
modules/variant_builder/default_task_selector.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import uuid
|
||||||
|
from math import inf
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
from modules.utils.utils import rnd
|
||||||
|
from modules.variant_builder.context import DynamicCtx
|
||||||
|
from modules.variant_builder.task_selector import QTaskSelector
|
||||||
|
|
||||||
|
|
||||||
|
class LeastUsedTaskSelector(QTaskSelector[C, V, Q, A]):
|
||||||
|
task_usage_count: dict[uuid.UUID, int]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.task_usage_count = {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
def select(
|
||||||
|
self, filtered_tasks: list[QTask[C, V, Q, A]], ctx: DynamicCtx[C, V, Q, A]
|
||||||
|
) -> QTask[C, V, Q, A]:
|
||||||
|
rnd.shuffle(filtered_tasks)
|
||||||
|
|
||||||
|
task_scores: list[int] = []
|
||||||
|
|
||||||
|
min_max_intersections = inf
|
||||||
|
for task in filtered_tasks:
|
||||||
|
max_intersections = 0
|
||||||
|
for variant in ctx.previous_variants:
|
||||||
|
intersections = 0
|
||||||
|
for t in ctx.current_variant_tasks:
|
||||||
|
if t in variant.tasks:
|
||||||
|
intersections += 1
|
||||||
|
if task in variant.tasks:
|
||||||
|
intersections += 1
|
||||||
|
if intersections > max_intersections:
|
||||||
|
max_intersections = intersections
|
||||||
|
task_scores.append(max_intersections)
|
||||||
|
if min_max_intersections > max_intersections:
|
||||||
|
min_max_intersections = max_intersections
|
||||||
|
|
||||||
|
best_candidates: list[QTask[C, V, Q, A]] = []
|
||||||
|
|
||||||
|
for task, score in zip(filtered_tasks, task_scores):
|
||||||
|
if score == min_max_intersections:
|
||||||
|
best_candidates.append(task)
|
||||||
|
|
||||||
|
least_used_score = inf
|
||||||
|
least_used_task: QTask[C, V, Q, A] = best_candidates[0]
|
||||||
|
|
||||||
|
for task in best_candidates:
|
||||||
|
if self.task_usage_count.setdefault(task.id, 0) < least_used_score:
|
||||||
|
least_used_score = self.task_usage_count[task.id]
|
||||||
|
least_used_task = task
|
||||||
|
|
||||||
|
self.task_usage_count[least_used_task.id] += 1
|
||||||
|
|
||||||
|
return least_used_task
|
||||||
28
modules/variant_builder/filters/__init__.py
Normal file
28
modules/variant_builder/filters/__init__.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Generic
|
||||||
|
|
||||||
|
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 Filter(Generic[C, V, Q, A]):
|
||||||
|
static: FilterStatic[C, V, Q, A]
|
||||||
|
dynamic: FilterDynamic[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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
122
modules/variant_builder/filters/builder.py
Normal file
122
modules/variant_builder/filters/builder.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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 import Filter
|
||||||
|
from modules.variant_builder.filters.dynamic import FilterDynamic
|
||||||
|
from modules.variant_builder.filters.static import FilterStatic
|
||||||
|
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))
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def include_tag(self, category: C, value: V) -> Self: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def include_tag(self, category: Tag[C, V], value: None = None) -> Self: ...
|
||||||
|
|
||||||
|
def include_tag(self, category: C | Tag[C, V], value: V | None = None) -> Self:
|
||||||
|
if isinstance(category, Tag):
|
||||||
|
return self.include_all_tags(category)
|
||||||
|
else:
|
||||||
|
assert value is not None
|
||||||
|
return self.include_all_tags([Tag(category, value)])
|
||||||
|
|
||||||
|
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(self) -> Filter[C, V, Q, A]:
|
||||||
|
return Filter(self.static_filters, self.dynamic_filters)
|
||||||
|
|
||||||
|
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()
|
||||||
45
modules/variant_builder/filters/dynamic/__init__.py
Normal file
45
modules/variant_builder/filters/dynamic/__init__.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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.context import DynamicCtx
|
||||||
|
from modules.variant_builder.task_pool import QTaskPool
|
||||||
|
from modules.variant_builder.variant_set import QVariantSet
|
||||||
|
|
||||||
|
|
||||||
|
class FilterDynamic(ABC, Generic[C, V, Q, A]):
|
||||||
|
@abstractmethod
|
||||||
|
def check_if_satisfied(
|
||||||
|
self, task: QTask[C, V, Q, A], ctx: DynamicCtx[C, V, Q, A]
|
||||||
|
) -> 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], ctx: DynamicCtx[C, V, Q, A]
|
||||||
|
) -> bool:
|
||||||
|
return not self.filter.check_if_satisfied(task, ctx)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __invert__(self) -> "FilterDynamic[C, V, Q, A]":
|
||||||
|
return self.filter
|
||||||
|
|
||||||
|
@override
|
||||||
|
def invert(self) -> "FilterDynamic[C, V, Q, A]":
|
||||||
|
return self.filter
|
||||||
Binary file not shown.
Binary file not shown.
27
modules/variant_builder/filters/dynamic/composite.py
Normal file
27
modules/variant_builder/filters/dynamic/composite.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from dataclasses import dataclass, field, replace
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
from modules.variant_builder.context import DynamicCtx
|
||||||
|
from modules.variant_builder.filters.dynamic import FilterDynamic
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompositeFilterDynamic(FilterDynamic[C, V, Q, A]):
|
||||||
|
filters: list[FilterDynamic[C, V, Q, A]]
|
||||||
|
is_inverted: bool = field(default=False)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def check_if_satisfied(
|
||||||
|
self, task: QTask[C, V, Q, A], ctx: DynamicCtx[C, V, Q, A]
|
||||||
|
) -> bool:
|
||||||
|
return all(filter.check_if_satisfied(task, ctx) for filter in self.filters)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def invert(self) -> "CompositeFilterDynamic[C, V, Q, A]":
|
||||||
|
return replace(self, is_inverted=not self.is_inverted)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __invert__(self) -> "CompositeFilterDynamic[C, V, Q, A]":
|
||||||
|
return self.invert()
|
||||||
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
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
28
modules/variant_builder/filters/static/composite.py
Normal file
28
modules/variant_builder/filters/static/composite.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def __invert__(self) -> "CompositeFilterStatic[C, V, Q, A]":
|
||||||
|
return self.invert()
|
||||||
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)
|
||||||
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]
|
||||||
11
modules/variant_builder/task_pool.py
Normal file
11
modules/variant_builder/task_pool.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import random
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Generic
|
||||||
|
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
|
||||||
|
|
||||||
|
class QTaskPool(list[QTask[C, V, Q, A]], Generic[C, V, Q, A]):
|
||||||
|
def shuffle(self):
|
||||||
|
random.shuffle(self)
|
||||||
15
modules/variant_builder/task_selector.py
Normal file
15
modules/variant_builder/task_selector.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Generic, Protocol
|
||||||
|
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
from modules.variant_builder.context import DynamicCtx
|
||||||
|
from modules.variant_builder.task_pool import QTaskPool
|
||||||
|
|
||||||
|
|
||||||
|
class QTaskSelector(ABC, Generic[C, V, Q, A]):
|
||||||
|
@abstractmethod
|
||||||
|
def select(
|
||||||
|
self, filtered_tasks: list[QTask[C, V, Q, A]], ctx: DynamicCtx[C, V, Q, A]
|
||||||
|
) -> QTask[C, V, Q, A]: ...
|
||||||
9
modules/variant_builder/variant_set.py
Normal file
9
modules/variant_builder/variant_set.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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(list[QVariant[C, V, Q, A]], Generic[C, V, Q, A]): ...
|
||||||
15
modules/variant_builder/variant_task.py
Normal file
15
modules/variant_builder/variant_task.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Generic
|
||||||
|
|
||||||
|
from option import Option
|
||||||
|
|
||||||
|
from modules.utils.types import A, C, Q, V
|
||||||
|
from modules.variant_builder.filters import Filter
|
||||||
|
from modules.variant_builder.filters.builder import FilterBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class VariantTask(Generic[C, V, Q, A]):
|
||||||
|
must: FilterBuilder[C, V, Q, A]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.must = FilterBuilder[C, V, Q, A]()
|
||||||
@ -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
|
|
||||||
BIN
parserd.xlsx
Normal file
BIN
parserd.xlsx
Normal file
Binary file not shown.
29
poetry.lock
generated
29
poetry.lock
generated
@ -13,6 +13,18 @@ files = [
|
|||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "et-xmlfile"
|
||||||
|
version = "2.0.0"
|
||||||
|
description = "An implementation of lxml.xmlfile for the standard library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"},
|
||||||
|
{file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@ -41,6 +53,21 @@ files = [
|
|||||||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openpyxl"
|
||||||
|
version = "3.1.5"
|
||||||
|
description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"},
|
||||||
|
{file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
et-xmlfile = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option"
|
name = "option"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
@ -150,4 +177,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.8,<4"
|
python-versions = ">=3.8,<4"
|
||||||
content-hash = "aa060e205a9e141d4941339e4d9d39cde82de5f8d0900aca25f331ddf46e2a05"
|
content-hash = "af9b1bb0ddd8587d40decb8a6cb9a61735ebd8cd1354e18afd1bfa55aed0eb33"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "quizard"
|
name = "QuizGen"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [
|
authors = [
|
||||||
@ -9,7 +9,8 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.8,<4"
|
requires-python = ">=3.8,<4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"option (>=2.1.0,<3.0.0)",
|
"option (>=2.1.0,<3.0.0)",
|
||||||
"pytest (>=8.3.5,<9.0.0)"
|
"pytest (>=8.3.5,<9.0.0)",
|
||||||
|
"openpyxl (>=3.1.5,<4.0.0)"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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]: ...
|
|
||||||
BIN
samples/clean.xlsx
Normal file
BIN
samples/clean.xlsx
Normal file
Binary file not shown.
BIN
samples/Варианты ДКР2 2023 2024 2025.xlsx
Normal file
BIN
samples/Варианты ДКР2 2023 2024 2025.xlsx
Normal file
Binary file not shown.
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
|
|
||||||
Binary file not shown.
43
variants.py
Normal file
43
variants.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
from modules.tag import Tag
|
||||||
|
from modules.task import QTask
|
||||||
|
from modules.variant_builder import VariantFactory
|
||||||
|
|
||||||
|
wb = load_workbook(filename="./clean.xlsx")
|
||||||
|
ws = wb["data"]
|
||||||
|
|
||||||
|
for i in list(range(6 * 5))[::5]:
|
||||||
|
tasks: list[QTask] = []
|
||||||
|
|
||||||
|
name = str(ws.cell(column=1, row=(i + 1)).value)
|
||||||
|
|
||||||
|
wb.create_sheet(name)
|
||||||
|
ws_sp = wb[name]
|
||||||
|
|
||||||
|
for ind, col in enumerate(
|
||||||
|
ws.iter_cols(min_col=2, max_col=8, min_row=i + 1, max_row=i + 5)
|
||||||
|
):
|
||||||
|
for cell in col:
|
||||||
|
tasks.append(
|
||||||
|
QTask[str, str, str, str | None](
|
||||||
|
question=str(cell.value), tags=Tag("topic", str(ind))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
vf = VariantFactory(7, tasks)
|
||||||
|
_ = vf.task[0].must.include_tag("topic", "0")
|
||||||
|
_ = vf.task[1].must.include_tag("topic", "1")
|
||||||
|
_ = vf.task[2].must.include_tag("topic", "2")
|
||||||
|
_ = vf.task[3].must.include_tag("topic", "3")
|
||||||
|
_ = vf.task[4].must.include_tag("topic", "4")
|
||||||
|
_ = vf.task[5].must.include_tag("topic", "5")
|
||||||
|
_ = vf.task[6].must.include_tag("topic", "6")
|
||||||
|
variants = vf.generate_variants(number_of_variants=8)
|
||||||
|
|
||||||
|
for ind, variant in enumerate(variants):
|
||||||
|
cell = ws_sp.cell(column=1, row=ind + 1, value=f"Вариант {ind + 1}")
|
||||||
|
for task_ind, task in enumerate(variant.tasks):
|
||||||
|
cell = ws_sp.cell(column=task_ind + 2, row=ind + 1, value=task.question)
|
||||||
|
|
||||||
|
wb.save("parserd.xlsx")
|
||||||
BIN
варианты.xlsx
Normal file
BIN
варианты.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user