functualize-state-sqlite 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_sqlite-0.1.0/.gitignore +101 -0
- functualize_state_sqlite-0.1.0/PKG-INFO +88 -0
- functualize_state_sqlite-0.1.0/README.md +67 -0
- functualize_state_sqlite-0.1.0/examples/README.md +11 -0
- functualize_state_sqlite-0.1.0/examples/persistent_counter/persistent_counter.py +25 -0
- functualize_state_sqlite-0.1.0/examples/persistent_counter/test_persistent_counter.py +23 -0
- functualize_state_sqlite-0.1.0/pyproject.toml +43 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/__init__.py +13 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/_backend.py +160 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/_execution_store.py +379 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/_migrations.py +157 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/_plugin.py +205 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/plugin.py +345 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/py.typed +0 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/sqlite_backend.py +721 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/state_store.py +178 -0
- functualize_state_sqlite-0.1.0/src/functualize_state_sqlite/tracker.py +318 -0
- functualize_state_sqlite-0.1.0/tests/__init__.py +0 -0
- functualize_state_sqlite-0.1.0/tests/conftest.py +19 -0
- functualize_state_sqlite-0.1.0/tests/test_execution_state_integration.py +638 -0
- functualize_state_sqlite-0.1.0/tests/test_plugin.py +86 -0
- functualize_state_sqlite-0.1.0/tests/test_sqlite_backend.py +437 -0
- functualize_state_sqlite-0.1.0/tests/test_sqlite_state_store_properties.py +373 -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,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: functualize-state-sqlite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SQLite-backed state persistence and execution tracking plugin for functualize
|
|
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: functualize-state<1.0.0,>=0.1.0
|
|
15
|
+
Requires-Dist: functualize<1.0.0,>=0.1.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: hypothesis>=6.82.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# functualize-state-sqlite
|
|
23
|
+
|
|
24
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
25
|
+
|
|
26
|
+
SQLite-backed state persistence and execution tracking plugin for functualize. Implements the `StateBackend` and `ExecutionStore` protocols from `functualize-state`, providing durable key-value storage and full execution history using a local SQLite database in WAL mode. Zero external dependencies beyond the Python standard library's `sqlite3` module.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install functualize-state-sqlite
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from functualize_state_sqlite import SQLiteStateBackend
|
|
38
|
+
|
|
39
|
+
# Use as a context manager for automatic cleanup
|
|
40
|
+
with SQLiteStateBackend(db_path="my_app.db") as backend:
|
|
41
|
+
backend.set("deploy_count", 42)
|
|
42
|
+
backend.set("last_env", "staging")
|
|
43
|
+
|
|
44
|
+
count = backend.get("deploy_count")
|
|
45
|
+
print(f"Deployments: {count}")
|
|
46
|
+
|
|
47
|
+
keys = backend.keys(prefix="deploy")
|
|
48
|
+
print(f"Keys: {keys}")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- **Persistent key-value state** — JSON-encoded values stored in SQLite with automatic schema initialization
|
|
54
|
+
- **WAL mode for concurrency** — concurrent read access without blocking writes, suitable for multi-process environments
|
|
55
|
+
- **Execution tracking** — full session and execution history with nested invocation support and phase recording
|
|
56
|
+
- **Namespace-scoped state** — `SQLiteStateStore` provides per-job namespace isolation within workflow scopes
|
|
57
|
+
- **Context manager support** — all backends support `with` statements for automatic connection cleanup
|
|
58
|
+
- **Zero external dependencies** — uses only Python's built-in `sqlite3` module
|
|
59
|
+
- **Automatic schema migrations** — database schema is created and migrated transparently on first access
|
|
60
|
+
|
|
61
|
+
## API Reference
|
|
62
|
+
|
|
63
|
+
Public classes exported by this plugin:
|
|
64
|
+
|
|
65
|
+
- `SQLiteStateBackend` — Implements the `StateBackend` protocol with `get()`, `set()`, `delete()`, and `keys()` methods for persistent key-value storage using a dedicated `kv_state` table.
|
|
66
|
+
- `SQLiteExecutionStore` — Implements the `ExecutionStore` protocol for recording and querying execution records and phase tracking. Methods include `insert_execution()`, `update_execution()`, `get_session_executions()`, `insert_phase()`, and `get_execution_phases()`.
|
|
67
|
+
- `SQLiteStatePlugin` — Plugin that registers `SQLiteStateBackend` and `SQLiteExecutionStore` with the DI registry at boot time. Hooks into `APP_READY` and `ON_SCOPE_CREATED` lifecycle events.
|
|
68
|
+
- `ExecutionStatePlugin` — Full lifecycle plugin that tracks job executions automatically. Hooks into `BEFORE_JOB`, `AFTER_SUCCESS`, `AFTER_FAILURE`, `INVOKE_START`, `INVOKE_END`, and `ON_SCOPE_CREATED` for comprehensive execution history.
|
|
69
|
+
|
|
70
|
+
Internal classes (available via direct import but not part of the public protocol surface):
|
|
71
|
+
|
|
72
|
+
- `SQLiteBackend` — Low-level connection manager with WAL mode, schema initialization, and query helpers for sessions, executions, steps, and namespaced state.
|
|
73
|
+
- `SQLiteStateStore` — `StateStoreProtocol` implementation scoped to a `(scope_id, job_namespace)` pair, with `get()`, `set()`, `delete()`, `keys()`, `to_dict()`, `clear()`, and cross-job access via `get_job_state()`.
|
|
74
|
+
- `ExecutionTracker` — High-level session management and execution recording with automatic session resume based on TTL, and AI context summary generation via `to_ai_context()`.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
Run plugin tests:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run pytest plugins/functualize-state-sqlite/tests/ -v
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Build the package:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv build --package functualize-state-sqlite
|
|
88
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# functualize-state-sqlite
|
|
2
|
+
|
|
3
|
+
> **Status: Published** — Independently installable from PyPI.
|
|
4
|
+
|
|
5
|
+
SQLite-backed state persistence and execution tracking plugin for functualize. Implements the `StateBackend` and `ExecutionStore` protocols from `functualize-state`, providing durable key-value storage and full execution history using a local SQLite database in WAL mode. Zero external dependencies beyond the Python standard library's `sqlite3` module.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install functualize-state-sqlite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from functualize_state_sqlite import SQLiteStateBackend
|
|
17
|
+
|
|
18
|
+
# Use as a context manager for automatic cleanup
|
|
19
|
+
with SQLiteStateBackend(db_path="my_app.db") as backend:
|
|
20
|
+
backend.set("deploy_count", 42)
|
|
21
|
+
backend.set("last_env", "staging")
|
|
22
|
+
|
|
23
|
+
count = backend.get("deploy_count")
|
|
24
|
+
print(f"Deployments: {count}")
|
|
25
|
+
|
|
26
|
+
keys = backend.keys(prefix="deploy")
|
|
27
|
+
print(f"Keys: {keys}")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Persistent key-value state** — JSON-encoded values stored in SQLite with automatic schema initialization
|
|
33
|
+
- **WAL mode for concurrency** — concurrent read access without blocking writes, suitable for multi-process environments
|
|
34
|
+
- **Execution tracking** — full session and execution history with nested invocation support and phase recording
|
|
35
|
+
- **Namespace-scoped state** — `SQLiteStateStore` provides per-job namespace isolation within workflow scopes
|
|
36
|
+
- **Context manager support** — all backends support `with` statements for automatic connection cleanup
|
|
37
|
+
- **Zero external dependencies** — uses only Python's built-in `sqlite3` module
|
|
38
|
+
- **Automatic schema migrations** — database schema is created and migrated transparently on first access
|
|
39
|
+
|
|
40
|
+
## API Reference
|
|
41
|
+
|
|
42
|
+
Public classes exported by this plugin:
|
|
43
|
+
|
|
44
|
+
- `SQLiteStateBackend` — Implements the `StateBackend` protocol with `get()`, `set()`, `delete()`, and `keys()` methods for persistent key-value storage using a dedicated `kv_state` table.
|
|
45
|
+
- `SQLiteExecutionStore` — Implements the `ExecutionStore` protocol for recording and querying execution records and phase tracking. Methods include `insert_execution()`, `update_execution()`, `get_session_executions()`, `insert_phase()`, and `get_execution_phases()`.
|
|
46
|
+
- `SQLiteStatePlugin` — Plugin that registers `SQLiteStateBackend` and `SQLiteExecutionStore` with the DI registry at boot time. Hooks into `APP_READY` and `ON_SCOPE_CREATED` lifecycle events.
|
|
47
|
+
- `ExecutionStatePlugin` — Full lifecycle plugin that tracks job executions automatically. Hooks into `BEFORE_JOB`, `AFTER_SUCCESS`, `AFTER_FAILURE`, `INVOKE_START`, `INVOKE_END`, and `ON_SCOPE_CREATED` for comprehensive execution history.
|
|
48
|
+
|
|
49
|
+
Internal classes (available via direct import but not part of the public protocol surface):
|
|
50
|
+
|
|
51
|
+
- `SQLiteBackend` — Low-level connection manager with WAL mode, schema initialization, and query helpers for sessions, executions, steps, and namespaced state.
|
|
52
|
+
- `SQLiteStateStore` — `StateStoreProtocol` implementation scoped to a `(scope_id, job_namespace)` pair, with `get()`, `set()`, `delete()`, `keys()`, `to_dict()`, `clear()`, and cross-job access via `get_job_state()`.
|
|
53
|
+
- `ExecutionTracker` — High-level session management and execution recording with automatic session resume based on TTL, and AI context summary generation via `to_ai_context()`.
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
Run plugin tests:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv run pytest plugins/functualize-state-sqlite/tests/ -v
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Build the package:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv build --package functualize-state-sqlite
|
|
67
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# functualize-state-sqlite Examples
|
|
2
|
+
|
|
3
|
+
Durable SQLite-backed state (WAL mode, stdlib-only).
|
|
4
|
+
|
|
5
|
+
| Directory | Demonstrates |
|
|
6
|
+
|-----------|--------------|
|
|
7
|
+
| [`persistent_counter/`](persistent_counter/) | `SQLiteStateBackend` as a `StateBackend`: values survive across processes; keys listing by prefix |
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv run pytest plugins/functualize-state-sqlite/examples/ -v
|
|
11
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Durable state: a run counter that survives process restarts.
|
|
2
|
+
|
|
3
|
+
Run twice and watch the count keep climbing:
|
|
4
|
+
func persistent_counter.py bump
|
|
5
|
+
func persistent_counter.py bump
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from functualize_state_sqlite import SQLiteStateBackend
|
|
11
|
+
|
|
12
|
+
from functualize.job import RunContext
|
|
13
|
+
|
|
14
|
+
DB_PATH = Path(__file__).parent / "counter.db"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bump(rc: RunContext) -> int:
|
|
18
|
+
"""Increment a counter stored durably in SQLite."""
|
|
19
|
+
with SQLiteStateBackend(db_path=str(DB_PATH)) as backend:
|
|
20
|
+
count = backend.get("runs", 0) + 1
|
|
21
|
+
backend.set("runs", count)
|
|
22
|
+
backend.set("last_run_note", f"run #{count}")
|
|
23
|
+
rc.log(f"Persistent run count: {count}")
|
|
24
|
+
rc.log(f"Keys stored: {backend.keys()}")
|
|
25
|
+
return count
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Tests for the persistent 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 persistent_counter
|
|
10
|
+
from functualize_state_sqlite import SQLiteStateBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_counter_persists_across_backend_instances(tmp_path, monkeypatch):
|
|
14
|
+
db = tmp_path / "counter.db"
|
|
15
|
+
monkeypatch.setattr(persistent_counter, "DB_PATH", db)
|
|
16
|
+
|
|
17
|
+
rc = MagicMock()
|
|
18
|
+
assert persistent_counter.bump(rc) == 1
|
|
19
|
+
# New backend instance (fresh "process") still sees the stored value
|
|
20
|
+
assert persistent_counter.bump(rc) == 2
|
|
21
|
+
|
|
22
|
+
with SQLiteStateBackend(db_path=str(db)) as backend:
|
|
23
|
+
assert backend.get("runs") == 2
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "functualize-state-sqlite"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "SQLite-backed state persistence and execution tracking plugin for functualize"
|
|
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
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"functualize>=0.1.0,<1.0.0",
|
|
21
|
+
"functualize-state>=0.1.0,<1.0.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.entry-points."functualize.state_providers"]
|
|
25
|
+
sqlite = "functualize_state_sqlite:SQLiteStatePlugin"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=7.4.0",
|
|
30
|
+
"pytest-cov>=4.1.0",
|
|
31
|
+
"hypothesis>=6.82.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.uv.sources]
|
|
39
|
+
functualize = { workspace = true }
|
|
40
|
+
functualize-state = { workspace = true }
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/functualize_state_sqlite"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Functualize State SQLite Plugin - SQLite-backed state persistence and execution tracking."""
|
|
2
|
+
|
|
3
|
+
from functualize_state_sqlite._backend import SQLiteStateBackend
|
|
4
|
+
from functualize_state_sqlite._execution_store import SQLiteExecutionStore
|
|
5
|
+
from functualize_state_sqlite._plugin import SQLiteStatePlugin
|
|
6
|
+
from functualize_state_sqlite.plugin import ExecutionStatePlugin
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ExecutionStatePlugin",
|
|
10
|
+
"SQLiteExecutionStore",
|
|
11
|
+
"SQLiteStateBackend",
|
|
12
|
+
"SQLiteStatePlugin",
|
|
13
|
+
]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""SQLite StateBackend implementation.
|
|
2
|
+
|
|
3
|
+
Implements the StateBackend protocol from functualize-state using SQLite
|
|
4
|
+
in WAL mode for concurrent access. Values are JSON-encoded for storage.
|
|
5
|
+
Uses only stdlib sqlite3 (zero external dependencies).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import sqlite3
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
__all__ = ["SQLiteStateBackend"]
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_STATE_SCHEMA_SQL = """\
|
|
22
|
+
CREATE TABLE IF NOT EXISTS kv_state (
|
|
23
|
+
key TEXT PRIMARY KEY,
|
|
24
|
+
value TEXT NOT NULL,
|
|
25
|
+
updated_at REAL NOT NULL
|
|
26
|
+
);
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SQLiteStateBackend:
|
|
31
|
+
"""StateBackend protocol implementation backed by SQLite.
|
|
32
|
+
|
|
33
|
+
Provides persistent key-value state storage with JSON-encoded values.
|
|
34
|
+
Uses WAL mode for concurrent read access without blocking writes.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
db_path: Path to the SQLite database file. If None, defaults to
|
|
38
|
+
`.functualize/state.db` relative to the current working directory.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, db_path: str | Path | None = None) -> None:
|
|
42
|
+
if db_path is None:
|
|
43
|
+
self._db_path = Path.cwd() / ".functualize" / "state.db"
|
|
44
|
+
else:
|
|
45
|
+
self._db_path = Path(db_path)
|
|
46
|
+
|
|
47
|
+
self._conn: sqlite3.Connection | None = None
|
|
48
|
+
self._initialized = False
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def db_path(self) -> Path:
|
|
52
|
+
"""The resolved path to the database file."""
|
|
53
|
+
return self._db_path
|
|
54
|
+
|
|
55
|
+
def _ensure_initialized(self) -> sqlite3.Connection:
|
|
56
|
+
"""Ensure the database is initialized and return the connection."""
|
|
57
|
+
if self._conn is not None and self._initialized:
|
|
58
|
+
return self._conn
|
|
59
|
+
|
|
60
|
+
# Ensure the directory exists
|
|
61
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
# Open connection
|
|
64
|
+
self._conn = sqlite3.connect(
|
|
65
|
+
str(self._db_path),
|
|
66
|
+
timeout=10.0,
|
|
67
|
+
check_same_thread=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Enable WAL mode for concurrent access
|
|
71
|
+
self._conn.execute("PRAGMA journal_mode = WAL")
|
|
72
|
+
self._conn.execute("PRAGMA synchronous = NORMAL")
|
|
73
|
+
self._conn.commit()
|
|
74
|
+
|
|
75
|
+
# Create schema
|
|
76
|
+
self._conn.executescript(_STATE_SCHEMA_SQL)
|
|
77
|
+
self._conn.commit()
|
|
78
|
+
|
|
79
|
+
self._initialized = True
|
|
80
|
+
logger.debug("SQLiteStateBackend initialized at %s", self._db_path)
|
|
81
|
+
return self._conn
|
|
82
|
+
|
|
83
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
84
|
+
"""Get a value by key, returning default if not found.
|
|
85
|
+
|
|
86
|
+
The stored JSON value is deserialized back to a Python object.
|
|
87
|
+
"""
|
|
88
|
+
conn = self._ensure_initialized()
|
|
89
|
+
cursor = conn.execute(
|
|
90
|
+
"SELECT value FROM kv_state WHERE key = ?",
|
|
91
|
+
(key,),
|
|
92
|
+
)
|
|
93
|
+
row = cursor.fetchone()
|
|
94
|
+
if row is None:
|
|
95
|
+
return default
|
|
96
|
+
return json.loads(row[0])
|
|
97
|
+
|
|
98
|
+
def set(self, key: str, value: Any) -> None:
|
|
99
|
+
"""Set a value for a key.
|
|
100
|
+
|
|
101
|
+
The value is JSON-encoded before storage.
|
|
102
|
+
"""
|
|
103
|
+
conn = self._ensure_initialized()
|
|
104
|
+
value_json = json.dumps(value)
|
|
105
|
+
now = time.time()
|
|
106
|
+
conn.execute(
|
|
107
|
+
"INSERT OR REPLACE INTO kv_state (key, value, updated_at) VALUES (?, ?, ?)",
|
|
108
|
+
(key, value_json, now),
|
|
109
|
+
)
|
|
110
|
+
conn.commit()
|
|
111
|
+
|
|
112
|
+
def delete(self, key: str) -> None:
|
|
113
|
+
"""Delete a key from the state backend.
|
|
114
|
+
|
|
115
|
+
No-op if the key does not exist.
|
|
116
|
+
"""
|
|
117
|
+
conn = self._ensure_initialized()
|
|
118
|
+
conn.execute("DELETE FROM kv_state WHERE key = ?", (key,))
|
|
119
|
+
conn.commit()
|
|
120
|
+
|
|
121
|
+
def keys(self, prefix: str = "") -> list[str]:
|
|
122
|
+
"""Return all keys, optionally filtered by prefix."""
|
|
123
|
+
conn = self._ensure_initialized()
|
|
124
|
+
if prefix:
|
|
125
|
+
cursor = conn.execute(
|
|
126
|
+
"SELECT key FROM kv_state WHERE key LIKE ? ESCAPE '\\'",
|
|
127
|
+
(self._escape_like(prefix) + "%",),
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
cursor = conn.execute("SELECT key FROM kv_state")
|
|
131
|
+
return [row[0] for row in cursor.fetchall()]
|
|
132
|
+
|
|
133
|
+
def close(self) -> None:
|
|
134
|
+
"""Close the database connection."""
|
|
135
|
+
if self._conn is not None:
|
|
136
|
+
try:
|
|
137
|
+
self._conn.close()
|
|
138
|
+
except sqlite3.Error as e:
|
|
139
|
+
logger.warning("Error closing SQLiteStateBackend connection: %s", e)
|
|
140
|
+
finally:
|
|
141
|
+
self._conn = None
|
|
142
|
+
self._initialized = False
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _escape_like(value: str) -> str:
|
|
146
|
+
"""Escape special characters in a LIKE pattern."""
|
|
147
|
+
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
148
|
+
|
|
149
|
+
def __enter__(self) -> SQLiteStateBackend:
|
|
150
|
+
"""Context manager entry — initialize if needed."""
|
|
151
|
+
self._ensure_initialized()
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, *exc: Any) -> None:
|
|
155
|
+
"""Context manager exit — close connection."""
|
|
156
|
+
self.close()
|
|
157
|
+
|
|
158
|
+
def __del__(self) -> None:
|
|
159
|
+
"""Ensure connection is closed on garbage collection."""
|
|
160
|
+
self.close()
|