reworked a loooot
This commit is contained in:
28
QuizTask.py
Normal file
28
QuizTask.py
Normal 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), "{}"))}"
|
||||
325
Quizard.py
325
Quizard.py
@ -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")
|
||||
|
||||
BIN
__pycache__/Quizard.cpython-313.pyc
Normal file
BIN
__pycache__/Quizard.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc
Normal file
BIN
__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc
Normal file
Binary file not shown.
54
example.py
Normal file
54
example.py
Normal 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
141
poetry.lock
generated
@ -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"
|
||||
|
||||
@ -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
0
quiz_task.py
Normal file
32
quiz_task_generator.py
Normal file
32
quiz_task_generator.py
Normal 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
68
tags.py
Normal 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)
|
||||
@ -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())
|
||||
BIN
tests/__pycache__/test_quizard.cpython-313-pytest-8.3.5.pyc
Normal file
BIN
tests/__pycache__/test_quizard.cpython-313-pytest-8.3.5.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc
Normal file
BIN
tests/__pycache__/test_sample.cpython-313-pytest-8.3.5.pyc
Normal file
Binary file not shown.
BIN
tests/__pycache__/tests.cpython-313.pyc
Normal file
BIN
tests/__pycache__/tests.cpython-313.pyc
Normal file
Binary file not shown.
6
tests/test_quizard.py
Normal file
6
tests/test_quizard.py
Normal file
@ -0,0 +1,6 @@
|
||||
def func(x):
|
||||
return x + 1
|
||||
|
||||
|
||||
def test_answer():
|
||||
assert func(3) == 5
|
||||
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/utils.cpython-313.pyc
Normal file
BIN
utils/__pycache__/utils.cpython-313.pyc
Normal file
Binary file not shown.
5
utils/option.py
Normal file
5
utils/option.py
Normal file
@ -0,0 +1,5 @@
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def to_option(value: Optional[T]) -> Option[T]:
|
||||
return Some()
|
||||
@ -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())
|
||||
|
||||
Reference in New Issue
Block a user