From 5aa151805ffbf1893961046dc0c742009b18d2eb Mon Sep 17 00:00:00 2001 From: ton1c Date: Fri, 11 Apr 2025 16:07:57 +0300 Subject: [PATCH] initial commit --- Quizard.py | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 34 ++++++++++++++ pyproject.toml | 17 +++++++ test_sample.py | 32 +++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 Quizard.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 test_sample.py diff --git a/Quizard.py b/Quizard.py new file mode 100644 index 0000000..abdc84d --- /dev/null +++ b/Quizard.py @@ -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") diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..91448ca --- /dev/null +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4612205 --- /dev/null +++ b/pyproject.toml @@ -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" diff --git a/test_sample.py b/test_sample.py new file mode 100644 index 0000000..70e38d8 --- /dev/null +++ b/test_sample.py @@ -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())