feat(ttp): E.1.11 RuleStore contract — base ABC, factory, filesystem + database stubs
Adds decnet/ttp/store/ subpackage: - base.py: RuleState frozen dataclass, RuleChange NamedTuple, RuleStore ABC - factory.py: get_rule_store() reading DECNET_TTP_RULE_STORE_TYPE - impl/filesystem.py: FilesystemRuleStore with sys.platform=='linux' fail-fast guard, allowlist filename regex, raw inotify mask bits (lib import deferred to E.3 so contract phase compiles without the asyncinotify dep installed) - impl/database.py: DatabaseRuleStore stub (no platform guard) TTPRule + TTPRuleState SQLModels were already shipped at E.1.1; this commit closes the type-only TYPE_CHECKING forward-ref in rule_engine.py via real runtime imports through the new package.
This commit is contained in:
25
decnet/ttp/store/__init__.py
Normal file
25
decnet/ttp/store/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""TTP rule store — pluggable backend for rule definitions + state.
|
||||||
|
|
||||||
|
Contract step E.1.11 of ``development/TTP_TAGGING.md``. Two backends
|
||||||
|
ship:
|
||||||
|
|
||||||
|
* :class:`FilesystemRuleStore` — reads ``./rules/ttp/`` at projroot,
|
||||||
|
inotify-watches for hot-reload, holds operational state in-process.
|
||||||
|
Linux-only (the inotify dependency is non-portable by design).
|
||||||
|
* :class:`DatabaseRuleStore` — mirrors rule content into ``ttp_rule``
|
||||||
|
with state in ``ttp_rule_state``; survives restart and propagates
|
||||||
|
to every worker in a swarm.
|
||||||
|
|
||||||
|
Selection via ``DECNET_TTP_RULE_STORE_TYPE`` (default ``"filesystem"``).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decnet.ttp.store.base import RuleChange, RuleState, RuleStore
|
||||||
|
from decnet.ttp.store.factory import get_rule_store
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"RuleChange",
|
||||||
|
"RuleState",
|
||||||
|
"RuleStore",
|
||||||
|
"get_rule_store",
|
||||||
|
]
|
||||||
149
decnet/ttp/store/base.py
Normal file
149
decnet/ttp/store/base.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
"""Rule store ABC + change/state value types.
|
||||||
|
|
||||||
|
Contract step E.1.11. The two backends (``impl/filesystem.py``,
|
||||||
|
``impl/database.py``) implement :class:`RuleStore` identically — the
|
||||||
|
E.2.14b conformance suite parametrizes over both and asserts the same
|
||||||
|
observable behavior.
|
||||||
|
|
||||||
|
Three types live here:
|
||||||
|
|
||||||
|
* :class:`RuleState` — operator-mutable knobs (enabled / disabled /
|
||||||
|
clipped, optional confidence ceiling, optional TTL). Frozen dataclass
|
||||||
|
so an entry sitting in an engine dispatch index cannot be torn by an
|
||||||
|
in-place mutation.
|
||||||
|
* :class:`RuleChange` — one event yielded per per-rule change by
|
||||||
|
:meth:`RuleStore.subscribe_changes`. The "incremental, never batched"
|
||||||
|
property in TTP_TAGGING.md §"Bus topics" is enforced *here*: the
|
||||||
|
store yields one change per edit, never an aggregate.
|
||||||
|
* :class:`RuleStore` — the four-method ABC: load all compiled rules,
|
||||||
|
read/write state, subscribe to changes.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal, NamedTuple, Union
|
||||||
|
|
||||||
|
from decnet.ttp.impl.rule_engine import CompiledRule
|
||||||
|
|
||||||
|
|
||||||
|
# ── Operational state ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RuleState:
|
||||||
|
"""Operator-controlled state stamped onto a :class:`CompiledRule`.
|
||||||
|
|
||||||
|
Frozen so engines reading the value during an evaluate() call see
|
||||||
|
a consistent snapshot even if a parallel ``set_state()`` is in
|
||||||
|
flight. The default constructor — ``RuleState()`` — is the
|
||||||
|
"enabled, no overrides" baseline that
|
||||||
|
:meth:`RuleStore.get_state` returns for any rule that has never
|
||||||
|
had operational state set.
|
||||||
|
|
||||||
|
Fields mirror the columns of :class:`TTPRuleState` so the
|
||||||
|
DB-backed store round-trips without translation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
state: Literal["enabled", "disabled", "clipped"] = "enabled"
|
||||||
|
#: Optional confidence ceiling. ``None`` means "use rule's base".
|
||||||
|
#: When set, the engine clamps the emitted tag's confidence
|
||||||
|
#: downward (never upward) per TTP_TAGGING.md §"Confidence model".
|
||||||
|
confidence_max: float | None = None
|
||||||
|
#: Optional TTL on the state itself. When ``expires_at`` is in the
|
||||||
|
#: past, the store returns the default enabled state and emits a
|
||||||
|
#: ``ttp.rule.state.{rule_id}`` auto-revert event.
|
||||||
|
expires_at: datetime | None = None
|
||||||
|
#: Free-form operator note (audit trail). Never PII.
|
||||||
|
reason: str | None = None
|
||||||
|
#: Operator who made the change ("filesystem" / "git" for the FS
|
||||||
|
#: store; the admin JWT subject for the DB store).
|
||||||
|
set_by: str | None = None
|
||||||
|
set_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Change events ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class RuleChange(NamedTuple):
|
||||||
|
"""One per-rule change yielded by :meth:`RuleStore.subscribe_changes`.
|
||||||
|
|
||||||
|
The ``change_kind`` discriminator pairs with the union type of
|
||||||
|
:attr:`new_value`:
|
||||||
|
|
||||||
|
* ``"definition"`` → ``new_value`` is a :class:`CompiledRule`
|
||||||
|
(the YAML changed; engine atomically swaps the entry in its
|
||||||
|
dispatch index).
|
||||||
|
* ``"state"`` → ``new_value`` is a :class:`RuleState` (only
|
||||||
|
operational state changed; the engine restamps the existing
|
||||||
|
compiled rule's ``state`` field).
|
||||||
|
|
||||||
|
The store NEVER batches: a 5-rule edit produces 5 :class:`RuleChange`
|
||||||
|
instances, not one carrying 5 entries. This is load-bearing — the
|
||||||
|
bus per-rule fan-out (``ttp.rule.reloaded.{rule_id}`` /
|
||||||
|
``ttp.rule.state.{rule_id}``) inherits its granularity from this
|
||||||
|
iterator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
change_kind: Literal["definition", "state"]
|
||||||
|
rule_id: str
|
||||||
|
new_value: Union[CompiledRule, RuleState]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Store ABC ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class RuleStore(ABC):
|
||||||
|
"""Pluggable backend for rule definitions + operational state.
|
||||||
|
|
||||||
|
Implementations land at :mod:`decnet.ttp.store.impl.filesystem`
|
||||||
|
and :mod:`decnet.ttp.store.impl.database`. Both must satisfy the
|
||||||
|
E.2.14b conformance contract observably — the test suite is
|
||||||
|
parametrized over both backends and asserts identical behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def load_compiled(self) -> list[CompiledRule]:
|
||||||
|
"""Return every rule this store knows about, fully compiled.
|
||||||
|
|
||||||
|
Includes operational state stamped onto each rule's ``state``
|
||||||
|
field (defaulting to enabled for rules without an explicit
|
||||||
|
state row). Called once at engine startup; per-rule edits
|
||||||
|
thereafter come through :meth:`subscribe_changes`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_state(self, rule_id: str) -> RuleState:
|
||||||
|
"""Return the current :class:`RuleState` for *rule_id*.
|
||||||
|
|
||||||
|
For an unknown rule_id (no state row exists) MUST return the
|
||||||
|
default ``RuleState()`` — never raise, never return ``None``.
|
||||||
|
Auto-reverts an expired state to default and emits a
|
||||||
|
``ttp.rule.state.{rule_id}`` event before returning.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_state(
|
||||||
|
self,
|
||||||
|
rule_id: str,
|
||||||
|
state: RuleState,
|
||||||
|
set_by: str,
|
||||||
|
) -> None:
|
||||||
|
"""Persist the new operational state and emit a change event.
|
||||||
|
|
||||||
|
On a backend failure (DB write error, disk full) MUST raise —
|
||||||
|
operational state changes are NOT a tolerated-absence path.
|
||||||
|
State drift would be silent and dangerous.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def subscribe_changes(self) -> AsyncIterator[RuleChange]:
|
||||||
|
"""Yield one :class:`RuleChange` per per-rule edit.
|
||||||
|
|
||||||
|
Never batches. A 5-rule edit produces 5 yields; a 50-rule
|
||||||
|
deploy produces 50. Subscribers (the engine, bus republishers)
|
||||||
|
rely on per-rule granularity — collapsing into a batch breaks
|
||||||
|
the ``ttp.rule.reloaded.{rule_id}`` topic fan-out.
|
||||||
|
"""
|
||||||
47
decnet/ttp/store/factory.py
Normal file
47
decnet/ttp/store/factory.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Rule store factory.
|
||||||
|
|
||||||
|
Mirrors :mod:`decnet.ttp.factory` and :mod:`decnet.intel.factory`:
|
||||||
|
callers obtain the active store via :func:`get_rule_store` rather than
|
||||||
|
instantiating a concrete class. The selected backend is whatever
|
||||||
|
``DECNET_TTP_RULE_STORE_TYPE`` resolves to (default ``"filesystem"``).
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* ``DECNET_TTP_RULE_STORE_TYPE`` — ``"filesystem"`` (Linux-only) or
|
||||||
|
``"database"`` (any platform). Unknown values raise
|
||||||
|
:class:`ValueError`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from decnet.ttp.store.base import RuleStore
|
||||||
|
|
||||||
|
_KNOWN: Final[tuple[str, ...]] = ("filesystem", "database")
|
||||||
|
_DEFAULT: Final[str] = "filesystem"
|
||||||
|
|
||||||
|
|
||||||
|
def get_rule_store() -> RuleStore:
|
||||||
|
"""Return the configured rule store instance.
|
||||||
|
|
||||||
|
The filesystem backend imports :mod:`asyncinotify` at construction
|
||||||
|
time and refuses to run on non-Linux platforms (per TTP_TAGGING.md
|
||||||
|
§"Linux-only worker host"). macOS / Windows developers running the
|
||||||
|
test suite set ``DECNET_TTP_RULE_STORE_TYPE=database``.
|
||||||
|
"""
|
||||||
|
name = os.environ.get(
|
||||||
|
"DECNET_TTP_RULE_STORE_TYPE", _DEFAULT,
|
||||||
|
).strip().lower()
|
||||||
|
if name == "filesystem":
|
||||||
|
from decnet.ttp.store.impl.filesystem import FilesystemRuleStore
|
||||||
|
return FilesystemRuleStore()
|
||||||
|
if name == "database":
|
||||||
|
from decnet.ttp.store.impl.database import DatabaseRuleStore
|
||||||
|
return DatabaseRuleStore()
|
||||||
|
raise ValueError(
|
||||||
|
f"Unknown rule store: {name!r}. Known: {_KNOWN}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["get_rule_store"]
|
||||||
1
decnet/ttp/store/impl/__init__.py
Normal file
1
decnet/ttp/store/impl/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Rule store backend implementations — filesystem + database."""
|
||||||
60
decnet/ttp/store/impl/database.py
Normal file
60
decnet/ttp/store/impl/database.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Database-backed rule store — ``ttp_rule`` + ``ttp_rule_state``.
|
||||||
|
|
||||||
|
Contract step E.1.11. Bodies raise ``NotImplementedError``; the
|
||||||
|
backing tables (:class:`TTPRule`, :class:`TTPRuleState`) shipped at
|
||||||
|
E.1.1.
|
||||||
|
|
||||||
|
Right for swarm: master syncs filesystem changes into ``ttp_rule``,
|
||||||
|
workers tail the DB, state in ``ttp_rule_state`` survives restart and
|
||||||
|
propagates to every worker. Pick via
|
||||||
|
``DECNET_TTP_RULE_STORE_TYPE=database``.
|
||||||
|
|
||||||
|
No platform guard — works on macOS / Windows where the filesystem
|
||||||
|
backend's inotify dependency is unavailable.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from decnet.ttp.impl.rule_engine import CompiledRule
|
||||||
|
from decnet.ttp.store.base import RuleChange, RuleState, RuleStore
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseRuleStore(RuleStore):
|
||||||
|
"""``ttp_rule`` content + ``ttp_rule_state`` operational state.
|
||||||
|
|
||||||
|
Contract phase: every method raises ``NotImplementedError``. The
|
||||||
|
impl step (E.3) implements DB-tail subscription + master-side
|
||||||
|
filesystem→DB sync. Worker-side tailing reads via the existing
|
||||||
|
repository pattern; the master's filesystem-watch sync is
|
||||||
|
structurally a delta from :class:`FilesystemRuleStore` plus a
|
||||||
|
``ttp_rule`` upsert.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def load_compiled(self) -> list[CompiledRule]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"DatabaseRuleStore.load_compiled lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_state(self, rule_id: str) -> RuleState:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"DatabaseRuleStore.get_state lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_state(
|
||||||
|
self,
|
||||||
|
rule_id: str,
|
||||||
|
state: RuleState,
|
||||||
|
set_by: str,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"DatabaseRuleStore.set_state lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe_changes(self) -> AsyncIterator[RuleChange]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"DatabaseRuleStore.subscribe_changes lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["DatabaseRuleStore"]
|
||||||
150
decnet/ttp/store/impl/filesystem.py
Normal file
150
decnet/ttp/store/impl/filesystem.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
"""Filesystem-backed rule store — reads ``./rules/ttp/`` + inotify watch.
|
||||||
|
|
||||||
|
Contract step E.1.11. Bodies raise ``NotImplementedError``; the
|
||||||
|
constants and platform guard are real so E.2.14b conformance tests
|
||||||
|
can introspect them today.
|
||||||
|
|
||||||
|
Linux-only. The inotify dependency (``asyncinotify`` /
|
||||||
|
``inotify_simple``) is non-portable by design; macOS / Windows
|
||||||
|
developers running the test suite use the database backend by
|
||||||
|
setting ``DECNET_TTP_RULE_STORE_TYPE=database``. The factory check in
|
||||||
|
:meth:`__init__` enforces this with a one-line operator-readable
|
||||||
|
error rather than a deep stack trace from the inotify import.
|
||||||
|
|
||||||
|
The dependency import is **deferred** to :meth:`subscribe_changes`
|
||||||
|
during the contract phase so this module is importable without the
|
||||||
|
inotify package installed. The implementation step (E.3) moves the
|
||||||
|
import to module top per TTP_TAGGING.md §"Linux-only worker host" —
|
||||||
|
which is when the dependency is added to ``pyproject.toml``. At
|
||||||
|
contract phase the codebase compiles, mypy passes, and the constants
|
||||||
|
below are introspectable for E.2.14b tests without forcing operators
|
||||||
|
on macOS or CI machines without the lib to install it just to import
|
||||||
|
the package.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from decnet.ttp.impl.rule_engine import CompiledRule
|
||||||
|
from decnet.ttp.store.base import RuleChange, RuleState, RuleStore
|
||||||
|
|
||||||
|
|
||||||
|
# ── Filename allowlist ──────────────────────────────────────────────
|
||||||
|
# A path is accepted iff its basename FULLY matches this pattern. The
|
||||||
|
# allowlist (rather than a denylist) is deliberate per TTP_TAGGING.md
|
||||||
|
# §E.1.11: vim swap files (``.foo.yaml.swp``), atomic-save probes
|
||||||
|
# (``4913``), tilde backups (``foo.yaml~``), random tempfile
|
||||||
|
# conventions a future editor invents — all silently ignored, no
|
||||||
|
# parse, no log line. Denylists rot the moment an editor changes its
|
||||||
|
# scratch convention; the allowlist stops being clever.
|
||||||
|
_VALID_RULE_FILENAME: Final[re.Pattern[str]] = re.compile(
|
||||||
|
r"[A-Za-z0-9_]+\.ya?ml",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Inotify event mask ──────────────────────────────────────────────
|
||||||
|
# Bit values from ``<sys/inotify.h>`` (man inotify(7)). Inlined as
|
||||||
|
# raw ints so this module is importable without the inotify library
|
||||||
|
# at contract phase. The implementation step replaces these with the
|
||||||
|
# library-supplied constants on the same module-top import that lands
|
||||||
|
# the dep — same numeric value, same bitwise OR.
|
||||||
|
#
|
||||||
|
# Rationale per TTP_TAGGING.md §E.1.11 "Inotify event mask",
|
||||||
|
# verified against an actual ``strace`` of vim:
|
||||||
|
# IN_CLOSE_WRITE — vim writes in place; dominant save signal.
|
||||||
|
# IN_MOVED_TO — atomic-write editors (gedit, IDEs, deploy
|
||||||
|
# scripts) write tempfile then ``rename()``.
|
||||||
|
# IN_CREATE — brand-new rule file appears (``touch``, ``cp``).
|
||||||
|
# IN_DELETE — rule removed; engine drops it from the dispatch
|
||||||
|
# index and emits ``ttp.rule.reloaded.{rule_id}``.
|
||||||
|
_IN_CLOSE_WRITE: Final[int] = 0x00000008
|
||||||
|
_IN_MOVED_TO: Final[int] = 0x00000080
|
||||||
|
_IN_CREATE: Final[int] = 0x00000100
|
||||||
|
_IN_DELETE: Final[int] = 0x00000200
|
||||||
|
|
||||||
|
_INOTIFY_MASK: Final[int] = (
|
||||||
|
_IN_CLOSE_WRITE | _IN_MOVED_TO | _IN_CREATE | _IN_DELETE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Watch root ──────────────────────────────────────────────────────
|
||||||
|
# Resolved relative to the project root. Tests override via a tmp_path
|
||||||
|
# fixture to avoid touching the real ``./rules/`` during the suite.
|
||||||
|
_DEFAULT_RULES_DIR: Final[Path] = Path("./rules/ttp/")
|
||||||
|
|
||||||
|
|
||||||
|
class FilesystemRuleStore(RuleStore):
|
||||||
|
"""``./rules/ttp/`` + inotify watch + in-process state cache.
|
||||||
|
|
||||||
|
Right for single-host dev — state lost on restart is fine when the
|
||||||
|
operator is local. Swarms use :class:`DatabaseRuleStore` so state
|
||||||
|
survives restart and propagates across worker hosts.
|
||||||
|
|
||||||
|
Contract phase: every method raises ``NotImplementedError``. The
|
||||||
|
impl step (E.3) implements YAML parse + Pydantic validation +
|
||||||
|
inotify event loop + atomic per-rule swap into the dispatch index.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rules_dir: Path | None = None) -> None:
|
||||||
|
# Fail-fast platform guard. Per TTP_TAGGING.md §E.1.11: a
|
||||||
|
# one-line operator-readable error beats a deep stack trace
|
||||||
|
# from a downstream import.
|
||||||
|
if sys.platform != "linux":
|
||||||
|
raise RuntimeError(
|
||||||
|
"FilesystemRuleStore requires Linux for inotify; use "
|
||||||
|
"DatabaseRuleStore on this platform "
|
||||||
|
"(DECNET_TTP_RULE_STORE_TYPE=database).",
|
||||||
|
)
|
||||||
|
self._rules_dir: Path = rules_dir or _DEFAULT_RULES_DIR
|
||||||
|
# In-process state cache — lost on restart by design. The
|
||||||
|
# database backend persists across restarts; choosing this
|
||||||
|
# backend is choosing the trade-off.
|
||||||
|
self._state: dict[str, RuleState] = {}
|
||||||
|
|
||||||
|
async def load_compiled(self) -> list[CompiledRule]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"FilesystemRuleStore.load_compiled lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_state(self, rule_id: str) -> RuleState:
|
||||||
|
# Auto-revert expired states is impl-phase behavior; the
|
||||||
|
# in-memory dict lookup is the trivial part. Even the lookup
|
||||||
|
# belongs to E.3 so the contract surface stays uniformly
|
||||||
|
# NotImplementedError across both backends.
|
||||||
|
cached = self._state.get(rule_id)
|
||||||
|
if cached is None:
|
||||||
|
return RuleState()
|
||||||
|
if cached.expires_at is not None and cached.expires_at < datetime.now(
|
||||||
|
tz=cached.expires_at.tzinfo,
|
||||||
|
):
|
||||||
|
# Auto-revert path — full impl (event emission, cache
|
||||||
|
# purge) lands at E.3.
|
||||||
|
return RuleState()
|
||||||
|
return cached
|
||||||
|
|
||||||
|
async def set_state(
|
||||||
|
self,
|
||||||
|
rule_id: str,
|
||||||
|
state: RuleState,
|
||||||
|
set_by: str,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"FilesystemRuleStore.set_state lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe_changes(self) -> AsyncIterator[RuleChange]:
|
||||||
|
raise NotImplementedError(
|
||||||
|
"FilesystemRuleStore.subscribe_changes lands at E.3",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"FilesystemRuleStore",
|
||||||
|
"_INOTIFY_MASK",
|
||||||
|
"_VALID_RULE_FILENAME",
|
||||||
|
]
|
||||||
@@ -2374,6 +2374,8 @@ unrelated events.
|
|||||||
**E.1.11 — RuleStore contract**
|
**E.1.11 — RuleStore contract**
|
||||||
(`decnet/ttp/store/{base.py, factory.py, impl/}`)
|
(`decnet/ttp/store/{base.py, factory.py, impl/}`)
|
||||||
|
|
||||||
|
**Status:** ✅ done.
|
||||||
|
|
||||||
- `class RuleState` frozen dataclass: state literal
|
- `class RuleState` frozen dataclass: state literal
|
||||||
("enabled" | "disabled" | "clipped"), `confidence_max`,
|
("enabled" | "disabled" | "clipped"), `confidence_max`,
|
||||||
`expires_at`, `reason`, `set_by`, `set_at`. Default constructor
|
`expires_at`, `reason`, `set_by`, `set_at`. Default constructor
|
||||||
|
|||||||
Reference in New Issue
Block a user