functualize-state 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+ *.whl
11
+
12
+ # Agents
13
+ .spec/archive/
14
+ .spec/features/
15
+ .spec/scrutiny-reports/
16
+ .spec/.agentic-coding
17
+ .spec/STATE.md
18
+ .spec/PROJECT.md
19
+ .spec/REQUIREMENTS.md
20
+ .spec/ROADMAP.md
21
+ .opencode/
22
+
23
+
24
+ # Virtual environments
25
+ .venv/
26
+ venv/
27
+ ENV/
28
+
29
+ # Testing
30
+ .coverage
31
+ .pytest_cache/
32
+ htmlcov/
33
+ .hypothesis/
34
+ snapshot_report.html
35
+ _*_result*.txt
36
+ _debug.txt
37
+ _tui_debug.txt
38
+ _tui_eval_debug.txt
39
+
40
+ # IDE
41
+ .idea/
42
+ *.swp
43
+ *.swo
44
+ *~
45
+ *.code-workspace
46
+
47
+ # Coding-agent tooling state (guards — these dirs are not part of the repo)
48
+ .kiro/
49
+ .moai/
50
+
51
+ # OS
52
+ .DS_Store
53
+ Thumbs.db
54
+
55
+ # Environment / secrets
56
+ .env
57
+ .env.*
58
+ !.env.example
59
+
60
+ # Agent scratch space (test output, temp scripts)
61
+ tmp/
62
+
63
+ # Local-only files (not for the repo)
64
+ *.local.md
65
+ *.local.*
66
+
67
+ # Personal notes
68
+ HUMAN_NOTE.md
69
+
70
+ # Distribution
71
+ dist/
72
+
73
+ # Documentation site build output
74
+ site/
75
+
76
+ # uv
77
+ .python-version
78
+ .functualize/cache.json
79
+ .functualize_cache.json
80
+ .todos/
81
+ .sidecar/
82
+ .sidecar-agent
83
+ .sidecar-task
84
+ .sidecar-pr
85
+ .sidecar-start.sh
86
+ .sidecar-base
87
+ .td-root
88
+ .functualize/
89
+ .import_linter_cache/
90
+ .mypy_cache/
91
+ .pytest_cache/
92
+ .ruff_cache/
93
+
94
+ # OmO / OpenCode agent run-continuation scratch state
95
+ .omo/
96
+ .mcp.json
97
+ .agentsroom/handoff-transcript-*.txt
98
+ .agentsroom/handoff-summary-*.md
99
+
100
+ # Internal pre-release audit reports (contain session IDs / local infra notes)
101
+ .release/
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: functualize-state
3
+ Version: 0.1.0
4
+ Summary: State Domain SDK providing protocols for state persistence and execution tracking
5
+ Author-email: Mohammad Hakim Adiprasetya <viltohmyst@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: pydantic>=2.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
17
+ Requires-Dist: pytest>=7.4.0; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # functualize-state
21
+
22
+ > **Status: Published** — Independently installable from PyPI.
23
+
24
+ State Domain SDK for functualize providing well-defined protocols for key-value state persistence and execution tracking. Enables custom storage backend implementations (SQLite, Redis, DynamoDB, etc.) without coupling your application to any specific database.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install functualize-state
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```python
35
+ from functualize_state import InMemoryState, StateNamespace
36
+
37
+ # Create a backend (use InMemoryState for testing, or implement StateBackend)
38
+ backend = InMemoryState()
39
+
40
+ # Use namespaces to isolate keys by concern
41
+ ns = StateNamespace(backend, prefix="deploy:")
42
+ ns.set("version", "1.2.0")
43
+ ns.set("status", "pending")
44
+
45
+ print(ns.get("version")) # "1.2.0"
46
+ print(ns.keys()) # ["version", "status"]
47
+ ```
48
+
49
+ ## Features
50
+
51
+ - **Backend-agnostic protocols** — `StateBackend` and `ExecutionStore` define the interface; bring your own storage implementation
52
+ - **Namespace isolation** — `StateNamespace` provides prefix-scoped views over any backend, preventing key collisions between components
53
+ - **Execution tracking** — Record job executions, phases, and session metadata with structured `ExecutionRecord` and `PhaseRecord` types
54
+ - **Event-driven observability** — Built-in event constants (`STATE_EXECUTION_STARTED`, `STATE_EXECUTION_COMPLETED`, `STATE_PHASE_CHANGED`) for lifecycle hooks
55
+ - **Test-friendly** — Ships `InMemoryState`, a dict-backed backend for fast, deterministic unit tests
56
+ - **Fully typed** — PEP 561 compliant with `py.typed` marker; all protocols are `@runtime_checkable`
57
+
58
+ ## API Reference
59
+
60
+ ### Protocols
61
+
62
+ - `StateBackend` — Key-value state operations protocol (`get`, `set`, `delete`, `keys`)
63
+ - `ExecutionStore` — Execution record persistence protocol (`insert_execution`, `update_execution`, `get_session_executions`, `insert_phase`, `get_execution_phases`)
64
+
65
+ ### Classes
66
+
67
+ - `StateNamespace` — Prefix-scoped view over a `StateBackend` for isolated key access
68
+ - `InMemoryState` — Dict-backed `StateBackend` implementation for testing
69
+ - `DomainMetadata` — Self-describing metadata dataclass for the state domain SDK
70
+
71
+ ### Data Types
72
+
73
+ - `ExecutionRecord` — Frozen dataclass representing a single job execution (id, job name, session, status, timing, result)
74
+ - `PhaseRecord` — Frozen dataclass representing a phase within an execution
75
+ - `SessionRecord` — Frozen dataclass representing a session with metadata
76
+
77
+ ### Errors
78
+
79
+ - `StateNotAvailable` — Raised when a state backend is not available
80
+ - `KeyNotFoundError` — Raised when a requested key is not found
81
+
82
+ ### Event Constants
83
+
84
+ - `STATE_EXECUTION_STARTED` — Fired when an execution begins
85
+ - `STATE_EXECUTION_COMPLETED` — Fired when an execution finishes
86
+ - `STATE_PHASE_CHANGED` — Fired when a phase transition occurs
87
+
88
+ ### Module-Level
89
+
90
+ - `domain_metadata` — Pre-configured `DomainMetadata` instance for the state domain
91
+
92
+ ## Development
93
+
94
+ Run plugin tests:
95
+
96
+ ```bash
97
+ uv run pytest plugins/functualize-state/tests/ -v
98
+ ```
@@ -0,0 +1,79 @@
1
+ # functualize-state
2
+
3
+ > **Status: Published** — Independently installable from PyPI.
4
+
5
+ State Domain SDK for functualize providing well-defined protocols for key-value state persistence and execution tracking. Enables custom storage backend implementations (SQLite, Redis, DynamoDB, etc.) without coupling your application to any specific database.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install functualize-state
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```python
16
+ from functualize_state import InMemoryState, StateNamespace
17
+
18
+ # Create a backend (use InMemoryState for testing, or implement StateBackend)
19
+ backend = InMemoryState()
20
+
21
+ # Use namespaces to isolate keys by concern
22
+ ns = StateNamespace(backend, prefix="deploy:")
23
+ ns.set("version", "1.2.0")
24
+ ns.set("status", "pending")
25
+
26
+ print(ns.get("version")) # "1.2.0"
27
+ print(ns.keys()) # ["version", "status"]
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - **Backend-agnostic protocols** — `StateBackend` and `ExecutionStore` define the interface; bring your own storage implementation
33
+ - **Namespace isolation** — `StateNamespace` provides prefix-scoped views over any backend, preventing key collisions between components
34
+ - **Execution tracking** — Record job executions, phases, and session metadata with structured `ExecutionRecord` and `PhaseRecord` types
35
+ - **Event-driven observability** — Built-in event constants (`STATE_EXECUTION_STARTED`, `STATE_EXECUTION_COMPLETED`, `STATE_PHASE_CHANGED`) for lifecycle hooks
36
+ - **Test-friendly** — Ships `InMemoryState`, a dict-backed backend for fast, deterministic unit tests
37
+ - **Fully typed** — PEP 561 compliant with `py.typed` marker; all protocols are `@runtime_checkable`
38
+
39
+ ## API Reference
40
+
41
+ ### Protocols
42
+
43
+ - `StateBackend` — Key-value state operations protocol (`get`, `set`, `delete`, `keys`)
44
+ - `ExecutionStore` — Execution record persistence protocol (`insert_execution`, `update_execution`, `get_session_executions`, `insert_phase`, `get_execution_phases`)
45
+
46
+ ### Classes
47
+
48
+ - `StateNamespace` — Prefix-scoped view over a `StateBackend` for isolated key access
49
+ - `InMemoryState` — Dict-backed `StateBackend` implementation for testing
50
+ - `DomainMetadata` — Self-describing metadata dataclass for the state domain SDK
51
+
52
+ ### Data Types
53
+
54
+ - `ExecutionRecord` — Frozen dataclass representing a single job execution (id, job name, session, status, timing, result)
55
+ - `PhaseRecord` — Frozen dataclass representing a phase within an execution
56
+ - `SessionRecord` — Frozen dataclass representing a session with metadata
57
+
58
+ ### Errors
59
+
60
+ - `StateNotAvailable` — Raised when a state backend is not available
61
+ - `KeyNotFoundError` — Raised when a requested key is not found
62
+
63
+ ### Event Constants
64
+
65
+ - `STATE_EXECUTION_STARTED` — Fired when an execution begins
66
+ - `STATE_EXECUTION_COMPLETED` — Fired when an execution finishes
67
+ - `STATE_PHASE_CHANGED` — Fired when a phase transition occurs
68
+
69
+ ### Module-Level
70
+
71
+ - `domain_metadata` — Pre-configured `DomainMetadata` instance for the state domain
72
+
73
+ ## Development
74
+
75
+ Run plugin tests:
76
+
77
+ ```bash
78
+ uv run pytest plugins/functualize-state/tests/ -v
79
+ ```
@@ -0,0 +1,13 @@
1
+ # functualize-state Examples
2
+
3
+ The state domain SDK: `StateBackend` protocol, `StateNamespace` scoping, and the `InMemoryState` testing double.
4
+
5
+ | Directory | Demonstrates |
6
+ |-----------|--------------|
7
+ | [`counter/`](counter/) | Namespace-scoped key-value state across job runs, using `InMemoryState` |
8
+
9
+ ```bash
10
+ uv run pytest plugins/functualize-state/examples/ -v
11
+ ```
12
+
13
+ For durable storage, see [`functualize-state-sqlite/examples/`](../../functualize-state-sqlite/examples/). To implement your own backend, see [`examples/plugins/custom_state_backend/`](../../../examples/plugins/custom_state_backend/).
@@ -0,0 +1,23 @@
1
+ """Namespace-scoped state: count how many times a job has run.
2
+
3
+ Uses the InMemoryState testing double directly so the example is
4
+ self-contained; in a real app the framework injects a `State` capability
5
+ backed by whichever state provider is installed (e.g. SQLite).
6
+ """
7
+
8
+ from functualize_state import StateNamespace
9
+ from functualize_state.testing import InMemoryState
10
+
11
+ from functualize.job import RunContext
12
+
13
+ # Shared backend for the module (a real app injects this per-scope)
14
+ _backend = InMemoryState()
15
+
16
+
17
+ def bump(rc: RunContext) -> int:
18
+ """Increment and report this job's run counter."""
19
+ state = StateNamespace(_backend, prefix="counter:")
20
+ count = state.get("runs", 0) + 1
21
+ state.set("runs", count)
22
+ rc.log(f"This job has now run {count} time(s)")
23
+ return count
@@ -0,0 +1,22 @@
1
+ """Tests for the counter example."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from unittest.mock import MagicMock
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent))
8
+
9
+ import counter
10
+
11
+
12
+ def test_counter_increments_across_runs():
13
+ rc = MagicMock()
14
+ first = counter.bump(rc)
15
+ second = counter.bump(rc)
16
+ assert second == first + 1
17
+
18
+
19
+ def test_namespace_isolates_keys():
20
+ # The raw backend key carries the namespace prefix
21
+ assert counter._backend.get("counter:runs") is not None
22
+ assert counter._backend.get("runs") is None
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "functualize-state"
3
+ version = "0.1.0"
4
+ description = "State Domain SDK providing protocols for state persistence and execution tracking"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Mohammad Hakim Adiprasetya", email = "viltohmyst@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "pydantic>=2.0.0",
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.entry-points."functualize.domains"]
24
+ state = "functualize_state._metadata:domain_metadata"
25
+
26
+ [project.optional-dependencies]
27
+ dev = [
28
+ "pytest>=7.4.0",
29
+ "pytest-cov>=4.1.0",
30
+ ]
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/functualize_state"]
@@ -0,0 +1,41 @@
1
+ """functualize-state: State Domain SDK for state persistence and execution tracking.
2
+
3
+ Provides well-defined protocols for state persistence and execution tracking,
4
+ enabling custom storage backend implementations without coupling to SQLite.
5
+ """
6
+
7
+ from functualize_state._errors import KeyNotFoundError, StateNotAvailableError
8
+ from functualize_state._events import (
9
+ STATE_EXECUTION_COMPLETED,
10
+ STATE_EXECUTION_STARTED,
11
+ STATE_PHASE_CHANGED,
12
+ )
13
+ from functualize_state._metadata import DomainMetadata, domain_metadata
14
+ from functualize_state._namespace import StateNamespace
15
+ from functualize_state._protocols import ExecutionStore, StateBackend
16
+ from functualize_state._types import ExecutionRecord, PhaseRecord, SessionRecord
17
+ from functualize_state.testing._in_memory import InMemoryState
18
+
19
+ __all__ = [
20
+ # Protocols
21
+ "StateBackend",
22
+ "ExecutionStore",
23
+ # Utility
24
+ "StateNamespace",
25
+ # Types
26
+ "ExecutionRecord",
27
+ "PhaseRecord",
28
+ "SessionRecord",
29
+ # Errors
30
+ "StateNotAvailableError",
31
+ "KeyNotFoundError",
32
+ # Event Constants
33
+ "STATE_EXECUTION_STARTED",
34
+ "STATE_EXECUTION_COMPLETED",
35
+ "STATE_PHASE_CHANGED",
36
+ # Testing Doubles
37
+ "InMemoryState",
38
+ # Metadata
39
+ "DomainMetadata",
40
+ "domain_metadata",
41
+ ]
@@ -0,0 +1,13 @@
1
+ """Error classes for the State Domain SDK."""
2
+
3
+
4
+ class StateNotAvailableError(Exception):
5
+ """Raised when a state backend is not available."""
6
+
7
+ pass
8
+
9
+
10
+ class KeyNotFoundError(Exception):
11
+ """Raised when a requested key is not found in the state backend."""
12
+
13
+ pass
@@ -0,0 +1,5 @@
1
+ """Event name constants for the State Domain SDK."""
2
+
3
+ STATE_EXECUTION_STARTED: str = "state.execution.started"
4
+ STATE_EXECUTION_COMPLETED: str = "state.execution.completed"
5
+ STATE_PHASE_CHANGED: str = "state.phase.changed"
@@ -0,0 +1,37 @@
1
+ """Domain metadata for the State SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class DomainMetadata:
10
+ """Self-describing metadata for a domain SDK."""
11
+
12
+ name: str
13
+ display_name: str
14
+ description: str
15
+ capability_class: str
16
+ provider_protocol: str
17
+ config_section: str
18
+ entry_point_group: str
19
+ events_prefix: str
20
+ scaffold_template: str | None = None
21
+ documentation_url: str | None = None
22
+ mock_factory: str | None = None
23
+
24
+
25
+ domain_metadata = DomainMetadata(
26
+ name="state",
27
+ display_name="State / Persistence",
28
+ description="State persistence and execution tracking",
29
+ capability_class="functualize_state.StateBackend",
30
+ provider_protocol="functualize_state.StateBackend",
31
+ config_section="state",
32
+ entry_point_group="functualize.state_providers",
33
+ events_prefix="state.",
34
+ scaffold_template=None,
35
+ documentation_url=None,
36
+ mock_factory="functualize_state.testing:InMemoryState",
37
+ )
@@ -0,0 +1,37 @@
1
+ """StateNamespace utility for prefix-scoped state operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from functualize_state._protocols import StateBackend
9
+
10
+
11
+ class StateNamespace:
12
+ """Prefix-scoped view over a StateBackend.
13
+
14
+ All key operations are transparently prefixed, providing isolated
15
+ read/write access to a specific key namespace within the backend.
16
+ """
17
+
18
+ def __init__(self, backend: StateBackend, prefix: str) -> None:
19
+ self._backend = backend
20
+ self._prefix = prefix
21
+
22
+ def get(self, key: str, default: Any = None) -> Any:
23
+ """Get a value by key, scoped to this namespace's prefix."""
24
+ return self._backend.get(self._prefix + key, default)
25
+
26
+ def set(self, key: str, value: Any) -> None:
27
+ """Set a value for a key, scoped to this namespace's prefix."""
28
+ self._backend.set(self._prefix + key, value)
29
+
30
+ def delete(self, key: str) -> None:
31
+ """Delete a key, scoped to this namespace's prefix."""
32
+ self._backend.delete(self._prefix + key)
33
+
34
+ def keys(self) -> list[str]:
35
+ """Return all keys in this namespace with the prefix stripped."""
36
+ prefix_len = len(self._prefix)
37
+ return [k[prefix_len:] for k in self._backend.keys(self._prefix)]
@@ -0,0 +1,56 @@
1
+ """Protocol definitions for the State Domain SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
6
+
7
+ if TYPE_CHECKING:
8
+ from functualize_state._types import ExecutionRecord, PhaseRecord
9
+
10
+
11
+ @runtime_checkable
12
+ class StateBackend(Protocol):
13
+ """Backend-agnostic key-value state operations."""
14
+
15
+ def get(self, key: str, default: Any = None) -> Any:
16
+ """Get a value by key, returning default if not found."""
17
+ ...
18
+
19
+ def set(self, key: str, value: Any) -> None:
20
+ """Set a value for a key."""
21
+ ...
22
+
23
+ def delete(self, key: str) -> None:
24
+ """Delete a key from the state backend."""
25
+ ...
26
+
27
+ def keys(self, prefix: str = "") -> list[str]:
28
+ """Return all keys, optionally filtered by prefix."""
29
+ ...
30
+
31
+
32
+ @runtime_checkable
33
+ class ExecutionStore(Protocol):
34
+ """Backend-agnostic execution record persistence operations."""
35
+
36
+ def insert_execution(self, record: ExecutionRecord) -> str:
37
+ """Insert an execution record, returning the execution ID."""
38
+ ...
39
+
40
+ def update_execution(self, execution_id: str, **updates: Any) -> None:
41
+ """Update fields on an existing execution record."""
42
+ ...
43
+
44
+ def get_session_executions(
45
+ self, session_id: str, limit: int = 50
46
+ ) -> list[ExecutionRecord]:
47
+ """Get execution records for a session, limited by count."""
48
+ ...
49
+
50
+ def insert_phase(self, execution_id: str, phase: PhaseRecord) -> None:
51
+ """Insert a phase record for an execution."""
52
+ ...
53
+
54
+ def get_execution_phases(self, execution_id: str) -> list[PhaseRecord]:
55
+ """Get all phase records for an execution."""
56
+ ...
@@ -0,0 +1,41 @@
1
+ """Shared types for the State Domain SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ExecutionRecord:
11
+ """A record of a single job execution."""
12
+
13
+ execution_id: str
14
+ job_name: str
15
+ session_id: str
16
+ status: str # "running" | "success" | "failure"
17
+ started_at: float
18
+ ended_at: float | None = None
19
+ duration_ms: float | None = None
20
+ kwargs: dict[str, Any] = field(default_factory=dict)
21
+ result: Any = None
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class PhaseRecord:
26
+ """A record of a phase within an execution."""
27
+
28
+ name: str
29
+ status: str
30
+ started_at: float
31
+ ended_at: float | None = None
32
+ duration_ms: float | None = None
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class SessionRecord:
37
+ """A record of a session."""
38
+
39
+ session_id: str
40
+ started_at: float
41
+ metadata: dict[str, Any] = field(default_factory=dict)
File without changes
@@ -0,0 +1,10 @@
1
+ """Testing doubles for the State Domain SDK.
2
+
3
+ Provides in-memory implementations usable without installing any implementation plugin.
4
+ """
5
+
6
+ from functualize_state.testing._in_memory import InMemoryState
7
+
8
+ __all__ = [
9
+ "InMemoryState",
10
+ ]
@@ -0,0 +1,34 @@
1
+ """In-memory StateBackend implementation for testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class InMemoryState:
9
+ """Dict-backed StateBackend for testing.
10
+
11
+ Satisfies the StateBackend protocol using a plain dict as the backing store.
12
+ Useful for unit tests and integration tests that don't need persistence.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ self._store: dict[str, Any] = {}
17
+
18
+ def get(self, key: str, default: Any = None) -> Any:
19
+ """Retrieve a value by key, returning default if not found."""
20
+ return self._store.get(key, default)
21
+
22
+ def set(self, key: str, value: Any) -> None:
23
+ """Store a value under the given key."""
24
+ self._store[key] = value
25
+
26
+ def delete(self, key: str) -> None:
27
+ """Remove a key from the store. No-op if key doesn't exist."""
28
+ self._store.pop(key, None)
29
+
30
+ def keys(self, prefix: str = "") -> list[str]:
31
+ """Return all keys matching the given prefix."""
32
+ if not prefix:
33
+ return list(self._store.keys())
34
+ return [k for k in self._store if k.startswith(prefix)]
File without changes
@@ -0,0 +1 @@
1
+ """Shared fixtures for functualize-state plugin tests."""
@@ -0,0 +1,53 @@
1
+ """Unit tests for functualize-state domain SDK.
2
+
3
+ Tests the state protocols, namespace isolation, and in-memory testing backend.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from functualize_state._namespace import StateNamespace
9
+ from functualize_state.testing._in_memory import InMemoryState
10
+
11
+
12
+ class TestInMemoryBackend:
13
+ """Tests for the InMemoryState test double."""
14
+
15
+ def test_get_missing_returns_default(self):
16
+ state = InMemoryState()
17
+ assert state.get("missing") is None
18
+ assert state.get("missing", "default") == "default"
19
+
20
+ def test_set_and_get(self):
21
+ state = InMemoryState()
22
+ state.set("key", "value")
23
+ assert state.get("key") == "value"
24
+
25
+ def test_delete(self):
26
+ state = InMemoryState()
27
+ state.set("key", "value")
28
+ state.delete("key")
29
+ assert state.get("key") is None
30
+
31
+
32
+ class TestStateNamespace:
33
+ """Tests for the StateNamespace prefix isolation."""
34
+
35
+ def test_prefixes_keys(self):
36
+ backend = InMemoryState()
37
+ ns = StateNamespace(backend, prefix="app.")
38
+ ns.set("name", "functualize")
39
+ # The key in the backend should be prefixed
40
+ assert backend.get("app.name") == "functualize"
41
+
42
+ def test_get_uses_prefix(self):
43
+ backend = InMemoryState()
44
+ backend.set("app.version", "1.0")
45
+ ns = StateNamespace(backend, prefix="app.")
46
+ assert ns.get("version") == "1.0"
47
+
48
+ def test_delete_uses_prefix(self):
49
+ backend = InMemoryState()
50
+ backend.set("app.key", "val")
51
+ ns = StateNamespace(backend, prefix="app.")
52
+ ns.delete("key")
53
+ assert backend.get("app.key") is None