initial commit

This commit is contained in:
2025-04-11 16:07:57 +03:00
commit 5aa151805f
4 changed files with 202 additions and 0 deletions

119
Quizard.py Normal file
View File

@ -0,0 +1,119 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import (
Dict,
Generic,
List,
Optional,
Protocol,
Set,
Tuple,
TypeVar,
Union,
cast,
)
from option import NONE, Err, Ok, Option, Result, Some
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 tag in tags:
tag_dict.setdefault(tag[0], set()).add(tag[1])
return Tags(tag_dict)
def has_tag(self, category: C, value: V) -> bool:
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):
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}")
return "\n".join(lines)
def indent(text: str, spaces: int = 2) -> str:
prefix = " " * spaces
return "\n".join(prefix + line for line in text.splitlines())
@dataclass
class QuizTask(Generic[C, V]):
question: str
answer: str
tags: Option[Tags[C, V]]
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 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)
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), "{}"))}"
class TaskGenerator(Protocol, Generic[C, V]):
def generate(self) -> QuizTask[C, V]: ...
@dataclass
class TaskPool(Generic[C, V]):
tasks: list[TaskGenerator[C, V]]
def __len__(self):
return len(self.tasks)
@dataclass
class QuizVariant:
tasks: list[QuizTask]
@dataclass
class VariantSet:
variants: list[QuizVariant]
class Quizard(Generic[C, V]):
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("Not implemented")

34
poetry.lock generated Normal file
View File

@ -0,0 +1,34 @@
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "option"
version = "2.1.0"
description = "Rust like Option and Result types in Python"
optional = false
python-versions = ">=3.7,<4"
groups = ["main"]
files = [
{file = "option-2.1.0-py3-none-any.whl", hash = "sha256:21ccd9a437dbee0341700367efb68e82065fd7a7dba09f8c3263cf2dc1a2b0e0"},
{file = "option-2.1.0.tar.gz", hash = "sha256:9fe95a231e54724d2382a5124b55cd84b82339edf1d4e88d6977cedffbfeadf1"},
]
[package.dependencies]
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.8\""}
[[package]]
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
optional = false
python-versions = ">=3.7"
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"},
]
[metadata]
lock-version = "2.1"
python-versions = ">=3.7,<4"
content-hash = "ff4d54c7cdd727c8db07ae48de4b3afe30cf14fba5f0ea7832bb9ed39a0f0d48"

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[project]
name = "quizard"
version = "0.1.0"
description = ""
authors = [
{name = "ton1c",email = "sembl1@ya.ru"}
]
readme = "README.md"
requires-python = ">=3.7,<4"
dependencies = [
"option (>=2.1.0,<3.0.0)"
]
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

32
test_sample.py Normal file
View File

@ -0,0 +1,32 @@
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())