reworked a loooot

This commit is contained in:
2025-04-15 22:09:15 +03:00
parent 7704f0dd5c
commit 3cc871c3ff
19 changed files with 609 additions and 93 deletions

28
QuizTask.py Normal file
View File

@ -0,0 +1,28 @@
@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), "{}"))}"

View File

@ -1,19 +1,10 @@
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import (
Dict,
Generic,
List,
Optional,
Protocol,
Set,
Tuple,
TypeVar,
Union,
cast,
)
from typing import (Dict, Generic, List, Optional, Protocol, Set, Tuple,
TypeVar, Union, overload, runtime_checkable)
from option import NONE, Err, Ok, Option, Result, Some
from option import Err, Option, Result, Some
from utils.utils import indent
@ -28,39 +19,76 @@ class Tags(Generic[C, V]):
@staticmethod
def from_list(tags: List[Tuple[C, V]]) -> "Tags[C, V]":
tag_dict: Dict[C, Set[V]] = {}
for tag in tags:
tag_dict.setdefault(tag[0], set()).add(tag[1])
for cat, val in tags:
tag_dict.setdefault(cat, set()).add(val)
return Tags(tag_dict)
def has_tag(self, category: C, value: V) -> bool:
return value in self._dict.get(category, set())
@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 remove_tag(self, category: C, value: V) -> None:
self._dict.get(category, set()).discard(value)
def __str__(self):
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 = [v.value if isinstance(v, Enum) else str(v) for v in values]
value_list = ", ".join(str(v) for v in sorted(val_strs))
lines.append(f"{cat_str}: {value_list}")
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: Option[Tags[C, V]]
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,
@ -70,47 +98,248 @@ class QuizTask(Generic[C, V]):
):
self.question = question
self.answer = answer
if tags is None:
self.tags = cast(Option[Tags[C, V]], NONE)
elif isinstance(tags, list):
self.tags = Some(Tags[C, V].from_list(tags))
else:
self.tags = Some(tags)
def has_tag(self, category: C, value: V) -> bool:
return self.tags.map_or(lambda t: t.has_tag(category, value), False)
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]: ...
@dataclass
class TaskPool(Generic[C, V]):
tasks: list[TaskGenerator[C, V]]
def __len__(self):
return len(self.tasks)
class TaskGeneratorPool(List[TaskGenerator[C, V]], Generic[C, V]): ...
@dataclass
class QuizVariant:
tasks: list[QuizTask]
class TaskPool(List[QuizTask[C, V]], Generic[C, V]): ...
@dataclass
class VariantSet:
variants: list[QuizVariant]
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")
return Err("There must be at least one task in the pool")
return Err("Not implemented")

Binary file not shown.

Binary file not shown.

54
example.py Normal file
View File

@ -0,0 +1,54 @@
from enum import Enum
from typing import Generic
from option import Some
from Quizard import Quizard, QuizTask, QuizTaskGeneratorMetadata, TaskGenerator
class TagCategory(str, Enum):
TOPIC = "topic"
DIFFICULTY = "difficulty"
class TopicTag(str, Enum):
AVERAGE = "average"
VARIANCE = "variance"
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")

141
poetry.lock generated
View File

@ -1,5 +1,46 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "exceptiongroup"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "iniconfig"
version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]]
name = "option"
version = "2.1.0"
@ -12,23 +53,101 @@ files = [
{file = "option-2.1.0.tar.gz", hash = "sha256:9fe95a231e54724d2382a5124b55cd84b82339edf1d4e88d6977cedffbfeadf1"},
]
[package.dependencies]
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.8\""}
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version == \"3.7\""
files = [
{file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
{file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pytest"
version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "tomli"
version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
]
[metadata]
lock-version = "2.1"
python-versions = ">=3.7,<4"
content-hash = "ff4d54c7cdd727c8db07ae48de4b3afe30cf14fba5f0ea7832bb9ed39a0f0d48"
python-versions = ">=3.8,<4"
content-hash = "aa060e205a9e141d4941339e4d9d39cde82de5f8d0900aca25f331ddf46e2a05"

View File

@ -6,9 +6,10 @@ authors = [
{name = "ton1c",email = "sembl1@ya.ru"}
]
readme = "README.md"
requires-python = ">=3.7,<4"
requires-python = ">=3.8,<4"
dependencies = [
"option (>=2.1.0,<3.0.0)"
"option (>=2.1.0,<3.0.0)",
"pytest (>=8.3.5,<9.0.0)"
]

0
quiz_task.py Normal file
View File

32
quiz_task_generator.py Normal file
View File

@ -0,0 +1,32 @@
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]: ...

68
tags.py Normal file
View File

@ -0,0 +1,68 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union, overload
from utils.utils import C, V
type Tag[C, V] = Tuple[C, V]
@dataclass
class Tags(Dict[C, Set[V]], Generic[C, V]):
@staticmethod
def from_list(tags_list: List[Tag[C, V]]) -> "Tags[C, V]":
tags: Tags[C, V] = Tags()
for cat, val in tags_list:
tags.setdefault(cat, set()).add(val)
return tags
def has_tag_tuple(self, tag: Tag[C, V]) -> bool:
cat, val = tag
return val in self.get(cat, set())
@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, Tuple):
return self.has_tag_tuple(category)
else:
assert (
value is not None
), "Value must be provided if category is not a tuple"
return value in self.get(category, set())
def add_tag_tuple(self, tag: Tag[C, V]) -> None:
cat, val = tag
self.get(cat, set()).add(val)
@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, Tuple):
self.add_tag_tuple(category)
else:
assert (
value is not None
), "Value must be provided if category is not a tuple"
self.get(category, set()).add(value)
def __str__(self) -> str:
if len(self) == 0:
return "No tags"
lines = []
for category, values in self.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)

View File

@ -1,32 +0,0 @@
from enum import Enum
from typing import Generic
from Quizard import QuizTask, TaskGenerator
class TagCategory(str, Enum):
TOPIC = "topic"
DIFFICULTY = "difficulty"
class TopicTag(str, Enum):
AVERAGE = "average"
VARIANCE = "variance"
class MyTaskGenerator(TaskGenerator[TagCategory, TopicTag]): ...
class AverageTask(MyTaskGenerator):
def generate(self):
return QuizTask(
"What is an average of 1, 2, 3 and 4?",
"2.5",
tags=[
(TagCategory.TOPIC, TopicTag.AVERAGE),
(TagCategory.TOPIC, TopicTag.VARIANCE),
],
)
print(AverageTask().generate())

Binary file not shown.

6
tests/test_quizard.py Normal file
View File

@ -0,0 +1,6 @@
def func(x):
return x + 1
def test_answer():
assert func(3) == 5

Binary file not shown.

Binary file not shown.

5
utils/option.py Normal file
View File

@ -0,0 +1,5 @@
T = TypeVar("T")
def to_option(value: Optional[T]) -> Option[T]:
return Some()

View File

@ -1,3 +1,9 @@
from typing import TypeVar
C = TypeVar("C", default=str)
V = TypeVar("V", default=str)
def indent(text: str, spaces: int = 2) -> str:
prefix = " " * spaces
return "\n".join(prefix + line for line in text.splitlines())