From 2a45cfe45e1cfb9d0589a506f78bd1187f67e785 Mon Sep 17 00:00:00 2001 From: ton1c Date: Fri, 18 Apr 2025 14:02:48 +0300 Subject: [PATCH] working on stuff (with great commit messages) --- modules/constrains/__init__.py | 106 ++++++++++++++++++ modules/constrains/dynamic/__init__.py | 21 ++++ modules/constrains/static/__init__.py | 13 +++ modules/constrains/static/must_be_any.py | 42 +++++++ .../static/must_include_all_tags.py | 37 ++++++ .../constrains/static/must_include_any_tag.py | 37 ++++++ modules/constrains/static/must_not.py | 13 +++ modules/constrains/types.py | 6 + modules/fabric.py | 15 +++ modules/fabric_metadata.py | 24 ++++ tags.py => modules/tags.py | 65 +++++++---- modules/task.py | 35 ++++++ modules/task_pool.py | 9 ++ modules/variant.py | 12 ++ modules/variant_set.py | 12 ++ quiz_task.py | 0 utils/types.py | 6 + 17 files changed, 429 insertions(+), 24 deletions(-) create mode 100644 modules/constrains/__init__.py create mode 100644 modules/constrains/dynamic/__init__.py create mode 100644 modules/constrains/static/__init__.py create mode 100644 modules/constrains/static/must_be_any.py create mode 100644 modules/constrains/static/must_include_all_tags.py create mode 100644 modules/constrains/static/must_include_any_tag.py create mode 100644 modules/constrains/static/must_not.py create mode 100644 modules/constrains/types.py create mode 100644 modules/fabric.py create mode 100644 modules/fabric_metadata.py rename tags.py => modules/tags.py (55%) create mode 100644 modules/task.py create mode 100644 modules/task_pool.py create mode 100644 modules/variant.py create mode 100644 modules/variant_set.py delete mode 100644 quiz_task.py create mode 100644 utils/types.py diff --git a/modules/constrains/__init__.py b/modules/constrains/__init__.py new file mode 100644 index 0000000..90e677d --- /dev/null +++ b/modules/constrains/__init__.py @@ -0,0 +1,106 @@ +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) diff --git a/modules/constrains/dynamic/__init__.py b/modules/constrains/dynamic/__init__.py new file mode 100644 index 0000000..0e48d11 --- /dev/null +++ b/modules/constrains/dynamic/__init__.py @@ -0,0 +1,21 @@ +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: ... diff --git a/modules/constrains/static/__init__.py b/modules/constrains/static/__init__.py new file mode 100644 index 0000000..cbf6297 --- /dev/null +++ b/modules/constrains/static/__init__.py @@ -0,0 +1,13 @@ +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: ... diff --git a/modules/constrains/static/must_be_any.py b/modules/constrains/static/must_be_any.py new file mode 100644 index 0000000..1efb2c6 --- /dev/null +++ b/modules/constrains/static/must_be_any.py @@ -0,0 +1,42 @@ +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]) diff --git a/modules/constrains/static/must_include_all_tags.py b/modules/constrains/static/must_include_all_tags.py new file mode 100644 index 0000000..889ba29 --- /dev/null +++ b/modules/constrains/static/must_include_all_tags.py @@ -0,0 +1,37 @@ +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) diff --git a/modules/constrains/static/must_include_any_tag.py b/modules/constrains/static/must_include_any_tag.py new file mode 100644 index 0000000..ffea0c9 --- /dev/null +++ b/modules/constrains/static/must_include_any_tag.py @@ -0,0 +1,37 @@ +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) diff --git a/modules/constrains/static/must_not.py b/modules/constrains/static/must_not.py new file mode 100644 index 0000000..bcbddb6 --- /dev/null +++ b/modules/constrains/static/must_not.py @@ -0,0 +1,13 @@ +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) diff --git a/modules/constrains/types.py b/modules/constrains/types.py new file mode 100644 index 0000000..39cfaec --- /dev/null +++ b/modules/constrains/types.py @@ -0,0 +1,6 @@ +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]] diff --git a/modules/fabric.py b/modules/fabric.py new file mode 100644 index 0000000..025fbc8 --- /dev/null +++ b/modules/fabric.py @@ -0,0 +1,15 @@ +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]: ... diff --git a/modules/fabric_metadata.py b/modules/fabric_metadata.py new file mode 100644 index 0000000..b5d2651 --- /dev/null +++ b/modules/fabric_metadata.py @@ -0,0 +1,24 @@ +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), + ) diff --git a/tags.py b/modules/tags.py similarity index 55% rename from tags.py rename to modules/tags.py index 632d16e..30da6f6 100644 --- a/tags.py +++ b/modules/tags.py @@ -1,24 +1,38 @@ from dataclasses import dataclass, field from enum import Enum -from typing import Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union, overload +from typing import ( + Dict, + Generic, + Iterable, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, + overload, + override, +) from utils.utils import C, V -type Tag[C, V] = Tuple[C, V] + +@dataclass(frozen=True) +class Tag(Generic[C, V]): + cat: C + val: 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 +class Tags(Generic[C, V]): + _dict: Dict[C, Set[V]] = {} - def has_tag_tuple(self, tag: Tag[C, V]) -> bool: - cat, val = tag - return val in self.get(cat, set()) + @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: ... @@ -27,17 +41,14 @@ class Tags(Dict[C, Set[V]], Generic[C, V]): 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) + 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.get(category, set()) - - def add_tag_tuple(self, tag: Tag[C, V]) -> None: - cat, val = tag - self.get(cat, set()).add(val) + return value in self._dict.get(category, set()) @overload def add_tag(self, category: C, value: V) -> None: ... @@ -46,23 +57,29 @@ class Tags(Dict[C, Set[V]], Generic[C, V]): 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) + 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.get(category, set()).add(value) + self._dict.get(category, set()).add(value) def __str__(self) -> str: - if len(self) == 0: + if len(self._dict) == 0: return "No tags" lines = [] - for category, values in self.items(): + 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) diff --git a/modules/task.py b/modules/task.py new file mode 100644 index 0000000..37ee010 --- /dev/null +++ b/modules/task.py @@ -0,0 +1,35 @@ +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))}" diff --git a/modules/task_pool.py b/modules/task_pool.py new file mode 100644 index 0000000..2f33e15 --- /dev/null +++ b/modules/task_pool.py @@ -0,0 +1,9 @@ +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]] diff --git a/modules/variant.py b/modules/variant.py new file mode 100644 index 0000000..b2692b0 --- /dev/null +++ b/modules/variant.py @@ -0,0 +1,12 @@ +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 diff --git a/modules/variant_set.py b/modules/variant_set.py new file mode 100644 index 0000000..0ff458a --- /dev/null +++ b/modules/variant_set.py @@ -0,0 +1,12 @@ +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 diff --git a/quiz_task.py b/quiz_task.py deleted file mode 100644 index e69de29..0000000 diff --git a/utils/types.py b/utils/types.py new file mode 100644 index 0000000..4f2a02b --- /dev/null +++ b/utils/types.py @@ -0,0 +1,6 @@ +from typing import TypeVar + +C = TypeVar("C", default=str) +V = TypeVar("V", default=str) +Q = TypeVar("Q", default=str) +A = TypeVar("A", default=Q)