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.
- functualize_state-0.1.0/.gitignore +101 -0
- functualize_state-0.1.0/PKG-INFO +98 -0
- functualize_state-0.1.0/README.md +79 -0
- functualize_state-0.1.0/examples/README.md +13 -0
- functualize_state-0.1.0/examples/counter/counter.py +23 -0
- functualize_state-0.1.0/examples/counter/test_counter.py +22 -0
- functualize_state-0.1.0/pyproject.toml +37 -0
- functualize_state-0.1.0/src/functualize_state/__init__.py +41 -0
- functualize_state-0.1.0/src/functualize_state/_errors.py +13 -0
- functualize_state-0.1.0/src/functualize_state/_events.py +5 -0
- functualize_state-0.1.0/src/functualize_state/_metadata.py +37 -0
- functualize_state-0.1.0/src/functualize_state/_namespace.py +37 -0
- functualize_state-0.1.0/src/functualize_state/_protocols.py +56 -0
- functualize_state-0.1.0/src/functualize_state/_types.py +41 -0
- functualize_state-0.1.0/src/functualize_state/py.typed +0 -0
- functualize_state-0.1.0/src/functualize_state/testing/__init__.py +10 -0
- functualize_state-0.1.0/src/functualize_state/testing/_in_memory.py +34 -0
- functualize_state-0.1.0/tests/__init__.py +0 -0
- functualize_state-0.1.0/tests/conftest.py +1 -0
- functualize_state-0.1.0/tests/test_plugin.py +53 -0
|
@@ -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,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,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
|